diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 68a8eaefd9..05c93a6188 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -50,7 +50,8 @@ env: triggerLabelQuick: "tests-requested: quick" pythonVersion: '3.8' xcodeVersion: '16.2' - artifactRetentionDays: 2 + logArtifactRetentionDays: 90 + binaryArtifactRetentionDays: 7 GITHUB_TOKEN: ${{ github.token }} jobs: @@ -376,14 +377,14 @@ jobs: with: name: testapps-desktop-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.ssl_variant }} path: testapps-desktop-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.ssl_variant }} - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.binaryArtifactRetentionDays }} - name: Upload Desktop build results artifact uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: log-artifact-build-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.ssl_variant }} path: build-results-desktop-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.ssl_variant }}* - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Download log artifacts if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }} uses: actions/download-artifact@v4 @@ -496,14 +497,14 @@ jobs: with: name: testapps-android-${{ matrix.os }} path: testapps-android-${{ matrix.os }} - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.binaryArtifactRetentionDays }} - name: Upload Android build results artifact uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: log-artifact-build-android-${{ matrix.os }} path: build-results-android-${{ matrix.os }}* - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Download log artifacts if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }} uses: actions/download-artifact@v4 @@ -604,14 +605,14 @@ jobs: with: name: testapps-ios-${{ matrix.os }} path: testapps-ios-${{ matrix.os }} - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.binaryArtifactRetentionDays }} - name: Upload iOS build results artifact uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: log-artifact-build-ios-${{ matrix.os }} path: build-results-ios-${{ matrix.os }}* - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Download log artifacts if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }} uses: actions/download-artifact@v4 @@ -711,14 +712,14 @@ jobs: with: name: testapps-tvos-${{ matrix.os }} path: testapps-tvos-${{ matrix.os }} - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.binaryArtifactRetentionDays }} - name: Upload tvOS build results artifact uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: log-artifact-build-tvos-${{ matrix.os }} path: build-results-tvos-${{ matrix.os }}* - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Download log artifacts if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }} uses: actions/download-artifact@v4 @@ -848,7 +849,7 @@ jobs: with: name: log-artifact-test-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.ssl_variant }} path: testapps/test-results-desktop-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.ssl_variant }}* - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Download log artifacts if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }} uses: actions/download-artifact@v4 @@ -963,21 +964,21 @@ jobs: with: name: log-artifact-test-android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }} path: testapps/test-results-android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }}* - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Upload Android test video artifact if: ${{ steps.device-info.outputs.device_type == 'virtual' && !cancelled() }} uses: actions/upload-artifact@v4 with: name: mobile-simulator-test-video-artifact-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }} path: testapps/video-*-android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }}.mp4 - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.binaryArtifactRetentionDays }} - name: Upload Android test logcat artifact if: ${{ steps.device-info.outputs.device_type == 'virtual' && !cancelled() }} uses: actions/upload-artifact@v4 with: name: mobile-simulator-test-logcat-artifact-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }} path: testapps/logcat-*-android-${{ matrix.build_os }}-${{ matrix.android_device }}-${{ matrix.test_type }}.txt - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Download log artifacts if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }} uses: actions/download-artifact@v4 @@ -1149,14 +1150,14 @@ jobs: with: name: log-artifact-test-ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }} path: testapps/test-results-ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }}* - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Upload iOS test video artifact if: ${{ steps.device-info.outputs.device_type == 'virtual' && !cancelled() }} uses: actions/upload-artifact@v4 with: name: mobile-simulator-test-video-artifact-ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }} path: testapps/video-*-ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }}.mp4 - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.binaryArtifactRetentionDays }} - name: Download log artifacts if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }} uses: actions/download-artifact@v4 @@ -1289,14 +1290,14 @@ jobs: with: name: log-artifact-test-tvos-${{ matrix.build_os }}-${{ matrix.tvos_device }} path: testapps/test-results-tvos-${{ matrix.build_os }}-${{ matrix.tvos_device }}* - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.logArtifactRetentionDays }} - name: Upload tvOS test video artifact if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: mobile-simulator-test-video-artifact-tvos-${{ matrix.build_os }}-${{ matrix.tvos_device }} path: testapps/video-*-tvos-${{ matrix.build_os }}-${{ matrix.tvos_device }}.mp4 - retention-days: ${{ env.artifactRetentionDays }} + retention-days: ${{ env.binaryArtifactRetentionDays }} - name: Download log artifacts if: ${{ needs.check_and_prepare.outputs.pr_number && failure() && !cancelled() }} uses: actions/download-artifact@v4 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..c35a7e8958 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,679 @@ +# Introduction + +> **Note on Document Formatting:** This document (`AGENTS.md`) should be +> maintained with lines word-wrapped to a maximum of 80 characters to ensure +> readability across various editors and terminals. + +This document provides context and guidance for AI agents (like Jules) when +making changes to the Firebase C++ SDK repository. It covers essential +information about the repository's structure, setup, testing procedures, API +surface, best practices, and common coding patterns. + +For a detailed view of which Firebase products are supported on each C++ +platform (Android, iOS, tvOS, macOS, Windows, Linux), refer to the official +[Firebase library support by platform table](https://firebase.google.com/docs/cpp/learn-more#library-support-by-platform). + +The Firebase C++ SDKs for desktop platforms (Windows, Linux, macOS) are +entirely open source and hosted in the main `firebase/firebase-cpp-sdk` GitHub +repository. The C++ SDKs for mobile platforms (iOS, tvOS, Android) are built +on top of the respective native open-source Firebase SDKs (Firebase iOS SDK and +Firebase Android SDK). + +The goal is to enable agents to understand the existing conventions and +contribute effectively to the codebase. + +# Setup Commands + +## Prerequisites + +Before building the Firebase C++ SDK, ensure the following prerequisites are +installed. Refer to the main `README.md` for detailed installation +instructions for your specific platform. + +* **CMake**: Version 3.7 or newer. +* **Python**: Version 3.7 or newer. +* **Abseil-py**: Python package. +* **OpenSSL**: Required for desktop builds, unless you build with the + `-DFIREBASE_USE_BORINGSSL=YES` cmake flag. +* **libsecret-1-dev**: (Linux Desktop) Required for secure credential storage. + Install using `sudo apt-get install libsecret-1-dev`. +* **Android SDK & NDK**: Required for building Android libraries. `sdkmanager` + can be used for installation. CMake for Android (version 3.10.2 + recommended) is also needed. +* **(Windows Only) Strings**: From Microsoft Sysinternals, required for + Android builds on Windows. +* **Cocoapods**: Required for building iOS or tvOS libraries. + +To build for Desktop, you can install prerequisites by running the following +script in the root of the repository: `scripts/gha/install_prereqs_desktop.py` + +To build for Android, you can install prerequisites by running the following +script in the root of the repository: `build_scripts/android/install_prereqs.sh` + +## Building the SDK + +The SDK uses CMake for C++ compilation and Gradle for Android-specific parts. + +### CMake (Desktop, iOS, tvOS) + +1. Create a build directory (e.g., `mkdir desktop_build && cd desktop_build`). +2. Run CMake to configure: `cmake ..` + * For Desktop: Run as is. You can use BORINGSSL instead of OpenSSL (for fewer + system dependencies with the `-DFIREBASE_USE_BORINGSSL=YES` parameter. + * For iOS, include the `-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/ios.cmake` + parameter. This requires running on a Mac build machine. +3. Build specific targets: `cmake --build . --target firebase_analytics` + (replace `firebase_analytics` with the desired library). + Or omit the entire `--target` parameter to build all targets. + + For development, building specific targets + (e.g., `cmake --build . --target firebase_app`) is generally faster and + recommended once CMake configuration is complete. The full build + (`cmake --build .`) can be very time-consuming (but can be sped up by adding + `-j4` to the command-line). + +You can also use the `scripts/gha/build_desktop.py` script to build the full +desktop SDK. + +Refer to `README.md` for details on CMake generators and providing custom +third-party dependency locations. + +### Gradle (Android) + +Each Firebase C++ library is a Gradle subproject. To build a specific library +(e.g., Analytics): + +```bash +./gradlew :analytics:assembleRelease +``` + +This command should be run from the root of the repository. Proguard files are +generated in each library's build directory (e.g., +`analytics/build/analytics.pro`). + +You can build the entire SDK for Android by running `./gradlew build` or +`build_scripts/android/build.sh`. + +### Xcode (iOS) + +Unfortunately, the iOS version of the SDK cannot be built on Linux, it can only +be built in a MacOS environment. You will have to rely on GitHub Actions to +build for iOS, and have the user inform you of any build issues that come up. + +### Troubleshooting Desktop Builds + +* Linux: **Missing `libsecret-1-dev`**: + CMake configuration may fail if `libsecret-1-dev` is not installed. + The `scripts/gha/install_prereqs_desktop.py` script should handle this. + If it doesn't, or if the package is removed, you might need to install it + manually: `sudo apt-get update && sudo apt-get install -y libsecret-1-dev`. + +* Linux: **LevelDB Patch Failure when building Firestore**: + If you are building the SDK with Firestore enabled + (`-DFIREBASE_INCLUDE_FIRESTORE=ON`, which is the default for desktop) and + encounter a patch error related to `leveldb-1.23_windows_paths.patch` (e.g., + `util/env_windows.cc: patch does not apply`), you can ignore this issue if + it does not prevent the rest of the build from running. The patch is only + important on Windows. + +Common system library dependencies for desktop: +* **Windows**: Common dependencies include `advapi32.lib`, `ws2_32.lib`, + `crypt32.lib`. Specific products might need others (e.g., Firestore: + `rpcrt4.lib`, `ole32.lib`, `shell32.lib`). +* **macOS**: Common dependencies include `pthread` (system library) and + frameworks like `CoreFoundation`, `Foundation`, and `Security`. +* **Linux**: Common dependencies include `pthread` (system library). When + using GCC 5+, define `-D_GLIBCXX_USE_CXX11_ABI=0`. + +On all desktop platforms, building with -DFIREBASE_USE_BORINGSSL=YES can help +bypass any OpenSSL dependency issues. + +## Including the SDK in Projects + +### CMake Projects + +Use `add_subdirectory()` in your `CMakeLists.txt`: + +```cmake +add_subdirectory("[[Path to the Firebase C++ SDK]]") +target_link_libraries([[Your CMake Target]] firebase_analytics firebase_app) +``` + +### Android Gradle Projects + +In addition to CMake setup, use `Android/firebase_dependencies.gradle` in your +`build.gradle`: + +```gradle +apply from: "[[Path to the Firebase C++ SDK]]/Android/firebase_dependencies.gradle" +firebaseCpp.dependencies { + analytics +} +``` + +For more detailed instructions and examples, always refer to the main +`README.md` and the +[C++ Quickstarts](https://github.com/firebase/quickstart-cpp). + +# Testing + +## Testing Strategy + +The primary method for testing in this repository is through **integration +tests** for each Firebase library. While the `README.md` mentions unit tests +run via CTest, the current and preferred approach is to ensure comprehensive +coverage within the integration tests. + +## Running Tests + +* **Integration Test Location**: Integration tests for each Firebase product + (e.g., Firestore, Auth) are typically located in the `integration_test/` + directory within that product's module (e.g., + `firestore/integration_test/`). + + Because building integration tests requires internal google-services files, + Jules cannot do it in its environment; instead, we rely on GitHub Actions's + Integration Test workflow to build and run the integration tests. + +## Writing Tests + +When adding new features or fixing bugs: + +* Prioritize adding or updating integration tests within the respective + product's `integration_test/` directory. +* Ensure tests cover the new functionality thoroughly and verify interactions + with the Firebase backend or other relevant components. +* Follow existing patterns within the integration tests for consistency. + +# API Surface + +## General API Structure + +The Firebase C++ SDK exposes its functionality through a set of classes and +functions organized by product (e.g., Firestore, Authentication, Realtime +Database). + +### Initialization + +1. **`firebase::App`**: This is the central entry point for the SDK. + * It must be initialized first using `firebase::App::Create(...)`. + * On Android, this requires passing the JNI environment (`JNIEnv*`) and + the Android Activity (`jobject`). + * `firebase::AppOptions` can be used to configure the app with specific + parameters if not relying on a `google-services.json` or + `GoogleService-Info.plist` file. +2. **Service Instances**: Once `firebase::App` is initialized, you generally + obtain instances of specific Firebase services using a static + `GetInstance()` method on the service's class, passing the `firebase::App` + object. + * Examples for services like Auth, Database, Storage, Firestore: + * `firebase::auth::Auth* auth = firebase::auth::Auth::GetAuth(app, &init_result);` + * `firebase::database::Database* database = firebase::database::Database::GetInstance(app, &init_result);` + * `firebase::storage::Storage* storage = firebase::storage::Storage::GetInstance(app, &init_result);` + * Always check the `init_result` (an `InitResult` enum, often + `firebase::kInitResultSuccess` on success) to ensure these services + were initialized successfully. + * **Note on Analytics**: Some products, like Firebase Analytics, have a + different pattern. Analytics is typically initialized with + `firebase::analytics::Initialize(const firebase::App& app)` (often + handled automatically for the default `App` instance). After this, + Analytics functions (e.g., `firebase::analytics::LogEvent(...)`) are + called as global functions within the `firebase::analytics` namespace, + rather than on an instance object obtained via `GetInstance()`. + Refer to the specific product's header file for its exact + initialization mechanism if it deviates from the common + `GetInstance(app, ...)` pattern. + +### Asynchronous Operations: `firebase::Future` + +All asynchronous operations in the SDK return a `firebase::Future` object, +where `T` is the type of the expected result. + +* **Status Checking**: Use `future.status()` to check if the operation is + `kFutureStatusPending`, `kFutureStatusComplete`, or + `kFutureStatusInvalid`. +* **Getting Results**: Once `future.status() == kFutureStatusComplete`: + * Check for errors: `future.error()`. A value of `0` (e.g., + `firebase::auth::kAuthErrorNone`, + `firebase::database::kErrorNone`) usually indicates success. + * Get the error message: `future.error_message()`. + * Get the result: `future.result()`. This returns a pointer to the result + object of type `T`. The result is only valid if `future.error()` + indicates success. +* **Completion Callbacks**: Use `future.OnCompletion(...)` to register a + callback function (lambda or function pointer) that will be invoked when + the future completes. The callback receives the completed future as an + argument. +* k?????Fn_* enums: A list of each SDK's asynchronous functions is usually + kept in an enum in that SDK. For example, all of Auth's asynchronous + functions are named kAuthFn_* and kUserFn_*. Only asynchronous operations + (which return a Future) need to be in those function enums; these are used + internally to hold a reference to the FutureHandle for the *LastResult() + methods. If you add a new asynchronous operation, it should be added to + that enum, and that ID should be used for all of the internal FutureApi + operations. Non-async functions never need to touch this. +* Asynchronous functions ONLY: Only asynchronous functions need to use + the Future pattern, e.g. anything with a callback. If you are simply + calling an underlying SDK function that finishes its work and returns + immediately, with no callback, there is no need to use a Future. See + `STYLE_GUIDE.md` for more details on asynchronous operations. + +### Core Classes and Operations (Examples from Auth and Database) + +While each Firebase product has its own specific classes, the following +examples illustrate common API patterns: + +* **`firebase::auth::Auth`**: The main entry point for Firebase + Authentication. + * Used to manage users, sign in/out, etc. + * Successful authentication operations (like + `SignInWithEmailAndPassword()`) return a + `Future`. The `firebase::auth::User` + object can then be obtained from this `AuthResult` (e.g., + `auth_result.result()->user()` after `result()` is confirmed + successful and the pointer is checked). + * Example: `firebase::auth::User* current_user = auth->current_user();` + * Methods for user creation/authentication: + `CreateUserWithEmailAndPassword()`, `SignInWithEmailAndPassword()`, + `SignInWithCredential()`. +* **`firebase::auth::User`**: + * Represents a user account. Data is typically accessed via its methods. + * Methods for profile updates: `UpdateEmail()`, `UpdatePassword()`, + `UpdateUserProfile()`. + * Other operations: `SendEmailVerification()`, `Delete()`. +* **`firebase::database::Database`**: The main entry point for Firebase + Realtime Database. + * Used to get `DatabaseReference` objects to specific locations in the + database. +* **`firebase::database::DatabaseReference`**: + * Represents a reference to a specific location (path) in the Realtime + Database. + * Methods for navigation: `Child()`, `Parent()`, `Root()`. + * Methods for data manipulation: `GetValue()`, `SetValue()`, + `UpdateChildren()`, `RemoveValue()`. + * Methods for listeners: `AddValueListener()`, `AddChildListener()`. +* **`firebase::database::Query`**: + * Used to retrieve data from a Realtime Database location based on + specific criteria. Obtained from a `DatabaseReference`. + * **Filtering**: `OrderByChild()`, `OrderByKey()`, `OrderByValue()`, + `EqualTo()`, `StartAt()`, `EndAt()`. + * **Limiting**: `LimitToFirst()`, `LimitToLast()`. + * Execution: `GetValue()` returns a `Future`. +* **`firebase::database::DataSnapshot`**: Contains data read from a Realtime + Database location (either directly or as a result of a query). Accessed + via `future.result()` or through listeners. + * Methods: `value()` (returns a `firebase::Variant` representing the + data), `children_count()`, `children()`, `key()`, `exists()`. +* **`firebase::Variant`**: A type that can hold various data types like + integers, strings, booleans, vectors (arrays), and maps (objects), + commonly used for reading and writing data with Realtime Database and other + services. +* **Operation-Specific Options**: Some operations might take optional + parameters to control behavior, though not always through a dedicated + "Options" class like Firestore's `SetOptions`. For example, + `User::UpdateUserProfile()` takes a `UserProfile` struct. + +### Listeners for Real-time Updates + +Many Firebase services support real-time data synchronization using listeners. + +* **`firebase::database::ValueListener` / + `firebase::database::ChildListener`**: Implemented by the developer and + registered with a `DatabaseReference`. + * `ValueListener::OnValueChanged(const firebase::database::DataSnapshot& snapshot)` + is called when the data at that location changes. + * `ChildListener` has methods like `OnChildAdded()`, `OnChildChanged()`, + `OnChildRemoved()`. +* **`firebase::auth::AuthStateListener`**: Implemented by the developer and + registered with `firebase::auth::Auth`. + * `AuthStateListener::OnAuthStateChanged(firebase::auth::Auth* auth)` is + called when the user's sign-in state changes. +* **Removing Listeners**: Listeners are typically removed by passing the + listener instance to a corresponding `Remove...Listener()` method (e.g., + `reference->RemoveValueListener(my_listener);`, + `auth->RemoveAuthStateListener(my_auth_listener);`). + +This overview provides a general understanding. Always refer to the specific +header files in `firebase/app/client/cpp/include/firebase/` and +`firebase/product_name/client/cpp/include/firebase/product_name/` for detailed +API documentation. + +# Best Practices + +## Coding Style + +* **Firebase C++ Style Guide**: For specific C++ API design and coding + conventions relevant to this SDK, refer to the + [STYLE_GUIDE.md](STYLE_GUIDE.md). +* **Google C++ Style Guide**: Adhere to the + [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) + as mentioned in `CONTRIBUTING.md`. +* **Formatting**: Use `python3 scripts/format_code.py -git_diff -verbose` to + format your code before committing. +* **Naming Precision for Dynamic Systems**: Function names should precisely + reflect their behavior, especially in systems with dynamic or asynchronous + interactions. For example, a function that processes a list of items should + be named differently from one that operates on a single, specific item + captured asynchronously. Regularly re-evaluate function names as + requirements evolve to maintain clarity. + +## Comments + +* Write clear and concise comments where necessary to explain complex logic or + non-obvious behavior. +* **Avoid overly verbose comments**: Do not state the obvious. The code should + be as self-documenting as possible. +* Follow existing comment styles within the module you are working on. +* "Avoid adding comments next to `#include` directives merely to explain why + the include is necessary; the code usage should make this clear, or it can + be part of a broader comment block if truly non-obvious for a section of + code." +* "Do not include comments that narrate the AI agent's iterative development + process (e.g., 'Removed old logic here', 'Changed variable name from X to + Y because...', 'Attempted Z but it did not work'). Comments should focus on + explaining the current state of the code for future maintainers, not its + development history or the AI's thought process." + +## Error Handling + +* **Check `Future` status and errors**: Always check `future.status()` and + `future.error()` before attempting to use `future.result()`. + * A common success code is `0` (e.g., + `firebase::auth::kAuthErrorNone`, + `firebase::database::kErrorNone`). Other specific error codes are + defined per module (e.g., + `firebase::auth::kAuthErrorUserNotFound`). +* **Callback error parameters**: When using listeners or other callbacks, + always check the provided error code and message before processing the + data. +* Provide meaningful error messages if your code introduces new error + conditions. + +## Resource Management + +* **`firebase::Future`**: `Future` objects manage their own resources. + `Future::Release()` can be called, but it's often handled by RAII when + futures go out of scope. Be mindful of `Future` lifetimes if they are + stored as members or passed around. +* **ListenerRegistrations**: When a listener is no longer needed, call + `ListenerRegistration::Remove()` to detach it. Failure to do so can lead + to memory leaks and unnecessary network/CPU usage. +* **Pointers**: Standard C++ smart pointers (`std::unique_ptr`, + `std::shared_ptr`) should be used where appropriate for managing + dynamically allocated memory. +* **`Future` Lifecycle**: Ensure `Future` objects returned from API calls are + properly managed. While `Future`s handle their own internal memory for the + result, the asynchronous operations they represent need to complete to + reliably free all associated operational resources or to ensure actions + (like writes to a database) are definitely finalized. Abandoning a `Future` + (letting it go out of scope without checking its result, attaching an + `OnCompletion` callback, or explicitly `Wait()`ing for it) can sometimes + lead to operations not completing as expected or resources not being + cleaned up promptly by the underlying services, especially if the `Future` + is the only handle to that operation. Prefer using `OnCompletion` or + otherwise ensuring the `Future` completes its course, particularly for + operations with side effects or those that allocate significant backend + resources. +* **Lifecycle of Queued Callbacks/Blocks**: If blocks or callbacks are queued + to be run upon an asynchronous event (e.g., an App Delegate class being set + or a Future completing), clearly define and document their lifecycle. + Determine if they are one-shot (cleared after first execution) or + persistent (intended to run for multiple or future events). This impacts + how associated data and the blocks themselves are stored and cleared, + preventing memory leaks or unexpected multiple executions. + +## Immutability + +* Be aware that some objects, like `firebase::firestore::Query`, are + immutable. Methods that appear to modify them (e.g., `query.Where(...)`) + actually return a new instance with the modification. + +## Platform-Specific Code + +* Firebase C++ SDK supports multiple platforms (Android, iOS, tvOS, desktop - + Windows, macOS, Linux). +* Platform-specific code is typically organized into subdirectories: + * `android/` (for Java/JNI related code) + * `ios/` (for Objective-C/Swift interoperability code) + * `desktop/` (for Windows, macOS, Linux specific implementations) + * `common/` (for C++ code shared across all platforms) +* Use preprocessor directives (e.g., `#if FIREBASE_PLATFORM_ANDROID`, + `#if FIREBASE_PLATFORM_IOS`) to conditionally compile platform-specific + sections when necessary, but prefer separate implementation files where + possible for better organization. + +## Platform-Specific Considerations + +* **Realtime Database (Desktop)**: The C++ SDK for Realtime Database on + desktop platforms (Windows, macOS, Linux) uses a REST-based + implementation. This means that any queries involving + `Query::OrderByChild()` require corresponding indexes to be defined in your + Firebase project's Realtime Database rules. Without these indexes, queries + may fail or not return expected results. +* **iOS Method Swizzling**: Be aware that some Firebase products on iOS + (e.g., Dynamic Links, Cloud Messaging) use method swizzling to + automatically attach handlers to your `AppDelegate`. While this simplifies + integration, it can occasionally be a factor to consider when debugging app + delegate behavior or integrating with other libraries that also perform + swizzling. + When implementing or interacting with swizzling, especially for App Delegate + methods like `[UIApplication setDelegate:]`: + * Be highly aware that `setDelegate:` can be called multiple times + with different delegate class instances, including proxy classes + from other libraries (e.g., GUL - Google Utilities). Swizzling + logic must be robust against being invoked multiple times for the + same effective method on the same class or on classes in a + hierarchy. An idempotency check (i.e., if the method's current IMP + is already the target swizzled IMP, do nothing more for that + specific swizzle attempt) in any swizzling utility can prevent + issues like recursion. + * When tracking unique App Delegate classes (e.g., for applying hooks + or callbacks via swizzling), consider the class hierarchy. If a + superclass has already been processed, processing a subclass for + the same inherited methods might be redundant or problematic. A + strategy to check if a newly set delegate is a subclass of an + already processed delegate can prevent such issues. + * For code that runs very early in the application lifecycle on + iOS/macOS (e.g., `+load` methods, static initializers involved in + swizzling), prefer using `NSLog` directly over custom logging + frameworks if there's any uncertainty about whether the custom + framework is fully initialized, to avoid crashes during logging + itself. + +## Class and File Structure + +* Follow the existing pattern of internal and common classes within each + Firebase library (e.g., `firestore/src/common`, `firestore/src/main`, + `firestore/src/android`). +* Public headers defining the API are typically in + `src/include/firebase/product_name/`. +* Internal implementation classes often use the `Internal` suffix (e.g., + `DocumentReferenceInternal`). + +# Common Patterns + +## Pimpl (Pointer to Implementation) Idiom + +* Many public API classes (e.g., `firebase::storage::StorageReference`, + `firebase::storage::Metadata`) use the Pimpl idiom. +* They hold a pointer to an internal implementation class (e.g., + `StorageReferenceInternal`, `MetadataInternal`). +* This pattern helps decouple the public interface from its implementation, + reducing compilation dependencies and hiding internal details. +* When working with these classes, you will primarily interact with the public + interface. Modifications to the underlying implementation are done in the + `*Internal` classes. + +A common convention for this Pimpl pattern in the codebase (e.g., as seen in +`firebase::storage::Metadata`) is that the public class owns the raw pointer to +its internal implementation (`internal_`). This requires careful manual memory +management: +* **Creation**: The `internal_` object is typically allocated with `new` in + the public class's constructor(s). For instance, the default constructor + might do `internal_ = new ClassNameInternal(...);`, and a copy constructor + would do `internal_ = new ClassNameInternal(*other.internal_);` for a + deep copy. +* **Deletion**: The `internal_` object is `delete`d in the public class's + destructor (e.g., `delete internal_;`). It's good practice to set + `internal_ = nullptr;` immediately after deletion if the destructor isn't + the absolute last point of usage, or if helper delete functions are used + (as seen with `MetadataInternalCommon::DeleteInternal` which nullifies the + pointer in the public class instance before deleting). +* **Copy Semantics**: + * **Copy Constructor**: If supported, a deep copy is usually performed. A + new `internal_` instance is created, and the contents from the source + object's `internal_` are copied into this new instance. + * **Copy Assignment Operator**: If supported, it must also perform a deep + copy. This typically involves deleting the current `internal_` object, + then creating a new `internal_` instance and copying data from the + source's `internal_` object. Standard self-assignment checks should be + present. +* **Move Semantics**: + * **Move Constructor**: If supported, ownership of the `internal_` pointer + is transferred from the source object to the new object. The source + object's `internal_` pointer is then set to `nullptr` to prevent + double deletion. + * **Move Assignment Operator**: Similar to the move constructor, it + involves deleting the current object's `internal_`, then taking + ownership of the source's `internal_` pointer, and finally setting the + source's `internal_` to `nullptr`. +* **Cleanup Registration (Advanced)**: In some cases, like + `firebase::storage::Metadata`, there might be an additional registration + with a central cleanup mechanism (e.g., via `StorageInternal`). This acts + as a safeguard or part of a larger resource management strategy within the + module, but the fundamental responsibility for creation and deletion in + typical scenarios lies with the Pimpl class itself. + +It's crucial to correctly implement all these aspects (constructors, +destructor, copy/move operators) when dealing with raw pointer Pimpls to +prevent memory leaks, dangling pointers, or double deletions. + +## Namespace Usage + +* Code for each Firebase product is typically within its own namespace under + `firebase`. + * Example: `firebase::firestore`, `firebase::auth`, + `firebase::database`. +* Internal or platform-specific implementation details might be in nested + namespaces like `firebase::firestore::internal` or + `firebase::firestore::android`. + +## Futures for Asynchronous Operations + +* As detailed in the API Surface section, `firebase::Future` is the + standard way all asynchronous operations return their results. +* Familiarize yourself with `Future::status()`, `Future::error()`, + `Future::error_message()`, `Future::result()`, and + `Future::OnCompletion()`. + +## Internal Classes and Helpers + +* Each module often has a set of internal classes (often suffixed with + `Internal` or residing in an `internal` namespace) that manage the core + logic, platform-specific interactions, and communication with the Firebase + backend. +* Utility functions and helper classes are common within the `common/` or + `util/` subdirectories of a product. + +## Singleton Usage (Limited) + +* The primary singleton is `firebase::App::GetInstance()`. +* Service entry points like `firebase::firestore::Firestore::GetInstance()` + also provide singleton-like access to service instances (scoped to an `App` + instance). +* Beyond these entry points, direct creation or use of singletons for core + data objects or utility classes is not a dominant pattern. Dependencies are + generally passed explicitly. + +## Platform Abstraction + +* For operations that differ by platform, there's often a common C++ + interface defined in `common/` or `main/`, with specific implementations + in `android/`, `ios/`, and `desktop/` directories. +* JNI is used extensively in `android/` directories for C++ to Java + communication. +* Objective-C++ (`.mm` files) is used in `ios/` directories for C++ to + Objective-C communication. + +## Listener Pattern + +* The `AddSnapshotListener`/`ListenerRegistration` pattern is common for + features requiring real-time updates (e.g., Firestore, Realtime + Database). + +## Builder/Fluent Interface for Queries + +* Classes like `firebase::firestore::Query` use a fluent interface (method + chaining) to construct complex queries by progressively adding filters, + ordering, and limits. Each call typically returns a new instance of the + query object. + +# Updating This Document + +This document is a living guide. As the Firebase C++ SDK evolves, new patterns +may emerge, or existing practices might change. If you introduce a new common +pattern, significantly alter a build process, or establish a new best practice +during your work, please take a moment to update this `AGENTS.md` file +accordingly. + +Keeping this document current will greatly benefit future AI agents and human +developers working on this repository. + +# Prompting Jules AI + +## Recommended General Prompt Instruction + +When working on this task, please consistently refer to the `AGENTS.md` guide +for all repository-specific conventions, including setup procedures, coding +style, common architectural patterns, and API usage. Pay close attention to the +testing strategies outlined, ensuring your implementation includes +comprehensive integration tests with detailed test cases in your plan. Implement +robust error handling for any new or modified public API methods, following the +patterns for `firebase::Future` error reporting. If the feature involves +platform-specific code, ensure the public API remains consistent across all +platforms, as discussed in `AGENTS.md`. Write clear, maintainable code, +adhering to the commenting guidelines, and if you need to add new third-party +dependencies, document the rationale and update build configurations according +to the established practices. Ensure your changes align with the overall best +practices detailed in `AGENTS.md`. + +## Key Directives for Jules AI + +* **Prioritize `AGENTS.md`**: This document (`AGENTS.md`) contains + repository-specific guidelines. Prioritize this information when making + decisions about coding style, testing procedures, architectural patterns, + and API usage. +* **Adherence to Patterns**: Strive to follow the common patterns and best + practices outlined here. This ensures consistency across the codebase. +* **Clarification for Deviations**: If the existing patterns in `AGENTS.md` do + not seem suitable for a specific task, or if a deviation is necessary, + please explicitly state this in your plan or request clarification before + proceeding with implementation. +* **Updating `AGENTS.md`**: If you introduce a new, broadly applicable + pattern, or if a significant change to the build process or best practices + occurs as part of your task, please include a step in your plan to update + `AGENTS.md` to reflect these changes. +* **Testing**: Remember that integration tests are the primary method of + testing. Ensure new features are accompanied by corresponding integration + tests as described in the 'Testing' section of `AGENTS.md`. +* **Commit Messages**: Follow standard commit message guidelines. A brief + summary line, followed by a more detailed explanation if necessary. +* **Tool Limitations & Path Specificity**: If codebase search tools (like + `grep` or recursive `ls`) are limited or unavailable, and initial attempts + to locate files/modules based on common directory structures are + unsuccessful, explicitly ask for more specific paths rather than assuming a + module doesn't exist or contains no relevant code. + +## GitHub Code Review Comments -- IMPORTANT, JULES READ THIS SECTION + +If the user asks you to look at GitHub code review comments, PR reviews, etc, +that means that you should run the following command: + +`python3 scripts/print_github_reviews.py --branch BRANCH_NAME_THAT_JULES_PUSHED_TO_GITHUB` + +(with the remote branch name that was actually pushed to GitHub substituted, and +if you aren't sure what it is or it returns an error, just ask the user, as they +may have renamed the branch before pushing) and then address the comments that +the output shows. If it succeeds, then subsequent times you run the script on +the same branch, include the `--since` parameter in accordance with the previous +script run's output to ensure you only fetch new comments. diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md new file mode 100644 index 0000000000..a141ee0b66 --- /dev/null +++ b/STYLE_GUIDE.md @@ -0,0 +1,378 @@ +# C++ API Guidelines + +**WIP** - *please feel free to improve* + +Intended for Firebase APIs, but also applicable to any C++ or Game APIs. + +# Code Style + +Please comply with the +[Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) +as much as possible. Refresh your memory of this document before you start :) + +# C++ API Design + +### Don't force any particular usage pattern upon the client. + +C++ is a huge language, with a great variety of ways in which things can be +done, compared to other languages. As a consequence, C++ projects can be very +particular about what features of the language they use or don't use, how they +represent their data, and structure their code. + +An API that forces the use of a feature or structure the client doesn't use +will be very unwelcome. A good API uses only the simplest common denominator +data types and features, and will be useable by all. This can generally be +done with minimal impact on your API’s simplicity, or at least should form +the baseline API. + +Examples of typical Do's and Don'ts: + +* Don't force the use of a particular data structure to supply or receive + data. Typical examples: + * `std::vector`: If the client doesn't have the data already in a + `std::vector` (or only wants to use part of a vector), they are forced + to copy/allocate a new one, and C++ programmers don't like unnecessary + copies/allocations. + Instead, your primary interface should always take a + `(const T *, size_t)` instead. You can still supply an optional helper + method that takes a `std::vector &` and then calls the former if + you anticipate it to be called very frequently. + * `std::string`: Unlike Java, these things aren't pooled, they're mutable + and copied. A common mistake is to take a `const std::string &` + argument, which forces all callers that supply a `const char *` to go + thru a strlen+malloc+copy that is possibly of no use to the callee. + Prefer to take a `const char *` instead for things that are + names/identifiers, especially if they possibly are compile-time + constant. If you're unsetting a string property, prefer to pass + nullptr rather than an empty string. (There are + [COW implementations](https://en.wikipedia.org/wiki/Copy-on-write), + but you can't rely on that). + * `std::map`: This is a costly data structure involving many + allocations. If all you wanted is for the caller to supply a list of + key/value pairs, take a `const char **` (yes, 2 stars!). Or + `const SimpleStruct *` instead, which allows the user to create this + data statically. +* Per-product configuration should be accomplished using an options struct + passed to the library's `firebase::::Initialize` function. + Default options should be provided by the options struct's default + constructor. The `Initialize` function should be overloaded with a version + that does not take the options struct (which is how the Google style guide + prefers that we pass default parameters). + + For example, + +```c++ + struct LibraryOptions { + LibraryOptions() : do_the_thing(false) {} + + bool do_the_thing; + }; + + InitResult Initialize(const App& app) { + return Initialize(app, LibraryOptions()); + } + InitResult Initialize(const App& app, const LibraryOptions& options); +``` + +* Don't make your client responsible for data you allocate or take ownership + of client data. Typical C++ APIs are written such that both the client + and the library own their own memory, and they take full responsibility + for managing it, regardless of what the other does. Any data exchanged is + typically done through weak references and copies. + An exception may be a file loading API where buffers exchanged may be + really big. If you are going to pass ownership, make this super obvious + in all comments and documentation (C++ programmers typically won't + expect it), and document which function should be used to free the data + (free, delete, or a custom one). + Alternatively, a simple way to pass ownership of a large new buffer to + the client is to ask the client to supply a std::string *, which you then + resize(), and write directly into its owned memory. This somewhat + violates the rule about use of std::string above, though. + +* Don't use exceptions. This one is worth mentioning separately. Though + exceptions are great in theory, in C++ hardly any libraries use them, and + many code-bases disallow them entirely. They also require the use of RTTI + which some environments turn off. Oh, yes, also don't use RTTI. + +* Go easy on templates when possible. Yes, they make your code more general, + but they also pull a lot of implementation detail into your API, lengthen + compile times and bloat binaries. In C++ they are glorified macros, so + they result in hard to understand errors, and can make correct use of + your API harder to understand. + +* Utilize C++11 features where appropriate. This project has adopted C++11, + and features such as `std::unique_ptr`, `std::shared_ptr`, + `std::make_unique`, and `std::move` are encouraged to improve code safety + and readability. However, avoid features from C++14 or newer standards. + +* Go easy on objectifying everything, and prefer value types. In languages + like Java it is common to give each "concept" your API deals with its own + class, such that methods on it have a nice home. In C++ this isn't + always desirable, because objects need to be managed, stored and + allocated, and you run into ownership/lifetime questions mentioned above. + Instead: + + * For simple data, prefer their management to happen in the parent + class that owns them. Actions on them are methods in the parent. If + at all possible, prefer not to refer to them by pointer/reference + (which creates ownership and lifetime issues) but by index/id, or + string if not performance sensitive (for example, when referring to + file resources, since the cost of loading a file dwarfs the cost of + a string lookup). + + * If you must create objects, and objects are not heavyweight (only + scalars and non-owned pointers), make use of these objects by value + (return by value, receive by const reference). This makes ownership + and lifetime management trivial and efficient. + +* If at all possible, don't depend on external libraries. C++ compilation, + linking, dependency management, testing (especially cross platform) are + generally way harder than any other language. Every dependency is a + potential source of build complexity, conflicts, efficiency issues, and + in general more dependencies means less adoption. + + * Don't pull in large libraries (e.g. BOOST) just for your + convenience, especially if their use is exposed in headers. + + * Only use external libraries that have hard to replicate essential + functionality (e.g. compression, serialization, image loading, + networking etc.). Make sure to only access them in implementation + files. + + * If possible, make a dependency optional, e.g. if what your API does + benefits from compression, make the client responsible for doing so, + or add an interface for it. Add sample glue code or an optional API + for working with the external library that is by default off in the + build files, and can be switched on if desired. + +* Take cross-platform-ness seriously: design the API to work on ALL + platforms even if you don't intend to supply implementations for all. + Hide platform issues in the implementation. Don't ever include platform + specific headers in your own headers. Have graceful fallback for + platforms you don't support, such that some level of building / testing + can happen anywhere. + +* If your API is meant to be VERY widespread in use, VERY general, and very + simple (e.g. a compression API), consider making at least the API (if + not all of it) in C, as you'll reach an even wider audience. C has a + more consistent ABI and is easier to access from a variety of systems / + languages. This is especially useful if the library implementation is + intended to be provided in binary. + +* Be careful not to to use idioms from other languages that may be foreign + to C++. + + * An example of this is a "Builder" API (common in Java). Prefer to + use regular constructors, with additional set_ methods for less + common parameters if the number of them gets overwhelming. + +* Do not expose your own UI to the user as part of an API. Give the + developer the data to work with, and let them handle displaying it to + the user in the way they see fit. + + * Rare exceptions can be made to this rule on a case-by-case basis. + For example, authentication libraries may need to display a sign-in + UI for the user to enter their credentials. Your API may work with + data owned by Google or by the user (e.g. the user's contacts) that + we don't want to expose to the app; in those cases, it is + appropriate to expose a UI (but to limit the scope of the UI to the + minimum necessary). + + * In these types of exceptional cases, the UI should be in an isolated + component, separate from the rest of the API. We do allow UIs to be + exposed to the user UI-specific libraries, e.g. FirebaseUI, which + should be open-source so developers can apply any customizations + they need. + +# Game API Design + +### Performance matters + +Most of this is already encoded in C++ API design above, but it bears +repeating: C++ game programmers can be more fanatic about performance than +you expect. + +It is easy to add a layer of usability on top of fast code, it is very +hard to impossible to "add performance" to an API that has performance +issues baked into its design. + +### Don't rely on state persisting for longer than one frame. + +Games have an interesting program structure very unlike apps or web pages: +they do all processing (and rendering) of almost all functionality of the +game within a *frame* (usually 1/60th of a second), and then start anew +for the next frame. + +It is common for all or part of the state of a game to be wiped out from +one frame to the next (e.g when going into the menu, loading a new level, +starting a cut-scene..). + +The consequence of this is that the state kept between frames is the only +record of what is currently going on, and that managing this state is a +source of complexity, especially when part of it is reflected in external +code: + +* Prefer API design that is stateless, or if it is stateful, is so only + within a frame (i.e. between the start of the frame and the start of + the next one). This really simplifies the client's use of your API: + they can't forget to "reset" your API's state whenever they change + state themselves. + +* Prefer not to use cross-frame callbacks at all (non-escaping callbacks + are fine). Callbacks can be problematic in other contexts, but they're + even more problematic in games. Since they will execute at a future + time, there's no guarantee that the state that was there when the + callback started will still be there. There's no easy way to robustly + "clear" pending callbacks that don't make sense anymore when state + changes. Instead, make your API based on *polling*. + Yes, everyone learned in school that polling is bad because it uses + CPU, but that's what games are based on anyway: they check a LOT of + things every frame (and only at 60hz, which is very friendly compared + to busy-wait polling). If your API can be in various states of a state + machine (e.g. a networking based API), make sure the client can poll + the state you're in. This can then easily be translated to user + feedback. + If you have to use asynchronous callbacks, see the section on async + operations below. + +* Be robust to the client needing to change state. If work done in your + API involves multiple steps, and the client never gets to the end of + those steps before starting a new sequence, don't be "stuck", but deal + with this gracefully. If the game's state got reset, it will have no + record of what it was doing before. Try to not make the client + responsible for knowing what it was doing. + +* Interaction with threading: + + * If you are going to use threading at all, make sure the use of that + is internal to the library, and any issues of thread-safety don't + leak into the client. Allow what appears to be synchronous access + to a possibly asynchronous implementation. If the asynchronous + nature will be externally visible, see the section on async + operations below. + + * Games are typically hard to thread (since it’s hard to combine with + its per-frame nature), so the client typically should have full + control over it: it is often better to make a fully synchronous + single-threaded library and leave threading it to the client. Do + not try to make your API itself thread-safe, as your API is + unlikely the threading boundary (if your client is threaded, use + of your library is typically isolated to one thread, and they do + not want to pay for synchronization cost they don't use). + + * When you do spin up threads to reduce a workload, it is often a + good idea to do that once per frame, as avoid the above mentioned + state based problems, and while starting threads isn't cheap, you + may find it not a problem to do 60x per second. Alternatively you + can pool them, and make sure you have an explicit way to wait for + their idleness at the end of a frame. + +* Games typically use their own memory allocator (for efficiency, but + also to be able to control and budget usage on memory constrained + systems). For this reason, most game APIs tend to provide allocation + hooks that will be used for all internal allocation. This is even more + important if you wish to be able to transfer ownership of memory. + +* Generally prefer solutions that are low on total memory usage. Games + are always constrained on memory, and having your game be killed by + the OS because the library you use has decided it is efficient to + cache everything is problematic. + + * Prefer to recompute values when possible. + + * When you do cache, give the client control over total memory used + for this purpose. + + * Your memory usage should be predictable and ideally have no peaks. + +# Async Operations + +### Application Initiated Async Operations + +* Use the Future / State Pattern. +* Add a `*LastResult()` method for each async operation method to allow + the caller to poll and not save state. + +e.g. + +```c++ + // Start async operation. + Future SignInWithCredential(...); + // Get the result of the pending / last async operation for the method. + Future SignInWithCredentialLastResult(); + + Usage examples: + // call and register callback + auto& result = SignInWithCredential(); + result.set_callback([](result) { if (result == kComplete) { do_something_neat(); wake_up(); } }); + // wait + + // call and poll #1 (saving result) + auto& result = SignInWithCredential(); + while (result.value() != kComplete) { + // wait + } + + // call and poll #2 (result stored in API) + SignInWithCredential(); + while (SignInWithCredentialLastResult().value() != kComplete) { + } +``` + +### API Initiated Async Event Handling + +* Follow the + [listener / observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) + for API initiated (i.e where the caller doesn't initiate the event) + async events. +* Provide a queued interface to allow users to poll for events. + +e.g. + +```c++ + class GcmListener { + public: + virtual void OnDeletedMessage() {} + virtual void OnMessageReceived(const MessageReceivedData* data) {} + }; + + class GcmListenerQueue : private GcmListener { + public: + enum EventType { + kEventTypeMessageDeleted, + kEventTypeMessageReceived, + }; + + struct Event { + EventType type; + MessageReceivedData data; // Set when type == kEventTypeMessageReceived + }; + + // Returns true when an event is retrieved from the queue. + bool PollEvent(Event *event); + }; + + // Wait for callbacks + class MyListener : public GcmListener { + public: + virtual void OnDeletedMessage() { /* do stuff */ } + virtual void OnMessageReceived() { /* display message */ } + }; + MyListener listener; + gcm::Initialize(app, &listener); + + // Poll + GcmListenerQueue queued_listener; + gcm::Initialize(app, &queued_listener); + GcmListenerQueue::Event event; + while (queued_listener(&event)) { + switch (event.type) { + case kEventTypeMessageDeleted: + // do stuff + break; + case kEventTypeMessageReceived: + // display event.data + break; + } + } +``` diff --git a/analytics/generate_windows_stubs.py b/analytics/generate_windows_stubs.py new file mode 100755 index 0000000000..4ef2a76b64 --- /dev/null +++ b/analytics/generate_windows_stubs.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generate stubs and function pointers for Windows SDK""" + +import argparse +import os +import re +import sys + +HEADER_GUARD_PREFIX = "FIREBASE_ANALYTICS_SRC_WINDOWS_" +INCLUDE_PATH = "src/windows/" +INCLUDE_PREFIX = "analytics/" + INCLUDE_PATH +COPYRIGHT_NOTICE = """// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +""" + +def generate_function_pointers(header_file_path, output_h_path, output_c_path): + """ + Parses a C header file to generate a self-contained header with typedefs, + extern function pointer declarations, and a source file with stub functions, + initialized pointers, and a dynamic loading function for Windows. + + Args: + header_file_path (str): The path to the input C header file. + output_h_path (str): The path for the generated C header output file. + output_c_path (str): The path for the generated C source output file. + """ + print(f"Reading header file: {header_file_path}") + try: + with open(header_file_path, 'r', encoding='utf-8') as f: + header_content = f.read() + except FileNotFoundError: + print(f"Error: Header file not found at '{header_file_path}'") + return + + # --- Extract necessary definitions from the original header --- + + # Find all standard includes (e.g., ) + includes = re.findall(r"#include\s+<.*?>", header_content) + + # Find all typedefs, including their documentation comments + typedefs = re.findall(r"/\*\*(?:[\s\S]*?)\*/\s*typedef[\s\S]*?;\s*", header_content) + + # --- Extract function prototypes --- + function_pattern = re.compile( + r"ANALYTICS_API\s+([\w\s\*]+?)\s+(\w+)\s*\((.*?)\);", + re.DOTALL + ) + matches = function_pattern.finditer(header_content) + + extern_declarations = [] + macro_definitions = [] + stub_functions = [] + pointer_initializations = [] + function_details_for_loader = [] + + for match in matches: + return_type = match.group(1).strip() + function_name = match.group(2).strip() + params_str = match.group(3).strip() + + cleaned_params_for_decl = re.sub(r'\s+', ' ', params_str) if params_str else "" + stub_name = f"Stub_{function_name}" + + # Generate return statement for the stub + if "void" in return_type: + return_statement = " // No return value." + elif "*" in return_type: + return_statement = f' return ({return_type})(&g_stub_memory);' + else: # bool, int64_t, etc. + return_statement = " return 1;" + + stub_function = ( + f"// Stub for {function_name}\n" + f"static {return_type} {stub_name}({params_str}) {{\n" + f"{return_statement}\n" + f"}}" + ) + stub_functions.append(stub_function) + + declaration = f"extern {return_type} (*ptr_{function_name})({cleaned_params_for_decl});" + extern_declarations.append(declaration) + + macro = f'#define {function_name} ptr_{function_name}' + macro_definitions.append(macro) + + pointer_init = f"{return_type} (*ptr_{function_name})({cleaned_params_for_decl}) = &{stub_name};" + pointer_initializations.append(pointer_init) + + function_details_for_loader.append((function_name, return_type, cleaned_params_for_decl)) + + print(f"Found {len(pointer_initializations)} functions. Generating output files...") + + # --- Write the self-contained Header File (.h) --- + header_guard = f"{HEADER_GUARD_PREFIX}{os.path.basename(output_h_path).upper().replace('.', '_')}_" + with open(output_h_path, 'w', encoding='utf-8') as f: + f.write(f"{COPYRIGHT_NOTICE}") + f.write(f"// Generated from {os.path.basename(header_file_path)} by {os.path.basename(sys.argv[0])}\n\n") + f.write(f"#ifndef {header_guard}\n") + f.write(f"#define {header_guard}\n\n") + f.write("#include // needed for bool type in pure C\n\n") + + f.write("// --- Copied from original header ---\n") + f.write("\n".join(includes) + "\n\n") + f.write("".join(typedefs)) + f.write("// --- End of copied section ---\n\n") + + f.write("#ifdef __cplusplus\n") + f.write('extern "C" {\n') + f.write("#endif\n\n") + f.write("// --- Function Pointer Declarations ---\n") + f.write("// clang-format off\n") + f.write("\n".join(extern_declarations)) + f.write("\n\n") + f.write("\n".join(macro_definitions)) + f.write("\n// clang-format on\n") + f.write("\n\n// --- Dynamic Loader Declaration for Windows ---\n") + f.write("#if defined(_WIN32)\n") + f.write('#include // For HMODULE\n') + f.write('// Load Google Analytics functions from the given DLL handle into function pointers.\n') + f.write(f'// Returns the number of functions successfully loaded (out of {len(function_details_for_loader)}).\n') + f.write("int FirebaseAnalytics_LoadAnalyticsFunctions(HMODULE dll_handle);\n\n") + f.write('// Reset all function pointers back to stubs.\n') + f.write("void FirebaseAnalytics_UnloadAnalyticsFunctions(void);\n\n") + f.write("#endif // defined(_WIN32)\n") + f.write("\n#ifdef __cplusplus\n") + f.write("}\n") + f.write("#endif\n\n") + f.write(f"#endif // {header_guard}\n") + + print(f"Successfully generated header file: {output_h_path}") + + # --- Write the Source File (.c) --- + with open(output_c_path, 'w', encoding='utf-8') as f: + f.write(f"{COPYRIGHT_NOTICE}") + f.write(f"// Generated from {os.path.basename(header_file_path)} by {os.path.basename(sys.argv[0])}\n\n") + f.write(f'#include "{INCLUDE_PREFIX}{os.path.basename(output_h_path)}"\n') + f.write('#include \n\n') + f.write("// clang-format off\n\n") + f.write("static void* g_stub_memory = NULL;\n\n") + f.write("// --- Stub Function Definitions ---\n") + f.write("\n\n".join(stub_functions)) + f.write("\n\n\n// --- Function Pointer Initializations ---\n") + f.write("\n".join(pointer_initializations)) + f.write("\n\n// --- Dynamic Loader Function for Windows ---\n") + loader_lines = [ + '#if defined(_WIN32)', + 'int FirebaseAnalytics_LoadAnalyticsFunctions(HMODULE dll_handle) {', + ' int count = 0;\n', + ' if (!dll_handle) {', + ' return count;', + ' }\n' + ] + for name, ret_type, params in function_details_for_loader: + pointer_type_cast = f"({ret_type} (*)({params}))" + proc_check = [ + f' FARPROC proc_{name} = GetProcAddress(dll_handle, "{name}");', + f' if (proc_{name}) {{', + f' ptr_{name} = {pointer_type_cast}proc_{name};', + f' count++;', + f' }}' + ] + loader_lines.extend(proc_check) + loader_lines.append('\n return count;') + loader_lines.append('}\n') + loader_lines.append('void FirebaseAnalytics_UnloadAnalyticsFunctions(void) {') + for name, ret_type, params in function_details_for_loader: + loader_lines.append(f' ptr_{name} = &Stub_{name};'); + loader_lines.append('}\n') + loader_lines.append('#endif // defined(_WIN32)\n') + f.write('\n'.join(loader_lines)) + f.write("// clang-format on\n") + + print(f"Successfully generated C source file: {output_c_path}") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description="Generate C stubs and function pointers from a header file." + ) + parser.add_argument( + "--windows_header", + default = os.path.join(os.path.dirname(sys.argv[0]), "windows/include/public/c/analytics.h"), + #required=True, + help="Path to the input C header file." + ) + parser.add_argument( + "--output_header", + default = os.path.join(os.path.dirname(sys.argv[0]), INCLUDE_PATH, "analytics_dynamic.h"), + #required=True, + help="Path for the generated output header file." + ) + parser.add_argument( + "--output_source", + default = os.path.join(os.path.dirname(sys.argv[0]), INCLUDE_PATH, "analytics_dynamic.c"), + #required=True, + help="Path for the generated output source file." + ) + + args = parser.parse_args() + + generate_function_pointers( + args.windows_header, + args.output_header, + args.output_source + ) diff --git a/analytics/src/windows/analytics_dynamic.c b/analytics/src/windows/analytics_dynamic.c new file mode 100644 index 0000000000..d7f483b72d --- /dev/null +++ b/analytics/src/windows/analytics_dynamic.c @@ -0,0 +1,284 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated from analytics.h by generate_windows_stubs.py + +#include "analytics/src/windows/analytics_dynamic.h" +#include + +// clang-format off + +static void* g_stub_memory = NULL; + +// --- Stub Function Definitions --- +// Stub for GoogleAnalytics_Item_Create +static GoogleAnalytics_Item* Stub_GoogleAnalytics_Item_Create() { + return (GoogleAnalytics_Item*)(&g_stub_memory); +} + +// Stub for GoogleAnalytics_Item_InsertInt +static void Stub_GoogleAnalytics_Item_InsertInt(GoogleAnalytics_Item* item, + const char* key, + int64_t value) { + // No return value. +} + +// Stub for GoogleAnalytics_Item_InsertDouble +static void Stub_GoogleAnalytics_Item_InsertDouble(GoogleAnalytics_Item* item, + const char* key, + double value) { + // No return value. +} + +// Stub for GoogleAnalytics_Item_InsertString +static void Stub_GoogleAnalytics_Item_InsertString(GoogleAnalytics_Item* item, + const char* key, + const char* value) { + // No return value. +} + +// Stub for GoogleAnalytics_Item_Destroy +static void Stub_GoogleAnalytics_Item_Destroy(GoogleAnalytics_Item* item) { + // No return value. +} + +// Stub for GoogleAnalytics_ItemVector_Create +static GoogleAnalytics_ItemVector* Stub_GoogleAnalytics_ItemVector_Create() { + return (GoogleAnalytics_ItemVector*)(&g_stub_memory); +} + +// Stub for GoogleAnalytics_ItemVector_InsertItem +static void Stub_GoogleAnalytics_ItemVector_InsertItem(GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item) { + // No return value. +} + +// Stub for GoogleAnalytics_ItemVector_Destroy +static void Stub_GoogleAnalytics_ItemVector_Destroy(GoogleAnalytics_ItemVector* item_vector) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_Create +static GoogleAnalytics_EventParameters* Stub_GoogleAnalytics_EventParameters_Create() { + return (GoogleAnalytics_EventParameters*)(&g_stub_memory); +} + +// Stub for GoogleAnalytics_EventParameters_InsertInt +static void Stub_GoogleAnalytics_EventParameters_InsertInt(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + int64_t value) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_InsertDouble +static void Stub_GoogleAnalytics_EventParameters_InsertDouble(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + double value) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_InsertString +static void Stub_GoogleAnalytics_EventParameters_InsertString(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + const char* value) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_InsertItemVector +static void Stub_GoogleAnalytics_EventParameters_InsertItemVector(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + GoogleAnalytics_ItemVector* value) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_Destroy +static void Stub_GoogleAnalytics_EventParameters_Destroy(GoogleAnalytics_EventParameters* event_parameter_map) { + // No return value. +} + +// Stub for GoogleAnalytics_LogEvent +static void Stub_GoogleAnalytics_LogEvent(const char* name, GoogleAnalytics_EventParameters* parameters) { + // No return value. +} + +// Stub for GoogleAnalytics_SetUserProperty +static void Stub_GoogleAnalytics_SetUserProperty(const char* name, + const char* value) { + // No return value. +} + +// Stub for GoogleAnalytics_SetUserId +static void Stub_GoogleAnalytics_SetUserId(const char* user_id) { + // No return value. +} + +// Stub for GoogleAnalytics_ResetAnalyticsData +static void Stub_GoogleAnalytics_ResetAnalyticsData() { + // No return value. +} + +// Stub for GoogleAnalytics_SetAnalyticsCollectionEnabled +static void Stub_GoogleAnalytics_SetAnalyticsCollectionEnabled(bool enabled) { + // No return value. +} + + +// --- Function Pointer Initializations --- +GoogleAnalytics_Item* (*ptr_GoogleAnalytics_Item_Create)() = &Stub_GoogleAnalytics_Item_Create; +void (*ptr_GoogleAnalytics_Item_InsertInt)(GoogleAnalytics_Item* item, const char* key, int64_t value) = &Stub_GoogleAnalytics_Item_InsertInt; +void (*ptr_GoogleAnalytics_Item_InsertDouble)(GoogleAnalytics_Item* item, const char* key, double value) = &Stub_GoogleAnalytics_Item_InsertDouble; +void (*ptr_GoogleAnalytics_Item_InsertString)(GoogleAnalytics_Item* item, const char* key, const char* value) = &Stub_GoogleAnalytics_Item_InsertString; +void (*ptr_GoogleAnalytics_Item_Destroy)(GoogleAnalytics_Item* item) = &Stub_GoogleAnalytics_Item_Destroy; +GoogleAnalytics_ItemVector* (*ptr_GoogleAnalytics_ItemVector_Create)() = &Stub_GoogleAnalytics_ItemVector_Create; +void (*ptr_GoogleAnalytics_ItemVector_InsertItem)(GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item) = &Stub_GoogleAnalytics_ItemVector_InsertItem; +void (*ptr_GoogleAnalytics_ItemVector_Destroy)(GoogleAnalytics_ItemVector* item_vector) = &Stub_GoogleAnalytics_ItemVector_Destroy; +GoogleAnalytics_EventParameters* (*ptr_GoogleAnalytics_EventParameters_Create)() = &Stub_GoogleAnalytics_EventParameters_Create; +void (*ptr_GoogleAnalytics_EventParameters_InsertInt)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, int64_t value) = &Stub_GoogleAnalytics_EventParameters_InsertInt; +void (*ptr_GoogleAnalytics_EventParameters_InsertDouble)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, double value) = &Stub_GoogleAnalytics_EventParameters_InsertDouble; +void (*ptr_GoogleAnalytics_EventParameters_InsertString)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, const char* value) = &Stub_GoogleAnalytics_EventParameters_InsertString; +void (*ptr_GoogleAnalytics_EventParameters_InsertItemVector)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, GoogleAnalytics_ItemVector* value) = &Stub_GoogleAnalytics_EventParameters_InsertItemVector; +void (*ptr_GoogleAnalytics_EventParameters_Destroy)(GoogleAnalytics_EventParameters* event_parameter_map) = &Stub_GoogleAnalytics_EventParameters_Destroy; +void (*ptr_GoogleAnalytics_LogEvent)(const char* name, GoogleAnalytics_EventParameters* parameters) = &Stub_GoogleAnalytics_LogEvent; +void (*ptr_GoogleAnalytics_SetUserProperty)(const char* name, const char* value) = &Stub_GoogleAnalytics_SetUserProperty; +void (*ptr_GoogleAnalytics_SetUserId)(const char* user_id) = &Stub_GoogleAnalytics_SetUserId; +void (*ptr_GoogleAnalytics_ResetAnalyticsData)() = &Stub_GoogleAnalytics_ResetAnalyticsData; +void (*ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled)(bool enabled) = &Stub_GoogleAnalytics_SetAnalyticsCollectionEnabled; + +// --- Dynamic Loader Function for Windows --- +#if defined(_WIN32) +int FirebaseAnalytics_LoadAnalyticsFunctions(HMODULE dll_handle) { + int count = 0; + + if (!dll_handle) { + return count; + } + + FARPROC proc_GoogleAnalytics_Item_Create = GetProcAddress(dll_handle, "GoogleAnalytics_Item_Create"); + if (proc_GoogleAnalytics_Item_Create) { + ptr_GoogleAnalytics_Item_Create = (GoogleAnalytics_Item* (*)())proc_GoogleAnalytics_Item_Create; + count++; + } + FARPROC proc_GoogleAnalytics_Item_InsertInt = GetProcAddress(dll_handle, "GoogleAnalytics_Item_InsertInt"); + if (proc_GoogleAnalytics_Item_InsertInt) { + ptr_GoogleAnalytics_Item_InsertInt = (void (*)(GoogleAnalytics_Item* item, const char* key, int64_t value))proc_GoogleAnalytics_Item_InsertInt; + count++; + } + FARPROC proc_GoogleAnalytics_Item_InsertDouble = GetProcAddress(dll_handle, "GoogleAnalytics_Item_InsertDouble"); + if (proc_GoogleAnalytics_Item_InsertDouble) { + ptr_GoogleAnalytics_Item_InsertDouble = (void (*)(GoogleAnalytics_Item* item, const char* key, double value))proc_GoogleAnalytics_Item_InsertDouble; + count++; + } + FARPROC proc_GoogleAnalytics_Item_InsertString = GetProcAddress(dll_handle, "GoogleAnalytics_Item_InsertString"); + if (proc_GoogleAnalytics_Item_InsertString) { + ptr_GoogleAnalytics_Item_InsertString = (void (*)(GoogleAnalytics_Item* item, const char* key, const char* value))proc_GoogleAnalytics_Item_InsertString; + count++; + } + FARPROC proc_GoogleAnalytics_Item_Destroy = GetProcAddress(dll_handle, "GoogleAnalytics_Item_Destroy"); + if (proc_GoogleAnalytics_Item_Destroy) { + ptr_GoogleAnalytics_Item_Destroy = (void (*)(GoogleAnalytics_Item* item))proc_GoogleAnalytics_Item_Destroy; + count++; + } + FARPROC proc_GoogleAnalytics_ItemVector_Create = GetProcAddress(dll_handle, "GoogleAnalytics_ItemVector_Create"); + if (proc_GoogleAnalytics_ItemVector_Create) { + ptr_GoogleAnalytics_ItemVector_Create = (GoogleAnalytics_ItemVector* (*)())proc_GoogleAnalytics_ItemVector_Create; + count++; + } + FARPROC proc_GoogleAnalytics_ItemVector_InsertItem = GetProcAddress(dll_handle, "GoogleAnalytics_ItemVector_InsertItem"); + if (proc_GoogleAnalytics_ItemVector_InsertItem) { + ptr_GoogleAnalytics_ItemVector_InsertItem = (void (*)(GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item))proc_GoogleAnalytics_ItemVector_InsertItem; + count++; + } + FARPROC proc_GoogleAnalytics_ItemVector_Destroy = GetProcAddress(dll_handle, "GoogleAnalytics_ItemVector_Destroy"); + if (proc_GoogleAnalytics_ItemVector_Destroy) { + ptr_GoogleAnalytics_ItemVector_Destroy = (void (*)(GoogleAnalytics_ItemVector* item_vector))proc_GoogleAnalytics_ItemVector_Destroy; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_Create = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_Create"); + if (proc_GoogleAnalytics_EventParameters_Create) { + ptr_GoogleAnalytics_EventParameters_Create = (GoogleAnalytics_EventParameters* (*)())proc_GoogleAnalytics_EventParameters_Create; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_InsertInt = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_InsertInt"); + if (proc_GoogleAnalytics_EventParameters_InsertInt) { + ptr_GoogleAnalytics_EventParameters_InsertInt = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, int64_t value))proc_GoogleAnalytics_EventParameters_InsertInt; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_InsertDouble = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_InsertDouble"); + if (proc_GoogleAnalytics_EventParameters_InsertDouble) { + ptr_GoogleAnalytics_EventParameters_InsertDouble = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, double value))proc_GoogleAnalytics_EventParameters_InsertDouble; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_InsertString = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_InsertString"); + if (proc_GoogleAnalytics_EventParameters_InsertString) { + ptr_GoogleAnalytics_EventParameters_InsertString = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, const char* value))proc_GoogleAnalytics_EventParameters_InsertString; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_InsertItemVector = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_InsertItemVector"); + if (proc_GoogleAnalytics_EventParameters_InsertItemVector) { + ptr_GoogleAnalytics_EventParameters_InsertItemVector = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, GoogleAnalytics_ItemVector* value))proc_GoogleAnalytics_EventParameters_InsertItemVector; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_Destroy = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_Destroy"); + if (proc_GoogleAnalytics_EventParameters_Destroy) { + ptr_GoogleAnalytics_EventParameters_Destroy = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map))proc_GoogleAnalytics_EventParameters_Destroy; + count++; + } + FARPROC proc_GoogleAnalytics_LogEvent = GetProcAddress(dll_handle, "GoogleAnalytics_LogEvent"); + if (proc_GoogleAnalytics_LogEvent) { + ptr_GoogleAnalytics_LogEvent = (void (*)(const char* name, GoogleAnalytics_EventParameters* parameters))proc_GoogleAnalytics_LogEvent; + count++; + } + FARPROC proc_GoogleAnalytics_SetUserProperty = GetProcAddress(dll_handle, "GoogleAnalytics_SetUserProperty"); + if (proc_GoogleAnalytics_SetUserProperty) { + ptr_GoogleAnalytics_SetUserProperty = (void (*)(const char* name, const char* value))proc_GoogleAnalytics_SetUserProperty; + count++; + } + FARPROC proc_GoogleAnalytics_SetUserId = GetProcAddress(dll_handle, "GoogleAnalytics_SetUserId"); + if (proc_GoogleAnalytics_SetUserId) { + ptr_GoogleAnalytics_SetUserId = (void (*)(const char* user_id))proc_GoogleAnalytics_SetUserId; + count++; + } + FARPROC proc_GoogleAnalytics_ResetAnalyticsData = GetProcAddress(dll_handle, "GoogleAnalytics_ResetAnalyticsData"); + if (proc_GoogleAnalytics_ResetAnalyticsData) { + ptr_GoogleAnalytics_ResetAnalyticsData = (void (*)())proc_GoogleAnalytics_ResetAnalyticsData; + count++; + } + FARPROC proc_GoogleAnalytics_SetAnalyticsCollectionEnabled = GetProcAddress(dll_handle, "GoogleAnalytics_SetAnalyticsCollectionEnabled"); + if (proc_GoogleAnalytics_SetAnalyticsCollectionEnabled) { + ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled = (void (*)(bool enabled))proc_GoogleAnalytics_SetAnalyticsCollectionEnabled; + count++; + } + + return count; +} + +void FirebaseAnalytics_UnloadAnalyticsFunctions(void) { + ptr_GoogleAnalytics_Item_Create = &Stub_GoogleAnalytics_Item_Create; + ptr_GoogleAnalytics_Item_InsertInt = &Stub_GoogleAnalytics_Item_InsertInt; + ptr_GoogleAnalytics_Item_InsertDouble = &Stub_GoogleAnalytics_Item_InsertDouble; + ptr_GoogleAnalytics_Item_InsertString = &Stub_GoogleAnalytics_Item_InsertString; + ptr_GoogleAnalytics_Item_Destroy = &Stub_GoogleAnalytics_Item_Destroy; + ptr_GoogleAnalytics_ItemVector_Create = &Stub_GoogleAnalytics_ItemVector_Create; + ptr_GoogleAnalytics_ItemVector_InsertItem = &Stub_GoogleAnalytics_ItemVector_InsertItem; + ptr_GoogleAnalytics_ItemVector_Destroy = &Stub_GoogleAnalytics_ItemVector_Destroy; + ptr_GoogleAnalytics_EventParameters_Create = &Stub_GoogleAnalytics_EventParameters_Create; + ptr_GoogleAnalytics_EventParameters_InsertInt = &Stub_GoogleAnalytics_EventParameters_InsertInt; + ptr_GoogleAnalytics_EventParameters_InsertDouble = &Stub_GoogleAnalytics_EventParameters_InsertDouble; + ptr_GoogleAnalytics_EventParameters_InsertString = &Stub_GoogleAnalytics_EventParameters_InsertString; + ptr_GoogleAnalytics_EventParameters_InsertItemVector = &Stub_GoogleAnalytics_EventParameters_InsertItemVector; + ptr_GoogleAnalytics_EventParameters_Destroy = &Stub_GoogleAnalytics_EventParameters_Destroy; + ptr_GoogleAnalytics_LogEvent = &Stub_GoogleAnalytics_LogEvent; + ptr_GoogleAnalytics_SetUserProperty = &Stub_GoogleAnalytics_SetUserProperty; + ptr_GoogleAnalytics_SetUserId = &Stub_GoogleAnalytics_SetUserId; + ptr_GoogleAnalytics_ResetAnalyticsData = &Stub_GoogleAnalytics_ResetAnalyticsData; + ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled = &Stub_GoogleAnalytics_SetAnalyticsCollectionEnabled; +} + +#endif // defined(_WIN32) +// clang-format on diff --git a/analytics/src/windows/analytics_dynamic.h b/analytics/src/windows/analytics_dynamic.h new file mode 100644 index 0000000000..a35784e980 --- /dev/null +++ b/analytics/src/windows/analytics_dynamic.h @@ -0,0 +1,132 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated from analytics.h by generate_windows_stubs.py + +#ifndef FIREBASE_ANALYTICS_SRC_WINDOWS_ANALYTICS_DYNAMIC_H_ +#define FIREBASE_ANALYTICS_SRC_WINDOWS_ANALYTICS_DYNAMIC_H_ + +#include // needed for bool type in pure C + +// --- Copied from original header --- +#include + +/** + * @brief Opaque type for an item. + * + * This type is an opaque object that represents an item in an item vector. + * + * The caller is responsible for creating the item using the + * GoogleAnalytics_Item_Create() function, and destroying it using the + * GoogleAnalytics_Item_Destroy() function, unless it has been added to an + * item vector, in which case it will be destroyed at that time. + */ +typedef struct GoogleAnalytics_Item_Opaque GoogleAnalytics_Item; + +/** + * @brief Opaque type for an item vector. + * + * This type is an opaque object that represents a list of items. It is + * used to pass item vectors to the + * GoogleAnalytics_EventParameters_InsertItemVector() function. + * + * The caller is responsible for creating the item vector using the + * GoogleAnalytics_ItemVector_Create() function, and destroying it using the + * GoogleAnalytics_ItemVector_Destroy() function, unless it has been added + * to an event parameter map, in which case it will be destroyed at that time. + */ +typedef struct GoogleAnalytics_ItemVector_Opaque GoogleAnalytics_ItemVector; + +/** + * @brief Opaque type for an event parameter map. + * + * This type is an opaque object that represents a dictionary of event + * parameters. It is used to pass event parameters to the + * GoogleAnalytics_LogEvent() function. + * + * The caller is responsible for creating the event parameter map using the + * GoogleAnalytics_EventParameters_Create() function, and destroying it using + * the GoogleAnalytics_EventParameters_Destroy() function, unless it has been + * logged, in which case it will be destroyed automatically. + */ +typedef struct GoogleAnalytics_EventParameters_Opaque + GoogleAnalytics_EventParameters; + +// --- End of copied section --- + +#ifdef __cplusplus +extern "C" { +#endif + +// --- Function Pointer Declarations --- +// clang-format off +extern GoogleAnalytics_Item* (*ptr_GoogleAnalytics_Item_Create)(); +extern void (*ptr_GoogleAnalytics_Item_InsertInt)(GoogleAnalytics_Item* item, const char* key, int64_t value); +extern void (*ptr_GoogleAnalytics_Item_InsertDouble)(GoogleAnalytics_Item* item, const char* key, double value); +extern void (*ptr_GoogleAnalytics_Item_InsertString)(GoogleAnalytics_Item* item, const char* key, const char* value); +extern void (*ptr_GoogleAnalytics_Item_Destroy)(GoogleAnalytics_Item* item); +extern GoogleAnalytics_ItemVector* (*ptr_GoogleAnalytics_ItemVector_Create)(); +extern void (*ptr_GoogleAnalytics_ItemVector_InsertItem)(GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item); +extern void (*ptr_GoogleAnalytics_ItemVector_Destroy)(GoogleAnalytics_ItemVector* item_vector); +extern GoogleAnalytics_EventParameters* (*ptr_GoogleAnalytics_EventParameters_Create)(); +extern void (*ptr_GoogleAnalytics_EventParameters_InsertInt)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, int64_t value); +extern void (*ptr_GoogleAnalytics_EventParameters_InsertDouble)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, double value); +extern void (*ptr_GoogleAnalytics_EventParameters_InsertString)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, const char* value); +extern void (*ptr_GoogleAnalytics_EventParameters_InsertItemVector)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, GoogleAnalytics_ItemVector* value); +extern void (*ptr_GoogleAnalytics_EventParameters_Destroy)(GoogleAnalytics_EventParameters* event_parameter_map); +extern void (*ptr_GoogleAnalytics_LogEvent)(const char* name, GoogleAnalytics_EventParameters* parameters); +extern void (*ptr_GoogleAnalytics_SetUserProperty)(const char* name, const char* value); +extern void (*ptr_GoogleAnalytics_SetUserId)(const char* user_id); +extern void (*ptr_GoogleAnalytics_ResetAnalyticsData)(); +extern void (*ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled)(bool enabled); + +#define GoogleAnalytics_Item_Create ptr_GoogleAnalytics_Item_Create +#define GoogleAnalytics_Item_InsertInt ptr_GoogleAnalytics_Item_InsertInt +#define GoogleAnalytics_Item_InsertDouble ptr_GoogleAnalytics_Item_InsertDouble +#define GoogleAnalytics_Item_InsertString ptr_GoogleAnalytics_Item_InsertString +#define GoogleAnalytics_Item_Destroy ptr_GoogleAnalytics_Item_Destroy +#define GoogleAnalytics_ItemVector_Create ptr_GoogleAnalytics_ItemVector_Create +#define GoogleAnalytics_ItemVector_InsertItem ptr_GoogleAnalytics_ItemVector_InsertItem +#define GoogleAnalytics_ItemVector_Destroy ptr_GoogleAnalytics_ItemVector_Destroy +#define GoogleAnalytics_EventParameters_Create ptr_GoogleAnalytics_EventParameters_Create +#define GoogleAnalytics_EventParameters_InsertInt ptr_GoogleAnalytics_EventParameters_InsertInt +#define GoogleAnalytics_EventParameters_InsertDouble ptr_GoogleAnalytics_EventParameters_InsertDouble +#define GoogleAnalytics_EventParameters_InsertString ptr_GoogleAnalytics_EventParameters_InsertString +#define GoogleAnalytics_EventParameters_InsertItemVector ptr_GoogleAnalytics_EventParameters_InsertItemVector +#define GoogleAnalytics_EventParameters_Destroy ptr_GoogleAnalytics_EventParameters_Destroy +#define GoogleAnalytics_LogEvent ptr_GoogleAnalytics_LogEvent +#define GoogleAnalytics_SetUserProperty ptr_GoogleAnalytics_SetUserProperty +#define GoogleAnalytics_SetUserId ptr_GoogleAnalytics_SetUserId +#define GoogleAnalytics_ResetAnalyticsData ptr_GoogleAnalytics_ResetAnalyticsData +#define GoogleAnalytics_SetAnalyticsCollectionEnabled ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled +// clang-format on + + +// --- Dynamic Loader Declaration for Windows --- +#if defined(_WIN32) +#include // For HMODULE +// Load Google Analytics functions from the given DLL handle into function pointers. +// Returns the number of functions successfully loaded (out of 19). +int FirebaseAnalytics_LoadAnalyticsFunctions(HMODULE dll_handle); + +// Reset all function pointers back to stubs. +void FirebaseAnalytics_UnloadAnalyticsFunctions(void); + +#endif // defined(_WIN32) + +#ifdef __cplusplus +} +#endif + +#endif // FIREBASE_ANALYTICS_SRC_WINDOWS_ANALYTICS_DYNAMIC_H_ diff --git a/analytics/windows/analytics_win.dll b/analytics/windows/analytics_win.dll new file mode 100755 index 0000000000..f0c83825e3 Binary files /dev/null and b/analytics/windows/analytics_win.dll differ diff --git a/analytics/windows/include/public/analytics.h b/analytics/windows/include/public/analytics.h new file mode 100644 index 0000000000..d2dcc448ae --- /dev/null +++ b/analytics/windows/include/public/analytics.h @@ -0,0 +1,225 @@ +// Copyright 2025 Google LLC +#ifndef ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_ANALYTICS_H_ +#define ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_ANALYTICS_H_ + +#include +#include +#include +#include +#include +#include + +#include "c/analytics.h" + +namespace google::analytics { + +/** + * The top level Firebase Analytics singleton that provides methods for logging + * events and setting user properties. See the + * developer guides for general information on using Firebase Analytics in + * your apps. + * + * @note The Analytics SDK uses SQLite to persist events and other app-specific + * data. Calling certain thread-unsafe global SQLite methods like + * `sqlite3_shutdown()` can result in unexpected crashes at runtime. + */ +class Analytics { + public: + using PrimitiveValue = std::variant; + using Item = std::unordered_map; + using ItemVector = std::vector; + using EventParameterValue = + std::variant; + using EventParameters = std::unordered_map; + + /** + * @brief Returns the singleton instance of the Analytics class. + */ + static Analytics& GetInstance() { + static Analytics instance; + return instance; + } + + // This type is neither copyable nor movable. + Analytics(const Analytics&) = delete; + Analytics& operator=(const Analytics&) = delete; + Analytics(Analytics&&) = delete; + Analytics& operator=(Analytics&&) = delete; + + /** + * @brief Logs an app event. + * + * The event can have up to 25 parameters. Events with the same name must have + * the same parameters. Up to 500 event names are supported. Using predefined + * events and/or parameters is recommended for optimal reporting. + * + * The following event names are reserved and cannot be used: + * - ad_activeview + * - ad_click + * - ad_exposure + * - ad_query + * - ad_reward + * - adunit_exposure + * - app_clear_data + * - app_exception + * - app_remove + * - app_store_refund + * - app_store_subscription_cancel + * - app_store_subscription_convert + * - app_store_subscription_renew + * - app_update + * - app_upgrade + * - dynamic_link_app_open + * - dynamic_link_app_update + * - dynamic_link_first_open + * - error + * - firebase_campaign + * - first_open + * - first_visit + * - in_app_purchase + * - notification_dismiss + * - notification_foreground + * - notification_open + * - notification_receive + * - os_update + * - session_start + * - session_start_with_rollout + * - user_engagement + * + * @param[in] name The name of the event. Should contain 1 to 40 alphanumeric + * characters or underscores. The name must start with an alphabetic + * character. Some event names are reserved. See event_names.h for the list + * of reserved event names. The "firebase_", "google_", and "ga_" prefixes are + * reserved and should not be used. Note that event names are case-sensitive + * and that logging two events whose names differ only in case will result in + * two distinct events. To manually log screen view events, use the + * `screen_view` event name. Must be UTF-8 encoded. + * @param[in] parameters The map of event parameters. Passing `std::nullopt` + * indicates that the event has no parameters. Parameter names can be up to 40 + * characters long and must start with an alphabetic character and contain + * only alphanumeric characters and underscores. Only String, Int, and Double + * parameter types are supported. String parameter values can be up to 100 + * characters long for standard Google Analytics properties, and up to 500 + * characters long for Google Analytics 360 properties. The "firebase_", + * "google_", and "ga_" prefixes are reserved and should not be used for + * parameter names. String keys and values must be UTF-8 encoded. + */ + void LogEvent(const std::string& event_name, + const std::optional& parameters) { + if (!parameters.has_value()) { + GoogleAnalytics_LogEvent(std::string(event_name).c_str(), nullptr); + return; + } + GoogleAnalytics_EventParameters* map = + GoogleAnalytics_EventParameters_Create(); + for (const auto& [name, value] : parameters.value()) { + if (auto* int_value = std::get_if(&value)) { + GoogleAnalytics_EventParameters_InsertInt(map, name.c_str(), + *int_value); + } else if (auto* double_value = std::get_if(&value)) { + GoogleAnalytics_EventParameters_InsertDouble(map, name.c_str(), + *double_value); + } else if (auto* string_value = std::get_if(&value)) { + GoogleAnalytics_EventParameters_InsertString(map, name.c_str(), + string_value->c_str()); + } else if (auto* items = std::get_if(&value)) { + GoogleAnalytics_ItemVector* item_vector = + GoogleAnalytics_ItemVector_Create(); + for (const auto& item : *items) { + GoogleAnalytics_Item* nested_item = GoogleAnalytics_Item_Create(); + for (const auto& [nested_name, nested_value] : item) { + if (auto* nested_int_value = std::get_if(&nested_value)) { + GoogleAnalytics_Item_InsertInt(nested_item, nested_name.c_str(), + *nested_int_value); + } else if (auto* nested_double_value = + std::get_if(&nested_value)) { + GoogleAnalytics_Item_InsertDouble( + nested_item, nested_name.c_str(), *nested_double_value); + } else if (auto* nested_string_value = + std::get_if(&nested_value)) { + GoogleAnalytics_Item_InsertString(nested_item, + nested_name.c_str(), + nested_string_value->c_str()); + } + } + GoogleAnalytics_ItemVector_InsertItem(item_vector, nested_item); + } + GoogleAnalytics_EventParameters_InsertItemVector(map, name.c_str(), + item_vector); + } + } + GoogleAnalytics_LogEvent(std::string(event_name).c_str(), map); + } + + /** + * @brief Sets a user property to a given value. + * + * Up to 25 user property names are supported. Once set, user property values + * persist throughout the app lifecycle and across sessions. + * + * The following user property names are reserved and cannot be used: + * + * - first_open_time + * - last_deep_link_referrer + * - user_id + * + * @param[in] name The name of the user property to set. Should contain 1 to + * 24 alphanumeric characters or underscores, and must start with an + * alphabetic character. The "firebase_", "google_", and "ga_" prefixes are + * reserved and should not be used for user property names. Must be UTF-8 + * encoded. + * @param[in] value The value of the user property. Values can be up to 36 + * characters long. Setting the value to `std::nullopt` removes the user + * property. Must be UTF-8 encoded. + */ + void SetUserProperty(const std::string& name, + std::optional value) { + const char* value_ptr = value.has_value() ? value->c_str() : nullptr; + GoogleAnalytics_SetUserProperty(name.c_str(), value_ptr); + } + + /** + * @brief Sets the user ID property. + * + * This feature must be used in accordance with + * Google's Privacy + * Policy + * + * @param[in] user_id The user ID associated with the user of this app on this + * device. The user ID must be non-empty and no more than 256 characters + * long, and UTF-8 encoded. Setting user_id to std::nullopt removes the + * user ID. + */ + void SetUserId(std::optional user_id) { + if (!user_id.has_value()) { + GoogleAnalytics_SetUserId(nullptr); + } else { + GoogleAnalytics_SetUserId(user_id->c_str()); + } + } + + /** + * @brief Clears all analytics data for this instance from the device and + * resets the app instance ID. + */ + void ResetAnalyticsData() { GoogleAnalytics_ResetAnalyticsData(); } + + /** + * @brief Sets whether analytics collection is enabled for this app on this + * device. + * + * This setting is persisted across app sessions. By default it is enabled. + * + * @param[in] enabled A flag that enables or disables Analytics collection. + */ + void SetAnalyticsCollectionEnabled(bool enabled) { + GoogleAnalytics_SetAnalyticsCollectionEnabled(enabled); + } + + private: + Analytics() = default; +}; + +} // namespace google::analytics + +#endif // ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_ANALYTICS_H_ diff --git a/analytics/windows/include/public/c/analytics.h b/analytics/windows/include/public/c/analytics.h new file mode 100644 index 0000000000..cb3047f310 --- /dev/null +++ b/analytics/windows/include/public/c/analytics.h @@ -0,0 +1,332 @@ +// Copyright 2025 Google LLC +#ifndef ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_C_ANALYTICS_H_ +#define ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_C_ANALYTICS_H_ + +#include + +#if defined(ANALYTICS_DLL) && defined(_WIN32) +#define ANALYTICS_API __declspec(dllexport) +#else +#define ANALYTICS_API +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque type for an item. + * + * This type is an opaque object that represents an item in an item vector. + * + * The caller is responsible for creating the item using the + * GoogleAnalytics_Item_Create() function, and destroying it using the + * GoogleAnalytics_Item_Destroy() function, unless it has been added to an + * item vector, in which case it will be destroyed at that time. + */ +typedef struct GoogleAnalytics_Item_Opaque GoogleAnalytics_Item; + +/** + * @brief Opaque type for an item vector. + * + * This type is an opaque object that represents a list of items. It is + * used to pass item vectors to the + * GoogleAnalytics_EventParameters_InsertItemVector() function. + * + * The caller is responsible for creating the item vector using the + * GoogleAnalytics_ItemVector_Create() function, and destroying it using the + * GoogleAnalytics_ItemVector_Destroy() function, unless it has been added + * to an event parameter map, in which case it will be destroyed at that time. + */ +typedef struct GoogleAnalytics_ItemVector_Opaque GoogleAnalytics_ItemVector; + +/** + * @brief Opaque type for an event parameter map. + * + * This type is an opaque object that represents a dictionary of event + * parameters. It is used to pass event parameters to the + * GoogleAnalytics_LogEvent() function. + * + * The caller is responsible for creating the event parameter map using the + * GoogleAnalytics_EventParameters_Create() function, and destroying it using + * the GoogleAnalytics_EventParameters_Destroy() function, unless it has been + * logged, in which case it will be destroyed automatically. + */ +typedef struct GoogleAnalytics_EventParameters_Opaque + GoogleAnalytics_EventParameters; + +/** + * @brief Creates an item. + * + * The caller is responsible for destroying the item using the + * GoogleAnalytics_Item_Destroy() function, unless it has been added to an + * item vector, in which case it will be destroyed when it is added. + */ +ANALYTICS_API GoogleAnalytics_Item* GoogleAnalytics_Item_Create(); + +/** + * @brief Inserts an int parameter into the item. + * + * @param[in] item The item to insert the int parameter into. + * @param[in] key The key of the int parameter. Must be UTF-8 encoded. + * @param[in] value The value of the int parameter. + */ +ANALYTICS_API void GoogleAnalytics_Item_InsertInt(GoogleAnalytics_Item* item, + const char* key, + int64_t value); + +/** + * @brief Inserts a double parameter into the item. + * + * @param[in] item The item to insert the double parameter into. + * @param[in] key The key of the double parameter. Must be UTF-8 encoded. + * @param[in] value The value of the double parameter. + */ +ANALYTICS_API void GoogleAnalytics_Item_InsertDouble(GoogleAnalytics_Item* item, + const char* key, + double value); + +/** + * @brief Inserts a string parameter into the item. + * + * @param[in] item The item to insert the string parameter into. + * @param[in] key The key of the string parameter. Must be UTF-8 encoded. + * @param[in] value The value of the string parameter. Must be UTF-8 encoded. + */ +ANALYTICS_API void GoogleAnalytics_Item_InsertString(GoogleAnalytics_Item* item, + const char* key, + const char* value); + +/** + * @brief Destroys the item. + * + * The caller is responsible for destroying the item using this + * function, unless it has been added to an item vector, in which case it + * will be destroyed when it is added. + * + * @param[in] item The item to destroy. + */ +ANALYTICS_API void GoogleAnalytics_Item_Destroy(GoogleAnalytics_Item* item); + +/** + * @brief Creates an item vector. + * + * The caller is responsible for destroying the item vector using the + * GoogleAnalytics_ItemVector_Destroy() function, unless it has been added + * to an event parameter map, in which case it will be destroyed when it is + * added. + */ +ANALYTICS_API GoogleAnalytics_ItemVector* GoogleAnalytics_ItemVector_Create(); + +/** + * @brief Inserts a item into the item vector. + * + * @param[in] item_vector The item vector to insert the item into. + * @param[in] item The item to insert. Automatically destroyed when added. + */ +ANALYTICS_API void GoogleAnalytics_ItemVector_InsertItem( + GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item); + +/** + * @brief Destroys the item vector. + * + * The caller has the option to destroy the item vector using this function, + * unless it has been added to an event parameter map, in which case it will be + * destroyed when it is added. + * + * @param[in] item_vector The item vector to destroy. + */ +ANALYTICS_API void GoogleAnalytics_ItemVector_Destroy( + GoogleAnalytics_ItemVector* item_vector); + +/** + * @brief Creates an event parameter map. + * + * The caller is responsible for destroying the event parameter map using the + * GoogleAnalytics_EventParameters_Destroy() function, unless it has been + * logged, in which case it will be destroyed automatically when it is logged. + */ +ANALYTICS_API GoogleAnalytics_EventParameters* +GoogleAnalytics_EventParameters_Create(); + +/** + * @brief Inserts an int parameter into the event parameter map. + * + * @param[in] event_parameter_map The event parameter map to insert the int + * parameter into. + * @param[in] key The key of the int parameter. Must be UTF-8 encoded. + * @param[in] value The value of the int parameter. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_InsertInt( + GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + int64_t value); + +/** + * @brief Inserts a double parameter into the event parameter map. + * + * @param[in] event_parameter_map The event parameter map to insert the double + * parameter into. + * @param[in] key The key of the double parameter. Must be UTF-8 encoded. + * @param[in] value The value of the double parameter. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_InsertDouble( + GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + double value); + +/** + * @brief Inserts a string parameter into the event parameter map. + * + * @param[in] event_parameter_map The event parameter map to insert the string + * parameter into. + * @param[in] key The key of the string parameter. Must be UTF-8 encoded. + * @param[in] value The value of the string parameter. Must be UTF-8 encoded. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_InsertString( + GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + const char* value); + +/** + * @brief Inserts an item vector into the event parameter map. + * + * @param[in] event_parameter_map The event parameter map to insert the item + * vector into. + * @param[in] key The key of the item vector. Must be UTF-8 encoded. + * @param[in] value The value of the item vector. Automatically destroyed as it + * is added. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_InsertItemVector( + GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + GoogleAnalytics_ItemVector* value); + +/** + * @brief Destroys the event parameter map. + * + * The caller is responsible for destroying the event parameter map using this + * function. Unless it has been logged, in which case it will be destroyed + * automatically when it is logged. + * + * @param[in] event_parameter_map The event parameter map to destroy. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_Destroy( + GoogleAnalytics_EventParameters* event_parameter_map); + +/** + * @brief Logs an app event. + * + * The event can have up to 25 parameters. Events with the same name must have + * the same parameters. Up to 500 event names are supported. Using predefined + * events and/or parameters is recommended for optimal reporting. + * + * The following event names are reserved and cannot be used: + * - ad_activeview + * - ad_click + * - ad_exposure + * - ad_query + * - ad_reward + * - adunit_exposure + * - app_clear_data + * - app_exception + * - app_remove + * - app_store_refund + * - app_store_subscription_cancel + * - app_store_subscription_convert + * - app_store_subscription_renew + * - app_update + * - app_upgrade + * - dynamic_link_app_open + * - dynamic_link_app_update + * - dynamic_link_first_open + * - error + * - firebase_campaign + * - first_open + * - first_visit + * - in_app_purchase + * - notification_dismiss + * - notification_foreground + * - notification_open + * - notification_receive + * - os_update + * - session_start + * - session_start_with_rollout + * - user_engagement + * + * @param[in] name The name of the event. Should contain 1 to 40 alphanumeric + * characters or underscores. The name must start with an alphabetic + * character. Some event names are reserved. See event_names.h for the list + * of reserved event names. The "firebase_", "google_", and "ga_" prefixes are + * reserved and should not be used. Note that event names are case-sensitive + * and that logging two events whose names differ only in case will result in + * two distinct events. To manually log screen view events, use the + * `screen_view` event name. Must be UTF-8 encoded. + * @param[in] parameters The map of event parameters. Passing `nullptr` + * indicates that the event has no parameters. Parameter names can be up to 40 + * characters long and must start with an alphabetic character and contain + * only alphanumeric characters and underscores. Only String, Int, and Double + * parameter types are supported. String parameter values can be up to 100 + * characters long for standard Google Analytics properties, and up to 500 + * characters long for Google Analytics 360 properties. The "firebase_", + * "google_", and "ga_" prefixes are reserved and should not be used for + * parameter names. The parameter map must be created using the + * GoogleAnalytics_EventParameters_Create() function. Automatically destroyed + * when it is logged. + */ +ANALYTICS_API void GoogleAnalytics_LogEvent( + const char* name, GoogleAnalytics_EventParameters* parameters); + +/** + * @brief Sets a user property to a given value. + * + * Up to 25 user property names are supported. Once set, user property values + * persist throughout the app lifecycle and across sessions. + * + * The following user property names are reserved and cannot be used: + * + * - first_open_time + * - last_deep_link_referrer + * - user_id + * + * @param[in] name The name of the user property to set. Should contain 1 to 24 + * alphanumeric characters or underscores, and must start with an alphabetic + * character. The "firebase_", "google_", and "ga_" prefixes are reserved and + * should not be used for user property names. Must be UTF-8 encoded. + * @param[in] value The value of the user property. Values can be up to 36 + * characters long. Setting the value to `nullptr` remove the user property. + * Must be UTF-8 encoded. + */ +ANALYTICS_API void GoogleAnalytics_SetUserProperty(const char* name, + const char* value); + +/* + * @brief Sets the user ID property. + * + * This feature must be used in accordance with + * Google's Privacy + * Policy + * + * @param[in] user_id The user ID associated with the user of this app on this + * device. The user ID must be non-empty and no more than 256 characters long, + * and UTF-8 encoded. Setting user_id to nullptr removes the user ID. + */ +ANALYTICS_API void GoogleAnalytics_SetUserId(const char* user_id); + +/* + * @brief Clears all analytics data for this instance from the device and resets + * the app instance ID. + */ +ANALYTICS_API void GoogleAnalytics_ResetAnalyticsData(); + +/* + * @brief Sets whether analytics collection is enabled for this app on this + * device. + * + * This setting is persisted across app sessions. By default it is enabled. + * + * @param[in] enabled A flag that enables or disables Analytics collection. + */ +ANALYTICS_API void GoogleAnalytics_SetAnalyticsCollectionEnabled(bool enabled); + +#ifdef __cplusplus +} +#endif + +#endif // ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_C_ANALYTICS_H_ diff --git a/analytics/windows/include/public/event_names.h b/analytics/windows/include/public/event_names.h new file mode 100644 index 0000000000..ef8330751f --- /dev/null +++ b/analytics/windows/include/public/event_names.h @@ -0,0 +1,426 @@ +// Copyright 2025 Google LLC +#ifndef ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_EVENT_NAMES_H_ +#define ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_EVENT_NAMES_H_ + +// Predefined event names. +// +// An Event is an important occurrence in your app that you want to measure. You +// can report up to 500 different types of Events per app and you can associate +// up to 25 unique parameters with each Event type. Some common events are +// suggested below, but you may also choose to specify custom Event types that +// are associated with your specific app. Each event type is identified by a +// unique name. Event names can be up to 40 characters long, may only contain +// alphanumeric characters and underscores ("_"), and must start with an +// alphabetic character. The "firebase_", "google_", and "ga_" prefixes are +// reserved and should not be used. + +namespace firebase::analytics { + +// Ad Impression event. This event signifies when a user sees an ad impression. +// Note: If you supply the @c kParameterValue parameter, you must also supply +// the @c kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
    +//
  • @c kParameterAdPlatform (string) (optional)
  • +//
  • @c kParameterAdFormat (string) (optional)
  • +//
  • @c kParameterAdSource (string) (optional)
  • +//
  • @c kParameterAdUnitName (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventPublicAdImpression[] = "ad_impression"; + +// Add Payment Info event. This event signifies that a user has submitted their +// payment information. Note: If you supply the @c kParameterValue parameter, +// you must also supply the @c kParameterCurrency parameter so that revenue +// metrics can be computed accurately. Params: +// +//
    +//
  • @c kParameterCoupon (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterPaymentType (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventAddPaymentInfo[] = "add_payment_info"; + +// Add Shipping Info event. This event signifies that a user has submitted their +// shipping information. Note: If you supply the @c kParameterValue parameter, +// you must also supply the @c kParameterCurrency parameter so that revenue +// metrics can be computed accurately. Params: +// +//
    +//
  • @c kParameterCoupon (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterShippingTier (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventAddShippingInfo[] = "add_shipping_info"; + +// Add to Cart event. This event signifies that an item was added to a cart for +// purchase. Add this event to a funnel with @c kEventPurchase to gauge the +// effectiveness of your checkout process. Note: If you supply the +// @c kParameterValue parameter, you must also supply the @c kParameterCurrency +// parameter so that revenue metrics can be computed accurately. Params: +// +//
    +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventAddToCart[] = "add_to_cart"; + +// Add to Wishlist event. This event signifies that an item was added to a +// wishlist. Use this event to identify popular gift items. Note: If you supply +// the @c kParameterValue parameter, you must also supply the @c +// kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
    +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventAddToWishlist[] = "add_to_wishlist"; + +// App Open event. By logging this event when an App becomes active, developers +// can understand how often users leave and return during the course of a +// Session. Although Sessions are automatically reported, this event can provide +// further clarification around the continuous engagement of app-users. +inline constexpr char kEventAppOpen[] = "app_open"; + +// E-Commerce Begin Checkout event. This event signifies that a user has begun +// the process of checking out. Add this event to a funnel with your @c +// kEventPurchase event to gauge the effectiveness of your checkout process. +// Note: If you supply the @c kParameterValue parameter, you must also supply +// the @c kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
    +//
  • @c kParameterCoupon (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventBeginCheckout[] = "begin_checkout"; + +// Campaign Detail event. Log this event to supply the referral details of a +// re-engagement campaign. Note: you must supply at least one of the required +// parameters kParameterSource, kParameterMedium or kParameterCampaign. Params: +// +//
    +//
  • @c kParameterSource (string)
  • +//
  • @c kParameterMedium (string)
  • +//
  • @c kParameterCampaign (string)
  • +//
  • @c kParameterTerm (string) (optional)
  • +//
  • @c kParameterContent (string) (optional)
  • +//
  • @c kParameterAdNetworkClickId (string) (optional)
  • +//
  • @c kParameterCP1 (string) (optional)
  • +//
+inline constexpr char kEventCampaignDetails[] = "campaign_details"; + +// Earn Virtual Currency event. This event tracks the awarding of virtual +// currency in your app. Log this along with @c kEventSpendVirtualCurrency to +// better understand your virtual economy. Params: +// +//
    +//
  • @c kParameterVirtualCurrencyName (string)
  • +//
  • @c kParameterValue (signed 64-bit integer or double)
  • +//
+inline constexpr char kEventEarnVirtualCurrency[] = "earn_virtual_currency"; + +// Generate Lead event. Log this event when a lead has been generated in the app +// to understand the efficacy of your install and re-engagement campaigns. Note: +// If you supply the +// @c kParameterValue parameter, you must also supply the @c kParameterCurrency +// parameter so that revenue metrics can be computed accurately. Params: +// +//
    +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventGenerateLead[] = "generate_lead"; + +// Join Group event. Log this event when a user joins a group such as a guild, +// team or family. Use this event to analyze how popular certain groups or +// social features are in your app. Params: +// +//
    +//
  • @c kParameterGroupId (string)
  • +//
+inline constexpr char kEventJoinGroup[] = "join_group"; + +// Level Start event. Log this event when the user starts a level. Params: +// +//
    +//
  • @c kParameterLevelName (string)
  • +//
  • @c kParameterSuccess (string)
  • +//
+inline constexpr char kEventLevelEnd[] = "level_end"; + +// Level Up event. Log this event when the user finishes a level. Params: +// +//
    +//
  • @c kParameterLevelName (string)
  • +//
  • @c kParameterSuccess (string)
  • +//
+inline constexpr char kEventLevelStart[] = "level_start"; + +// Level Up event. This event signifies that a player has leveled up in your +// gaming app. It can help you gauge the level distribution of your userbase +// and help you identify certain levels that are difficult to pass. Params: +// +//
    +//
  • @c kParameterLevel (signed 64-bit integer)
  • +//
  • @c kParameterCharacter (string) (optional)
  • +//
+inline constexpr char kEventLevelUp[] = "level_up"; + +// Login event. Apps with a login feature can report this event to signify that +// a user has logged in. +inline constexpr char kEventLogin[] = "login"; + +// Post Score event. Log this event when the user posts a score in your gaming +// app. This event can help you understand how users are actually performing in +// your game and it can help you correlate high scores with certain audiences or +// behaviors. Params: +// +//
    +//
  • @c kParameterScore (signed 64-bit integer)
  • +//
  • @c kParameterLevel (signed 64-bit integer) (optional)
  • +//
  • @c kParameterCharacter (string) (optional)
  • +//
+//
  • @c kParameterAffiliation (string) (optional)
  • +//
  • @c kParameterCoupon (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterShipping (double) (optional)
  • +//
  • @c kParameterTax (double) (optional)
  • +//
  • @c kParameterTransactionId (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +// +inline constexpr char kEventPurchase[] = "purchase"; + +// E-Commerce Refund event. This event signifies that a refund was issued. +// Note: If you supply the @c kParameterValue parameter, you must also supply +// the @c kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
      +//
    • @c kParameterAffiliation (string) (optional)
    • +//
    • @c kParameterCoupon (string) (optional)
    • +//
    • @c kParameterCurrency (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterShipping (double) (optional)
    • +//
    • @c kParameterTax (double) (optional)
    • +//
    • @c kParameterTransactionId (string) (optional)
    • +//
    • @c kParameterValue (double) (optional)
    • +//
    +inline constexpr char kEventRefund[] = "refund"; + +// E-Commerce Remove from Cart event. This event signifies that an item(s) was +// removed from a cart. Note: If you supply the @c kParameterValue parameter, +// you must also supply the @c kParameterCurrency parameter so that revenue +// metrics can be computed accurately. Params: +// +//
      +//
    • @c kParameterCurrency (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterValue (double) (optional)
    • +//
    +inline constexpr char kEventRemoveFromCart[] = "remove_from_cart"; + +// Screen View event. This event signifies that a screen in your app has +// appeared. Use this event to contextualize Events that occur on a specific +// screen. Note: The @c kParameterScreenName parameter is optional, and the @c +// kParameterScreenClass parameter is required. If the @c kParameterScreenClass +// is not provided, or if there are extra parameters, the call to log this event +// will be ignored. Params: +// +//
      +//
    • @c kParameterScreenClass (string) (required)
    • +//
    • @c kParameterScreenName (string) (optional)
    • +//
    +inline constexpr char kEventScreenView[] = "screen_view"; + +// Search event. Apps that support search features can use this event to +// contextualize search operations by supplying the appropriate, corresponding +// parameters. This event can help you identify the most popular content in your +// app. Params: +// +//
      +//
    • @c kParameterSearchTerm (string)
    • +//
    • @c kParameterStartDate (string) (optional)
    • +//
    • @c kParameterEndDate (string) (optional)
    • +//
    • @c kParameterNumberOfNights (signed 64-bit integer) +// (optional) for hotel bookings
    • +//
    • @c kParameterNumberOfRooms (signed 64-bit integer) +// (optional) for hotel bookings
    • +//
    • @c kParameterNumberOfPassengers (signed 64-bit integer) +// (optional) for travel bookings
    • +//
    • @c kParameterOrigin (string) (optional)
    • +//
    • @c kParameterDestination (string) (optional)
    • +//
    • @c kParameterTravelClass (string) (optional) for travel bookings
    • +//
    +inline constexpr char kEventSearch[] = "search"; + +// Select Content event. This general purpose event signifies that a user has +// selected some content of a certain type in an app. The content can be any +// object in your app. This event can help you identify popular content and +// categories of content in your app. Params: +// +//
      +//
    • @c kParameterContentType (string)
    • +//
    • @c kParameterItemId (string)
    • +//
    +inline constexpr char kEventSelectContent[] = "select_content"; + +// Select Item event. This event signifies that an item was selected by a user +// from a list. Use the appropriate parameters to contextualize the event. Use +// this event to discover the most popular items selected. Params: +// +//
      +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterItemListId (string) (optional)
    • +//
    • @c kParameterItemListName (string) (optional)
    • +//
    +inline constexpr char kEventSelectItem[] = "select_item"; + +// Select Promotion event. This event signifies that a user has selected a +// promotion offer. Use the appropriate parameters to contextualize the event, +// such as the item(s) for which the promotion applies. Params: +// +//
      +//
    • @c kParameterCreativeName (string) (optional)
    • +//
    • @c kParameterCreativeSlot (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterLocationId (string) (optional)
    • +//
    • @c kParameterPromotionId (string) (optional)
    • +//
    • @c kParameterPromotionName (string) (optional)
    • +//
    +inline constexpr char kEventSelectPromotion[] = "select_promotion"; + +// Share event. Apps with social features can log the Share event to identify +// the most viral content. Params: +// +//
      +//
    • @c kParameterContentType (string)
    • +//
    • @c kParameterItemId (string)
    • +//
    +inline constexpr char kEventShare[] = "share"; + +// Sign Up event. This event indicates that a user has signed up for an account +// in your app. The parameter signifies the method by which the user signed up. +// Use this event to understand the different behaviors between logged in and +// logged out users. Params: +// +//
      +//
    • @c kParameterSignUpMethod (string)
    • +//
    +inline constexpr char kEventSignUp[] = "sign_up"; + +// Spend Virtual Currency event. This event tracks the sale of virtual goods in +// your app and can help you identify which virtual goods are the most popular +// objects of purchase. Params: +// +//
      +//
    • @c kParameterItemName (string)
    • +//
    • @c kParameterVirtualCurrencyName (string)
    • +//
    • @c kParameterValue (signed 64-bit integer or double)
    • +//
    +inline constexpr char kEventSpendVirtualCurrency[] = "spend_virtual_currency"; + +// Tutorial Begin event. This event signifies the start of the on-boarding +// process in your app. Use this in a funnel with kEventTutorialComplete to +// understand how many users complete this process and move on to the full app +// experience. +inline constexpr char kEventTutorialBegin[] = "tutorial_begin"; + +// Tutorial End event. Use this event to signify the user's completion of your +// app's on-boarding process. Add this to a funnel with kEventTutorialBegin to +// gauge the completion rate of your on-boarding process. +inline constexpr char kEventTutorialComplete[] = "tutorial_complete"; + +// Unlock Achievement event. Log this event when the user has unlocked an +// achievement in your game. Since achievements generally represent the breadth +// of a gaming experience, this event can help you understand how many users +// are experiencing all that your game has to offer. Params: +// +//
      +//
    • @c kParameterAchievementId (string)
    • +//
    +inline constexpr char kEventUnlockAchievement[] = "unlock_achievement"; + +// E-commerce View Cart event. This event signifies that a user has viewed +// their cart. Use this to analyze your purchase funnel. Note: If you supply +// the @c kParameterValue parameter, you must also supply the +// @c kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
      +//
    • @c kParameterCurrency (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterValue (double) (optional)
    • +//
    +inline constexpr char kEventViewCart[] = "view_cart"; + +// View Item event. This event signifies that a user has viewed an item. Use +// the appropriate parameters to contextualize the event. Use this event to +// discover the most popular items viewed in your app. Note: If you supply the +// @c kParameterValue parameter, you must also supply the @c kParameterCurrency +// parameter so that revenue metrics can be computed accurately. Params: +// +//
      +//
    • @c kParameterCurrency (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterValue (double) (optional)
    • +//
    +inline constexpr char kEventViewItem[] = "view_item"; + +// View Item List event. Log this event when a user sees a list of items or +// offerings. Params: +// +//
      +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterItemListId (string) (optional)
    • +//
    • @c kParameterItemListName (string) (optional)
    • +//
    +inline constexpr char kEventViewItemList[] = "view_item_list"; + +// View Promotion event. This event signifies that a promotion was shown to a +// user. Add this event to a funnel with the @c kEventAddToCart and +// @c kEventPurchase to gauge your conversion process. Params: +// +//
      +//
    • @c kParameterCreativeName (string) (optional)
    • +//
    • @c kParameterCreativeSlot (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterLocationId (string) (optional)
    • +//
    • @c kParameterPromotionId (string) (optional)
    • +//
    • @c kParameterPromotionName (string) (optional)
    • +//
    +inline constexpr char kEventViewPromotion[] = "view_promotion"; + +// View Search Results event. Log this event when the user has been presented +// with the results of a search. Params: +// +//
      +//
    • @c kParameterSearchTerm (string)
    • +//
    +inline constexpr char kEventViewSearchResults[] = "view_search_results"; + +} // namespace firebase::analytics + +#endif // ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_EVENT_NAMES_H_ diff --git a/analytics/windows/include/public/parameter_names.h b/analytics/windows/include/public/parameter_names.h new file mode 100644 index 0000000000..7c5f45695e --- /dev/null +++ b/analytics/windows/include/public/parameter_names.h @@ -0,0 +1,253 @@ +// Copyright 2025 Google LLC +#ifndef ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_PARAMETER_NAMES_H_ +#define ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_PARAMETER_NAMES_H_ + +namespace firebase::analytics { + +// Game achievement ID (string). +inline constexpr char kParameterAchievementId[] = "achievement_id"; + +// The ad format (e.g. Banner, Interstitial, Rewarded, Native, Rewarded +// Interstitial, Instream). (string). +inline constexpr char kParameterAdFormat[] = "ad_format"; + +// Ad Network Click ID (string). Used for network-specific click IDs which vary +// in format. +inline constexpr char kParameterAdNetworkClickId[] = "aclid"; + +// The ad platform (e.g. MoPub, IronSource) (string). +inline constexpr char kParameterAdPlatform[] = "ad_platform"; + +// The ad source (e.g. AdColony) (string). +inline constexpr char kParameterAdSource[] = "ad_source"; + +// The ad unit name (e.g. Banner_03) (string). +inline constexpr char kParameterAdUnitName[] = "ad_unit_name"; + +// A product affiliation to designate a supplying company or brick and mortar +// store location (string). +inline constexpr char kParameterAffiliation[] = "affiliation"; + +// Campaign custom parameter (string). Used as a method of capturing custom data +// in a campaign. Use varies by network. +inline constexpr char kParameterCP1[] = "cp1"; + +// The individual campaign name, slogan, promo code, etc. Some networks have +// pre-defined macro to capture campaign information, otherwise can be populated +// by developer. Highly Recommended (string). +inline constexpr char kParameterCampaign[] = "campaign"; + +// Campaign ID (string). Used for keyword analysis to identify a specific +// product promotion or strategic campaign. This is a required key for GA4 data +// import. +inline constexpr char kParameterCampaignId[] = "campaign_id"; + +// Character used in game (string). +inline constexpr char kParameterCharacter[] = "character"; + +// Campaign content (string). +inline constexpr char kParameterContent[] = "content"; + +// Type of content selected (string). +inline constexpr char kParameterContentType[] = "content_type"; + +// Coupon code used for a purchase (string). +inline constexpr char kParameterCoupon[] = "coupon"; + +// Creative Format (string). Used to identify the high-level classification of +// the type of ad served by a specific campaign. +inline constexpr char kParameterCreativeFormat[] = "creative_format"; + +// The name of a creative used in a promotional spot (string). +inline constexpr char kParameterCreativeName[] = "creative_name"; + +// The name of a creative slot (string). +inline constexpr char kParameterCreativeSlot[] = "creative_slot"; + +// Currency of the purchase or items associated with the event, in 3-letter +// ISO_4217 format (string). +inline constexpr char kParameterCurrency[] = "currency"; + +// Flight or Travel destination (string). +inline constexpr char kParameterDestination[] = "destination"; + +// Monetary value of discount associated with a purchase (double). +inline constexpr char kParameterDiscount[] = "discount"; + +// The arrival date, check-out date or rental end date for the item. This should +// be in YYYY-MM-DD format (string). +inline constexpr char kParameterEndDate[] = "end_date"; + +// Indicates that the associated event should either extend the current session +// or start a new session if no session was active when the event was logged. +// Specify 1 to extend the current session or to start a new session; any other +// value will not extend or start a session. +inline constexpr char kParameterExtendSession[] = "extend_session"; + +// Flight or Travel origin (string). +inline constexpr char kParameterFlightNumber[] = "flight_number"; + +// Group/clan/guild ID (string). +inline constexpr char kParameterGroupId[] = "group_id"; + +// Index of an item in a list (integer). +inline constexpr char kParameterIndex[] = "index"; + +// Item brand (string). +inline constexpr char kParameterItemBrand[] = "item_brand"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory[] = "item_category"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory2[] = "item_category2"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory3[] = "item_category3"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory4[] = "item_category4"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory5[] = "item_category5"; + +// Item ID (context-specific) (string). +inline constexpr char kParameterItemId[] = "item_id"; + +// The ID of the list in which the item was presented to the user (string). +inline constexpr char kParameterItemListId[] = "item_list_id"; + +// The name of the list in which the item was presented to the user (string). +inline constexpr char kParameterItemListName[] = "item_list_name"; + +// Item Name (context-specific) (string). +inline constexpr char kParameterItemName[] = "item_name"; + +// Item variant (string). +inline constexpr char kParameterItemVariant[] = "item_variant"; + +// The list of items involved in the transaction expressed as `[[String: Any]]`. +inline constexpr char kParameterItems[] = "items"; + +// Level in game (integer). +inline constexpr char kParameterLevel[] = "level"; + +// Location (string). The Google Place ID +// that corresponds to the associated event. Alternatively, you can supply +// your own custom Location ID. +inline constexpr char kParameterLocation[] = "location"; + +// The location associated with the event. Preferred to be the Google +// Place ID that +// corresponds to the associated item but could be overridden to a custom +// location ID string.(string). +inline constexpr char kParameterLocationId[] = "location_id"; + +// Marketing Tactic (string). Used to identify the targeting criteria applied to +// a specific campaign. +inline constexpr char kParameterMarketingTactic[] = "marketing_tactic"; + +// The method used to perform an operation (string). +inline constexpr char kParameterMethod[] = "method"; + +// Number of nights staying at hotel (integer). +inline constexpr char kParameterNumberOfNights[] = "number_of_nights"; + +// Number of passengers traveling (integer). +inline constexpr char kParameterNumberOfPassengers[] = "number_of_passengers"; + +// Number of rooms for travel events (integer). +inline constexpr char kParameterNumberOfRooms[] = "number_of_rooms"; + +// Flight or Travel origin (string). +inline constexpr char kParameterOrigin[] = "origin"; + +// The chosen method of payment (string). +inline constexpr char kParameterPaymentType[] = "payment_type"; + +// Purchase price (double). +inline constexpr char kParameterPrice[] = "price"; + +// The ID of a product promotion (string). +inline constexpr char kParameterPromotionId[] = "promotion_id"; + +// The name of a product promotion (string). +inline constexpr char kParameterPromotionName[] = "promotion_name"; + +// Purchase quantity (integer). +inline constexpr char kParameterQuantity[] = "quantity"; + +// Score in game (integer). +inline constexpr char kParameterScore[] = "score"; + +// Current screen class, such as the class name of the UI view controller, +// logged with screen_view event and added to every event (string). +inline constexpr char kParameterScreenClass[] = "screen_class"; + +// Current screen name, such as the name of the UI view, logged with screen_view +// event and added to every event (string). +inline constexpr char kParameterScreenName[] = "screen_name"; + +// The search string/keywords used (string). +inline constexpr char kParameterSearchTerm[] = "search_term"; + +// Shipping cost associated with a transaction (double). +inline constexpr char kParameterShipping[] = "shipping"; + +// The shipping tier (e.g. Ground, Air, Next-day) selected for delivery of the +// purchased item (string). +inline constexpr char kParameterShippingTier[] = "shipping_tier"; + +// The origin of your traffic, such as an Ad network (for example, google) or +// partner (urban airship). Identify the advertiser, site, publication, etc. +// that is sending traffic to your property. Highly recommended (string). +inline constexpr char kParameterSource[] = "source"; + +// Source Platform (string). Used to identify the platform responsible for +// directing traffic to a given Analytics property (e.g., a buying platform +// where budgets, targeting criteria, etc. are set, a platform for managing +// organic traffic data, etc.). +inline constexpr char kParameterSourcePlatform[] = "source_platform"; + +// The departure date, check-in date or rental start date for the item. This +// should be in YYYY-MM-DD format (string). +inline constexpr char kParameterStartDate[] = "start_date"; + +// The result of an operation. Specify 1 to indicate success and 0 to indicate +// failure (integer). +inline constexpr char kParameterSuccess[] = "success"; + +// Tax cost associated with a transaction (double). +inline constexpr char kParameterTax[] = "tax"; + +// If you're manually tagging keyword campaigns, you should use utm_term to +// specify the keyword (string). +inline constexpr char kParameterTerm[] = "term"; + +// The unique identifier of a transaction (string). +inline constexpr char kParameterTransactionId[] = "transaction_id"; + +// Travel class (string). +inline constexpr char kParameterTravelClass[] = "travel_class"; + +// A context-specific numeric value which is accumulated automatically for each +// event type. This is a general purpose parameter that is useful for +// accumulating a key metric that pertains to an event. Examples include +// revenue, distance, time and points. Value should be specified as integer or +// double. +// Notes: Values for pre-defined currency-related events (such as @c +// kEventAddToCart) should be supplied using Double and must be accompanied by a +// @c kParameterCurrency parameter. The valid range of accumulated values is +// [-9,223,372,036,854.77, 9,223,372,036,854.77]. Supplying a non-numeric value, +// omitting the corresponding @c kParameterCurrency parameter, or supplying an +// invalid currency code for conversion +// events will cause that conversion to be omitted from reporting. +inline constexpr char kParameterValue[] = "value"; + +// The type of virtual currency being used (string). +inline constexpr char kParameterVirtualCurrencyName[] = "virtual_currency_name"; + +} // namespace firebase::analytics + +#endif // ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_PARAMETER_NAMES_H_ diff --git a/app/src/invites/ios/invites_ios_startup.mm b/app/src/invites/ios/invites_ios_startup.mm index 4ae901d57a..a99602ba79 100644 --- a/app/src/invites/ios/invites_ios_startup.mm +++ b/app/src/invites/ios/invites_ios_startup.mm @@ -286,7 +286,7 @@ @implementation UIApplication (FIRFBI) + (void)load { // C++ constructors may not be called yet so call NSLog rather than LogDebug. NSLog(@"Loading UIApplication category for Firebase App"); - ::firebase::util::ForEachAppDelegateClass(^(Class clazz) { + ::firebase::util::RunOnAppDelegateClasses(^(Class clazz) { ::firebase::invites::HookAppDelegateMethods(clazz); }); } diff --git a/app/src/util_ios.h b/app/src/util_ios.h index 454fab09cd..1a69484979 100644 --- a/app/src/util_ios.h +++ b/app/src/util_ios.h @@ -185,10 +185,12 @@ typedef BOOL ( id self, SEL selector_value, UIApplication *application, NSUserActivity *user_activity, void (^restoration_handler)(NSArray *)); -// Call the given block once for every Objective-C class that exists that -// implements the UIApplicationDelegate protocol (except for those in a -// blacklist we keep). -void ForEachAppDelegateClass(void (^block)(Class)); +// Calls the given block for each unique Objective-C class that has been +// previously passed to [UIApplication setDelegate:]. The block is executed +// immediately for all currently known unique delegate classes. +// Additionally, the block is queued to be executed if any new, unique +// Objective-C class is passed to [UIApplication setDelegate:] in the future. +void RunOnAppDelegateClasses(void (^block)(Class)); // Convert a string array into an NSMutableArray. NSMutableArray *StringVectorToNSMutableArray( diff --git a/app/src/util_ios.mm b/app/src/util_ios.mm index 21a70e6c85..6286eeae4b 100644 --- a/app/src/util_ios.mm +++ b/app/src/util_ios.mm @@ -27,6 +27,103 @@ #import #import +using firebase::GetLogLevel; +using firebase::kLogLevelDebug; + +// Key used in Info.plist to specify a custom AppDelegate class name. +static NSString *const kFirebaseAppDelegateClassNameKey = @"FirebaseAppDelegateClassName"; + +#define MAX_PENDING_APP_DELEGATE_BLOCKS 8 +#define MAX_SEEN_DELEGATE_CLASSES 32 + +static IMP g_original_setDelegate_imp = NULL; +static void (^g_pending_app_delegate_blocks[MAX_PENDING_APP_DELEGATE_BLOCKS])(Class) = {nil}; +static int g_pending_block_count = 0; +static Class g_seen_delegate_classes[MAX_SEEN_DELEGATE_CLASSES] = {nil}; +static int g_seen_delegate_classes_count = 0; + +static void Firebase_setDelegate(id self, SEL _cmd, id delegate) { + Class new_class = nil; + if (delegate) { + new_class = [delegate class]; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: UIApplication setDelegate: called with class %s (Swizzled)", + class_getName(new_class)); + } else { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: UIApplication setDelegate: called with nil delegate (Swizzled)"); + } + + if (new_class) { + // 1. Superclass Check + bool superclass_already_seen = false; + Class current_super = class_getSuperclass(new_class); + while (current_super) { + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i] == current_super) { + superclass_already_seen = true; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Delegate class %s has superclass %s which was already seen. Skipping " + @"processing for %s.", + class_getName(new_class), class_getName(current_super), class_getName(new_class)); + break; + } + } + if (superclass_already_seen) break; + current_super = class_getSuperclass(current_super); + } + + if (!superclass_already_seen) { + // 2. Direct Class Check (if no superclass was seen) + bool direct_class_already_seen = false; + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i] == new_class) { + direct_class_already_seen = true; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Delegate class %s already seen directly. Skipping processing.", + class_getName(new_class)); + break; + } + } + + if (!direct_class_already_seen) { + // 3. Process as New Class + if (g_seen_delegate_classes_count < MAX_SEEN_DELEGATE_CLASSES) { + g_seen_delegate_classes[g_seen_delegate_classes_count] = new_class; + g_seen_delegate_classes_count++; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Added new delegate class %s to seen list (total seen: %d).", + class_getName(new_class), g_seen_delegate_classes_count); + + if (g_pending_block_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Executing %d pending block(s) for new delegate class: %s.", + g_pending_block_count, class_getName(new_class)); + for (int i = 0; i < g_pending_block_count; i++) { + if (g_pending_app_delegate_blocks[i]) { + g_pending_app_delegate_blocks[i](new_class); + // Pending blocks persist to run for future new delegate classes. + } + } + } + } else { + NSLog(@"Firebase Error: Exceeded MAX_SEEN_DELEGATE_CLASSES (%d). Cannot add new delegate " + @"class %s or run pending blocks for it.", + MAX_SEEN_DELEGATE_CLASSES, class_getName(new_class)); + } + } + } + } + + // Call the original setDelegate: implementation + if (g_original_setDelegate_imp) { + ((void (*)(id, SEL, id))g_original_setDelegate_imp)(self, _cmd, + delegate); + } else { + NSLog(@"Firebase Error: Original setDelegate: IMP not found, cannot call original method."); + } +} + @implementation FIRSAMAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { @@ -76,41 +173,133 @@ - (BOOL)application:(UIApplication *)application #endif // defined(__IPHONE_12_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0 @end +@implementation UIApplication (FirebaseAppDelegateSwizzling) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *appDelegateClassName = + [[NSBundle mainBundle] objectForInfoDictionaryKey:kFirebaseAppDelegateClassNameKey]; + + if (appDelegateClassName && [appDelegateClassName isKindOfClass:[NSString class]] && + appDelegateClassName.length > 0) { + Class specificClass = NSClassFromString(appDelegateClassName); + if (specificClass) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Info.plist key '%@' found. Targeting AppDelegate class: %@. Swizzling " + @"of [UIApplication setDelegate:] will be skipped.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + + // Set this class as the sole "seen" delegate for Firebase processing. + // g_seen_delegate_classes_count should be 0 here in +load, but clear just in case. + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + g_seen_delegate_classes[i] = nil; + } + g_seen_delegate_classes[0] = specificClass; + g_seen_delegate_classes_count = 1; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: %@ is now the only delegate class Firebase will initially process.", + appDelegateClassName); + + // If there are already blocks pending (e.g., from other Firebase components' +load + // methods), execute them now for the specified delegate. These blocks will remain in the + // pending queue, mirroring the behavior of the original swizzled setDelegate: method which + // also does not clear pending blocks after execution (as they might apply to future + // delegates). In this Info.plist mode, however, Firebase won't process further setDelegate: + // calls. + if (g_pending_block_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: +load (Info.plist Mode) - Executing %d PENDING block(s) for " + @"specified delegate: %@. (Blocks are not removed from queue).", + g_pending_block_count, NSStringFromClass(specificClass)); + for (int i = 0; i < g_pending_block_count; i++) { + if (g_pending_app_delegate_blocks[i]) { + g_pending_app_delegate_blocks[i](specificClass); + } + } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: +load (Info.plist Mode) - Pending blocks executed for specific " + @"delegate."); + } + // Skip swizzling. g_original_setDelegate_imp remains NULL. + return; + } else { + NSLog(@"Firebase Error: Info.plist key '%@' specifies class '%@', which was not found. " + @"Proceeding with default swizzling.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + } + } else { + if (appDelegateClassName) { // Key is present but value is invalid (e.g., empty string or + // wrong type). + NSLog(@"Firebase Error: Info.plist key '%@' has an invalid value ('%@'). Proceeding " + @"with default swizzling.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + } else { // Key is not present. + // This is the default case, no special logging needed here beyond the swizzling log itself. + } + } + + // Standard behavior: Swizzle [UIApplication setDelegate:] + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Proceeding with swizzling of [UIApplication setDelegate:]."); + Class uiApplicationClass = [UIApplication class]; + SEL originalSelector = @selector(setDelegate:); + Method originalMethod = class_getInstanceMethod(uiApplicationClass, originalSelector); + + if (!originalMethod) { + NSLog( + @"Firebase Error: Original [UIApplication setDelegate:] method not found for swizzling."); + return; + } + + IMP previousImp = method_setImplementation(originalMethod, (IMP)Firebase_setDelegate); + if (previousImp) { + g_original_setDelegate_imp = previousImp; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Successfully swizzled [UIApplication setDelegate:] and stored original " + @"IMP."); + } else { + NSLog(@"Firebase Error: Swizzled [UIApplication setDelegate:], but original IMP was NULL (or " + @"method_setImplementation failed to return the previous IMP)."); + } + }); +} + +@end + namespace firebase { namespace util { -void ForEachAppDelegateClass(void (^block)(Class)) { - unsigned int number_of_classes; - Class *classes = objc_copyClassList(&number_of_classes); - for (unsigned int i = 0; i < number_of_classes; i++) { - Class clazz = classes[i]; - if (class_conformsToProtocol(clazz, @protocol(UIApplicationDelegate))) { - const char *class_name = class_getName(clazz); - bool blacklisted = false; - static const char *kClassNameBlacklist[] = { - // Declared in Firebase Analytics: - // //googlemac/iPhone/Firebase/Analytics/Sources/ApplicationDelegate/ - // FIRAAppDelegateProxy.m - "FIRAAppDelegate", - // Declared here. - "FIRSAMAppDelegate"}; - for (size_t i = 0; i < FIREBASE_ARRAYSIZE(kClassNameBlacklist); ++i) { - if (strcmp(class_name, kClassNameBlacklist[i]) == 0) { - blacklisted = true; - break; - } - } - if (!blacklisted) { - if (GetLogLevel() <= kLogLevelDebug) { - // Call NSLog directly because we may be in a +load method, - // and C++ classes may not be constructed yet. - NSLog(@"Firebase: Found UIApplicationDelegate class %s", class_name); - } - block(clazz); +void RunOnAppDelegateClasses(void (^block)(Class)) { + if (g_seen_delegate_classes_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses executing block for %d already seen delegate " + @"class(es).", + g_seen_delegate_classes_count); + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i]) { // Safety check + block(g_seen_delegate_classes[i]); } } + } else { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses - no delegate classes seen yet. Block will be " + @"queued for future delegates."); + } + + // Always try to queue the block for any future new delegate classes. + if (g_pending_block_count < MAX_PENDING_APP_DELEGATE_BLOCKS) { + g_pending_app_delegate_blocks[g_pending_block_count] = [block copy]; + g_pending_block_count++; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses - added block to pending list (total pending: %d). " + @"This block will run on future new delegate classes.", + g_pending_block_count); + } else { + NSLog(@"Firebase Error: RunOnAppDelegateClasses - pending block queue is full (max %d). Cannot " + @"add new block for future execution. Discarding block.", + MAX_PENDING_APP_DELEGATE_BLOCKS); } - free(classes); } NSDictionary *StringMapToNSDictionary(const std::map &string_map) { @@ -354,8 +543,27 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func const char *class_name = class_getName(clazz); Method method = class_getInstanceMethod(clazz, name); NSString *selector_name_nsstring = NSStringFromSelector(name); - const char *selector_name = selector_name_nsstring.UTF8String; - IMP original_method_implementation = method ? method_getImplementation(method) : nil; + const char *selector_name = selector_name_nsstring.UTF8String; // Used for logging later + + IMP current_actual_imp = method ? method_getImplementation(method) : nil; + + // === Begin idempotency check === + if (current_actual_imp == imp) { + // Assuming GetLogLevel() and kLogLevelDebug are available here. + // Based on previous file content, GetLogLevel is available in this file from util_ios.h. + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method %s on class %s is already swizzled with the target IMP. Skipping " + @"re-swizzle.", + selector_name, class_name); + + return; // Already swizzled to the desired implementation + } + // === End idempotency check === + + // If we reach here, current_actual_imp is different from imp, or the method didn't exist. + // We now assign original_method_implementation to be current_actual_imp for the rest of the + // function. + IMP original_method_implementation = current_actual_imp; // Get the type encoding of the selector from a type_encoding_class (which is a class which // implements a stub for the method). @@ -364,9 +572,9 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func assert(type_encoding); NSString *new_method_name_nsstring = nil; - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Registering method for %s selector %s", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Attempting to register method for %s selector %s", class_name, selector_name); + if (original_method_implementation) { // Try adding a method with randomized prefix on the name. int retry = kRandomNameGenerationRetries; @@ -381,32 +589,32 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func } const char *new_method_name = new_method_name_nsstring.UTF8String; if (retry == 0) { - NSLog(@"Failed to add method %s on class %s as the %s method already exists on the class. To " - @"resolve this issue, change the name of the method %s on the class %s.", - new_method_name, class_name, new_method_name, new_method_name, class_name); + NSLog( + @"Firebase Error: Failed to add method %s on class %s as the %s method already exists on " + @"the class. To resolve this issue, change the name of the method %s on the class %s.", + new_method_name, class_name, new_method_name, new_method_name, class_name); return; } method_setImplementation(method, imp); // Save the selector name that points at the original method implementation. SetMethod(name, new_method_name_nsstring); - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Registered method for %s selector %s (original method %s 0x%08x)", class_name, - selector_name, new_method_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Registered method for %s selector %s (original method %s 0x%08x)", + class_name, selector_name, new_method_name, static_cast(reinterpret_cast(original_method_implementation))); - } + } else if (add_method) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Adding method for %s selector %s", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Adding method for %s selector %s", class_name, selector_name); + // The class doesn't implement the selector so simply install our method implementation. if (!class_addMethod(clazz, name, imp, type_encoding)) { - NSLog(@"Failed to add new method %s on class %s.", selector_name, class_name); + NSLog(@"Firebase Error: Failed to add new method %s on class %s.", selector_name, class_name); } } else { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Method implementation for %s selector %s not found, ignoring.", class_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method implementation for %s selector %s not found, ignoring.", class_name, selector_name); - } } } @@ -420,9 +628,9 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func selector_implementation_names_per_selector_[selector_name_nsstring]; const char *class_name = class_getName(clazz); if (!selector_implementation_names) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Method not cached for class %s selector %s.", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method not cached for class %s selector %s.", class_name, selector_name); + return nil; } @@ -440,10 +648,10 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func search_class = clazz; for (; search_class; search_class = class_getSuperclass(search_class)) { const char *search_class_name = class_getName(search_class); - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Searching for selector %s (%s) on class %s", selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Searching for selector %s (%s) on class %s", selector_name, selector_implementation_name, search_class_name); - } + Method method = class_getInstanceMethod(search_class, selector_implementation); method_implementation = method ? method_getImplementation(method) : nil; if (method_implementation) break; @@ -451,18 +659,18 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func if (method_implementation) break; } if (!method_implementation) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Class %s does not respond to selector %s (%s)", class_name, selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Class %s does not respond to selector %s (%s)", class_name, selector_name, selector_implementation_name_nsstring.UTF8String); - } + return nil; } - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Found %s (%s, 0x%08x) on class %s (%s)", selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Found %s (%s, 0x%08x) on class %s (%s)", selector_name, selector_implementation_name_nsstring.UTF8String, static_cast(reinterpret_cast(method_implementation)), class_name, class_getName(search_class)); - } + return method_implementation; } diff --git a/cmake/external/leveldb.cmake b/cmake/external/leveldb.cmake index 3dd38315a8..bb6763a299 100644 --- a/cmake/external/leveldb.cmake +++ b/cmake/external/leveldb.cmake @@ -18,8 +18,10 @@ if(TARGET leveldb) return() endif() +if (DESKTOP AND MSVC) set(patch_file ${CMAKE_CURRENT_LIST_DIR}/../../scripts/git/patches/leveldb/0001-leveldb-1.23-windows-paths.patch) +endif() # This version must be kept in sync with the version in firestore.patch.txt. # If this version ever changes then make sure to update the version in diff --git a/messaging/src/ios/messaging.mm b/messaging/src/ios/messaging.mm index 3029bc21c0..680a29011e 100644 --- a/messaging/src/ios/messaging.mm +++ b/messaging/src/ios/messaging.mm @@ -870,7 +870,7 @@ @implementation UIApplication (FIRFCM) + (void)load { // C++ constructors may not be called yet so call NSLog rather than LogInfo. NSLog(@"FCM: Loading UIApplication FIRFCM category"); - ::firebase::util::ForEachAppDelegateClass(^(Class clazz) { + ::firebase::util::RunOnAppDelegateClasses(^(Class clazz) { FirebaseMessagingHookAppDelegate(clazz); }); } diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 09b9644733..0ecbafb0d9 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -537,9 +537,30 @@ addition to any you may have implemented. The Firebase Cloud Messaging library needs to attach handlers to the application delegate using method swizzling. If you are using -these libraries, at load time, Firebase will identify your `AppDelegate` class -and swizzle the required methods onto it, chaining a call back to your existing -method implementation. +these libraries, at load time, Firebase will typically identify your `AppDelegate` +class and swizzle the required methods onto it. + +#### Specifying Your AppDelegate Class Directly (iOS) + +For a more direct approach, or if you encounter issues with the default +method swizzling, you can explicitly tell Firebase which class is your +application's `AppDelegate`. To do this, add the `FirebaseAppDelegateClassName` +key to your app's `Info.plist` file: + +* **Key:** `FirebaseAppDelegateClassName` +* **Type:** `String` +* **Value:** Your AppDelegate's class name (e.g., `MyCustomAppDelegate`) + +**Example `Info.plist` entry:** +```xml +FirebaseAppDelegateClassName +MyCustomAppDelegate +``` + +If this key is provided with a valid class name, Firebase will use that class +directly for its AppDelegate-related interactions. If the key is not present, +is invalid, or the class is not found, Firebase will use its standard method +swizzling approach. ### Custom Android Build Systems @@ -654,6 +675,14 @@ workflow use only during the development of your app, not for publicly shipping code. ## Release Notes +### Upcoming Release +- Changes + - iOS: Added an option to explicitly specify your app's `AppDelegate` class + name via the `FirebaseAppDelegateClassName` key in `Info.plist`. This + provides a more direct way for Firebase to interact with your specified + AppDelegate. See "Platform Notes > iOS Method Swizzling > + Specifying Your AppDelegate Class Directly (iOS)" for details. + ### 12.8.0 - Changes - General (iOS): Update to Firebase Cocoapods version 11.14.0. diff --git a/scripts/dumpsrc.sh b/scripts/dumpsrc.sh new file mode 100755 index 0000000000..1ec63275b3 --- /dev/null +++ b/scripts/dumpsrc.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Print a formatted list of source files from specific directories. +# Suggested usage: dumpsrc.sh [path2] [path3] [...] | pbcopy + +included_files=( + '*.swig' + '*.i' + '*.c' + '*.cc' + '*.cpp' + '*.cs' + '*.cmake' + '*.fbs' + '*.gradle' + '*.h' + '*.hh' + '*.java' + '*.js' + '*.json' + '*.md' + '*.m' + '*.mm' + 'CMakeLists.txt' + 'Podfile' +) + +get_markdown_language_for_file() { + local filename="$1" + local base="$(basename "$filename")" + local ext="${base##*.}" + if [[ "$base" == "$ext" && "$base" != .* ]]; then + ext="" # No extension + fi + + # Handle special filename case first + if [[ "$base" == "CMakeLists.txt" ]]; then + echo "cmake" + return 0 + fi + + # Main logic using case based on extension + case "$ext" in + c) echo "c" ;; + cc) echo "cpp" ;; + cs) echo "csharp" ;; + hh) echo "cpp" ;; + m) echo "objectivec" ;; + mm) echo "objectivec" ;; + sh) echo "bash" ;; + py) echo "python" ;; + md) echo "markdown" ;; + json) echo "js" ;; + h) + if grep -qE "\@interface|\#import" "$filename"; then + echo "objectivec"; + else + echo "cpp"; + fi + ;; + "") # Explicitly handle no extension (after CMakeLists.txt check) + : # Output nothing + ;; + *) # Default case for any other non-empty extension + echo "$ext" + ;; + esac + return 0 +} + + +if [[ -z "$1" ]]; then + echo "Usage: $0 [path2] [path3] ..." + exit 1 +fi + +find_cmd_args=('-name' 'UNUSED') + +for pattern in "${included_files[@]}"; do + if [[ -n "${pattern}" ]]; then + find_cmd_args+=('-or' '-name' "$pattern") + fi +done + +for f in `find $* -type f -and "${find_cmd_args[@]}"`; do + echo "*** BEGIN CONTENTS OF FILE '$f' ***"; + echo '```'$(get_markdown_language_for_file "$f"); + cat "$f"; + echo '```'; + echo "*** END CONTENTS OF FILE '$f' ***"; + echo +done diff --git a/scripts/gha/firebase_github.py b/scripts/gha/firebase_github.py index 9bc961fbd7..91285edf29 100644 --- a/scripts/gha/firebase_github.py +++ b/scripts/gha/firebase_github.py @@ -57,13 +57,15 @@ def set_repo_url(repo): def requests_retry_session(retries=RETRIES, backoff_factor=BACKOFF, - status_forcelist=RETRY_STATUS): + status_forcelist=RETRY_STATUS, + allowed_methods=frozenset(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])): session = requests.Session() retry = Retry(total=retries, read=retries, connect=retries, backoff_factor=backoff_factor, - status_forcelist=status_forcelist) + status_forcelist=status_forcelist, + allowed_methods=allowed_methods) adapter = HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) @@ -183,14 +185,36 @@ def download_artifact(token, artifact_id, output_path=None): """https://docs.github.com/en/rest/reference/actions#download-an-artifact""" url = f'{GITHUB_API_URL}/actions/artifacts/{artifact_id}/zip' headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} - with requests_retry_session().get(url, headers=headers, stream=True, timeout=TIMEOUT_LONG) as response: - logging.info("download_artifact: %s response: %s", url, response) - if output_path: - with open(output_path, 'wb') as file: - shutil.copyfileobj(response.raw, file) - elif response.status_code == 200: - return response.content - return None + # Custom retry for artifact download due to potential for 410 errors (artifact expired) + # which shouldn't be retried indefinitely like other server errors. + artifact_retry = Retry(total=5, # Increased retries + read=5, + connect=5, + backoff_factor=1, # More aggressive backoff for artifacts + status_forcelist=(500, 502, 503, 504), # Only retry on these server errors + allowed_methods=frozenset(['GET'])) + session = requests.Session() + adapter = HTTPAdapter(max_retries=artifact_retry) + session.mount('https://', adapter) + + try: + with session.get(url, headers=headers, stream=True, timeout=TIMEOUT_LONG) as response: + logging.info("download_artifact: %s response: %s", url, response) + response.raise_for_status() # Raise an exception for bad status codes + if output_path: + with open(output_path, 'wb') as file: + shutil.copyfileobj(response.raw, file) + return True # Indicate success + else: + return response.content + except requests.exceptions.HTTPError as e: + logging.error(f"HTTP error downloading artifact {artifact_id}: {e.response.status_code} - {e.response.reason}") + if e.response.status_code == 410: + logging.warning(f"Artifact {artifact_id} has expired and cannot be downloaded.") + return None # Indicate failure + except requests.exceptions.RequestException as e: + logging.error(f"Error downloading artifact {artifact_id}: {e}") + return None # Indicate failure def dismiss_review(token, pull_number, review_id, message): diff --git a/scripts/gha/report_build_status.py b/scripts/gha/report_build_status.py index 29cf437343..8c41169f33 100644 --- a/scripts/gha/report_build_status.py +++ b/scripts/gha/report_build_status.py @@ -471,24 +471,63 @@ def main(argv): found_artifacts = False # There are possibly multiple artifacts, so iterate through all of them, # and extract the relevant ones into a temp folder, and then summarize them all. + # Prioritize artifacts by date, older ones might be expired. + sorted_artifacts = sorted(artifacts, key=lambda art: dateutil.parser.parse(art['created_at']), reverse=True) + with tempfile.TemporaryDirectory() as tmpdir: - for a in artifacts: + for a in sorted_artifacts: # Iterate over sorted artifacts if 'log-artifact' in a['name']: - print("Checking this artifact:", a['name'], "\n") - artifact_contents = firebase_github.download_artifact(FLAGS.token, a['id']) - if artifact_contents: - found_artifacts = True - artifact_data = io.BytesIO(artifact_contents) - artifact_zip = zipfile.ZipFile(artifact_data) - artifact_zip.extractall(path=tmpdir) + logging.debug("Attempting to download artifact: %s (ID: %s, Created: %s)", a['name'], a['id'], a['created_at']) + # Pass tmpdir to download_artifact to save directly + artifact_downloaded_path = os.path.join(tmpdir, f"{a['id']}.zip") + # Attempt to download the artifact with a timeout + download_success = False # Initialize download_success + try: + # download_artifact now returns True on success, None on failure. + if firebase_github.download_artifact(FLAGS.token, a['id'], output_path=artifact_downloaded_path): + download_success = True + except requests.exceptions.Timeout: + logging.warning(f"Timeout while trying to download artifact: {a['name']} (ID: {a['id']})") + # download_success remains False + + if download_success and os.path.exists(artifact_downloaded_path): + try: + with open(artifact_downloaded_path, "rb") as f: + artifact_contents = f.read() + if artifact_contents: # Ensure content was read + found_artifacts = True + artifact_data = io.BytesIO(artifact_contents) + with zipfile.ZipFile(artifact_data) as artifact_zip: # Use with statement for ZipFile + artifact_zip.extractall(path=tmpdir) + logging.info("Successfully downloaded and extracted artifact: %s", a['name']) + else: + logging.warning("Artifact %s (ID: %s) was downloaded but is empty.", a['name'], a['id']) + except zipfile.BadZipFile: + logging.error("Failed to open zip file for artifact %s (ID: %s). It might be corrupted or not a zip file.", a['name'], a['id']) + except Exception as e: + logging.error("An error occurred during artifact processing %s (ID: %s): %s", a['name'], a['id'], e) + finally: + # Clean up the downloaded zip file whether it was processed successfully or not + if os.path.exists(artifact_downloaded_path): + os.remove(artifact_downloaded_path) + elif not download_success : # Covers False or None from download_artifact + # Logging for non-timeout failures is now primarily handled within download_artifact + # We only log a general failure here if it wasn't a timeout (already logged) + # and download_artifact indicated failure (returned None). + # This avoids double logging for specific HTTP errors like 410. + pass # Most specific logging is now in firebase_github.py + if found_artifacts: (success, results) = summarize_test_results.summarize_logs(tmpdir, False, False, True) - print("Results:", success, " ", results, "\n") + logging.info("Summarized logs results - Success: %s, Results (first 100 chars): %.100s", success, results) run['log_success'] = success run['log_results'] = results + else: + logging.warning("No artifacts could be successfully downloaded and processed for run %s on day %s.", run['id'], day) + if not found_artifacts: - # Artifacts expire after some time, so if they are gone, we need + # Artifacts expire after some time, or download failed, so if they are gone, we need # to read the GitHub logs instead. This is much slower, so we # prefer to read artifacts instead whenever possible. logging.info("Reading github logs for run %s instead", run['id']) diff --git a/scripts/gha/utils.py b/scripts/gha/utils.py index 0bddfc7bff..d75a516580 100644 --- a/scripts/gha/utils.py +++ b/scripts/gha/utils.py @@ -19,7 +19,7 @@ platforms. """ -import distutils.spawn +import shutil import glob import platform import shutil @@ -63,7 +63,7 @@ def run_command(cmd, capture_output=False, cwd=None, check=False, as_root=False, def is_command_installed(tool): """Check if a command is installed on the system.""" - return distutils.spawn.find_executable(tool) + return shutil.which(tool) def glob_exists(glob_path): diff --git a/scripts/print_github_reviews.py b/scripts/print_github_reviews.py new file mode 100755 index 0000000000..33bebdf14a --- /dev/null +++ b/scripts/print_github_reviews.py @@ -0,0 +1,632 @@ +#!/usr/bin/env python3 +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fetches and formats review comments from a GitHub Pull Request.""" + +import argparse +import os +import sys +import datetime +from datetime import timezone, timedelta +import requests +import json +import re +import subprocess +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry + +# Constants for GitHub API interaction +RETRIES = 3 +BACKOFF = 5 +RETRY_STATUS = (403, 500, 502, 504) # HTTP status codes to retry on +TIMEOUT = 5 # Default timeout for requests in seconds + +# Global variables for the target repository, populated by set_repo_url_standalone() +OWNER = '' +REPO = '' +BASE_URL = 'https://api.github.com' +GITHUB_API_URL = '' + + +def set_repo_url_standalone(owner_name, repo_name): + global OWNER, REPO, GITHUB_API_URL + OWNER = owner_name + REPO = repo_name + GITHUB_API_URL = '%s/repos/%s/%s' % (BASE_URL, OWNER, REPO) + return True + + +def requests_retry_session(retries=RETRIES, + backoff_factor=BACKOFF, + status_forcelist=RETRY_STATUS): + session = requests.Session() + retry = Retry(total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session + + +def get_pull_request_review_comments(token, pull_number, since=None): + """https://docs.github.com/en/rest/pulls/comments#list-review-comments-on-a-pull-request""" + url = f'{GITHUB_API_URL}/pulls/{pull_number}/comments' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + + page = 1 + per_page = 100 # GitHub API default and max is 100 for many paginated endpoints + results = [] + + base_params = {'per_page': per_page} + if since: + base_params['since'] = since + + while True: + current_page_params = base_params.copy() + current_page_params['page'] = page + + try: + with requests_retry_session().get(url, headers=headers, params=current_page_params, + stream=True, timeout=TIMEOUT) as response: + response.raise_for_status() + + current_page_results = response.json() + if not current_page_results: # No more data + break + + results.extend(current_page_results) + + if len(current_page_results) < per_page: # Reached last page + break + + page += 1 + + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to fetch review comments (page {page}, params: {current_page_params}) for PR {pull_number}: {e}\n") + return None + return results + + +def list_pull_requests(token, state, head, base): + """https://docs.github.com/en/rest/reference/pulls#list-pull-requests""" + url = f'{GITHUB_API_URL}/pulls' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + page = 1 + per_page = 100 + results = [] + keep_going = True + while keep_going: + params = {'per_page': per_page, 'page': page} + if state: params.update({'state': state}) + if head: params.update({'head': head}) + if base: params.update({'base': base}) + page = page + 1 + keep_going = False + try: + with requests_retry_session().get(url, headers=headers, params=params, + stream=True, timeout=TIMEOUT) as response: + response.raise_for_status() + current_page_results = response.json() + if not current_page_results: + break + results.extend(current_page_results) + keep_going = (len(current_page_results) == per_page) + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to list pull requests (page {params.get('page', 'N/A')}, params: {params}) for {OWNER}/{REPO}: {e}\n") + return None + return results + + +def get_pull_request_reviews(token, owner, repo, pull_number): + """Fetches all reviews for a given pull request.""" + # Note: GitHub API for listing reviews does not support a 'since' parameter directly. + # Filtering by 'since' must be done client-side after fetching all reviews. + url = f'{GITHUB_API_URL}/pulls/{pull_number}/reviews' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + page = 1 + per_page = 100 + results = [] + keep_going = True + while keep_going: + params = {'per_page': per_page, 'page': page} + page = page + 1 + keep_going = False + try: + with requests_retry_session().get(url, headers=headers, params=params, + stream=True, timeout=TIMEOUT) as response: + response.raise_for_status() + current_page_results = response.json() + if not current_page_results: + break + results.extend(current_page_results) + keep_going = (len(current_page_results) == per_page) + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to list pull request reviews (page {params.get('page', 'N/A')}, params: {params}) for PR {pull_number} in {owner}/{repo}: {e}\n") + return None + return results + + +def get_current_branch_name(): + """Gets the current git branch name.""" + try: + branch_bytes = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.PIPE) + return branch_bytes.decode().strip() + except (subprocess.CalledProcessError, FileNotFoundError, UnicodeDecodeError) as e: + sys.stderr.write(f"Could not determine current git branch: {e}\n") + return None + +def get_latest_pr_for_branch(token, owner, repo, branch_name): + """Fetches the most recent open pull request for a given branch.""" + if not owner or not repo: + sys.stderr.write("Owner and repo must be set to find PR for branch.\n") + return None + + head_branch_spec = f"{owner}:{branch_name}" # Format required by GitHub API for head branch + prs = list_pull_requests(token=token, state="open", head=head_branch_spec, base=None) + + if not prs: + return None + + # Sort PRs by creation date (most recent first) to find the latest. + try: + prs.sort(key=lambda pr: pr.get("created_at", ""), reverse=True) + except Exception as e: # Broad exception for safety, though sort issues are rare with valid data. + sys.stderr.write(f"Could not sort PRs by creation date: {e}\n") + return None + + if prs: + return prs[0].get("number") + return None + + +def main(): + STATUS_IRRELEVANT = "[IRRELEVANT]" + STATUS_OLD = "[OLD]" + STATUS_CURRENT = "[CURRENT]" + + determined_owner = None + determined_repo = None + try: + git_url_bytes = subprocess.check_output(["git", "remote", "get-url", "origin"], stderr=subprocess.PIPE) + git_url = git_url_bytes.decode().strip() + match = re.search(r"(?:(?:https?://github\.com/)|(?:git@github\.com:))([^/]+)/([^/.]+)(?:\.git)?", git_url) + if match: + determined_owner = match.group(1) + determined_repo = match.group(2) + sys.stderr.write(f"Determined repository: {determined_owner}/{determined_repo} from git remote.\n") + except (subprocess.CalledProcessError, FileNotFoundError, UnicodeDecodeError) as e: + sys.stderr.write(f"Could not automatically determine repository from git remote: {e}\n") + except Exception as e: # Catch any other unexpected error. + sys.stderr.write(f"An unexpected error occurred while determining repository: {e}\n") + + def parse_repo_url(url_string): + """Parses owner and repository name from various GitHub URL formats.""" + url_match = re.search(r"(?:(?:https?://github\.com/)|(?:git@github\.com:))([^/]+)/([^/.]+?)(?:\.git)?/?$", url_string) + if url_match: + return url_match.group(1), url_match.group(2) + return None, None + + parser = argparse.ArgumentParser( + description="Fetch review comments from a GitHub PR and format into simple text output.\n" + "Repository can be specified via --url, or --owner AND --repo, or auto-detected from git remote 'origin'.", + formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument( + "--pull_number", + type=int, + default=None, + help="Pull request number. If not provided, script attempts to find the latest open PR for the current git branch." + ) + parser.add_argument( + "--branch", + type=str, + default=None, + help="Branch name to find the latest open PR for. Mutually exclusive with --pull_number. If neither --pull_number nor --branch is provided, uses the current git branch." + ) + parser.add_argument( + "--url", + type=str, + default=None, + help="Full GitHub repository URL (e.g., https://github.com/owner/repo or git@github.com:owner/repo.git). Takes precedence over --owner/--repo." + ) + parser.add_argument( + "--owner", + type=str, + default=determined_owner, + help=f"Repository owner. Used if --url is not provided. {'Default: ' + determined_owner if determined_owner else 'Required if --url is not used and not determinable from git.'}" + ) + parser.add_argument( + "--repo", + type=str, + default=determined_repo, + help=f"Repository name. Used if --url is not provided. {'Default: ' + determined_repo if determined_repo else 'Required if --url is not used and not determinable from git.'}" + ) + parser.add_argument( + "--token", + type=str, + default=os.environ.get("GITHUB_TOKEN"), + help="GitHub token. Can also be set via GITHUB_TOKEN env var or from ~/.github_token." + ) + parser.add_argument( + "--context-lines", + type=int, + default=10, + help="Number of context lines from the diff hunk. 0 for full hunk. If > 0, shows header (if any) and last N lines of the remaining hunk. Default: 10." + ) + parser.add_argument( + "--since", + type=str, + default=None, + help="Only show comments updated at or after this ISO 8601 timestamp (e.g., YYYY-MM-DDTHH:MM:SSZ)." + ) + parser.add_argument( + "--exclude-old", + action="store_true", + default=False, + help="Exclude comments marked [OLD] (where line number has changed due to code updates but position is still valid)." + ) + parser.add_argument( + "--include-irrelevant", + action="store_true", + default=False, + help="Include comments marked [IRRELEVANT] (where GitHub can no longer anchor the comment to the diff, i.e., position is null)." + ) + + args = parser.parse_args() + error_suffix = " (use --help for more details)" + + # Initialize tracking variables early, including processed_comments_count + latest_overall_review_activity_dt = None + latest_line_comment_activity_dt = None + processed_comments_count = 0 + + token = args.token + if not token: + try: + with open(os.path.expanduser("~/.github_token"), "r") as f: + token = f.read().strip() + if token: + sys.stderr.write("Using token from ~/.github_token\n") + except FileNotFoundError: + pass # File not found is fine, we'll check token next + except Exception as e: + sys.stderr.write(f"Warning: Could not read ~/.github_token: {e}\n") + + + if not token: + sys.stderr.write(f"Error: GitHub token not provided. Set GITHUB_TOKEN, use --token, or place it in ~/.github_token.{error_suffix}\n") + sys.exit(1) + args.token = token # Ensure args.token is populated for the rest of the script + + final_owner = None + final_repo = None + + # Determine repository owner and name + if args.url: + owner_explicitly_set = args.owner is not None and args.owner != determined_owner + repo_explicitly_set = args.repo is not None and args.repo != determined_repo + if owner_explicitly_set or repo_explicitly_set: + sys.stderr.write(f"Error: Cannot use --owner or --repo when --url is specified.{error_suffix}\n") + sys.exit(1) + + parsed_owner, parsed_repo = parse_repo_url(args.url) + if parsed_owner and parsed_repo: + final_owner = parsed_owner + final_repo = parsed_repo + sys.stderr.write(f"Using repository from --url: {final_owner}/{final_repo}\n") + else: + sys.stderr.write(f"Error: Invalid URL format: {args.url}. Expected https://github.com/owner/repo or git@github.com:owner/repo.git{error_suffix}\n") + sys.exit(1) + else: + is_owner_from_user = args.owner is not None and args.owner != determined_owner + is_repo_from_user = args.repo is not None and args.repo != determined_repo + + if (is_owner_from_user or is_repo_from_user): # User explicitly set at least one of owner/repo + if args.owner and args.repo: + final_owner = args.owner + final_repo = args.repo + sys.stderr.write(f"Using repository from --owner/--repo args: {final_owner}/{final_repo}\n") + else: + sys.stderr.write(f"Error: Both --owner and --repo must be specified if one is provided explicitly (and --url is not used).{error_suffix}\n") + sys.exit(1) + elif args.owner and args.repo: # Both args have values, from successful auto-detection + final_owner = args.owner + final_repo = args.repo + elif args.owner or args.repo: # Only one has a value from auto-detection (e.g. git remote parsing failed partially) + sys.stderr.write(f"Error: Both --owner and --repo are required if not using --url, and auto-detection was incomplete.{error_suffix}\n") + sys.exit(1) + # If final_owner/repo are still None here, it means auto-detection failed AND user provided nothing. + + if not final_owner or not final_repo: + sys.stderr.write(f"Error: Could not determine repository. Please specify --url, OR both --owner and --repo, OR ensure git remote 'origin' is configured correctly.{error_suffix}\n") + sys.exit(1) + + if not set_repo_url_standalone(final_owner, final_repo): + sys.stderr.write(f"Error: Could not set repository to {final_owner}/{final_repo}. Ensure owner/repo are correct.{error_suffix}\n") + sys.exit(1) + + pull_request_number = args.pull_number + branch_to_find_pr_for = None + + if args.pull_number and args.branch: + sys.stderr.write(f"Error: --pull_number and --branch are mutually exclusive.{error_suffix}\n") + sys.exit(1) + + if not pull_request_number: + if args.branch: + branch_to_find_pr_for = args.branch + sys.stderr.write(f"Pull number not specified, attempting to find PR for branch: {branch_to_find_pr_for}...\n") + else: + sys.stderr.write("Pull number and branch not specified, attempting to find PR for current git branch...\n") + branch_to_find_pr_for = get_current_branch_name() + if branch_to_find_pr_for: + sys.stderr.write(f"Current git branch is: {branch_to_find_pr_for}\n") + else: + sys.stderr.write(f"Error: Could not determine current git branch. Cannot find PR automatically.{error_suffix}\n") + sys.exit(1) + + if branch_to_find_pr_for: # This will be true if args.branch was given, or if get_current_branch_name() succeeded + pull_request_number = get_latest_pr_for_branch(args.token, OWNER, REPO, branch_to_find_pr_for) + if pull_request_number: + sys.stderr.write(f"Found PR #{pull_request_number} for branch {branch_to_find_pr_for}.\n") + else: + sys.stderr.write(f"No open PR found for branch {branch_to_find_pr_for} in {OWNER}/{REPO}.\n") + # If branch_to_find_pr_for is None here, it means get_current_branch_name() failed and we already exited. + + if not pull_request_number: # Final check for PR number + error_message = "Error: Pull request number could not be determined." + if args.branch: # Specific error if --branch was used + error_message = f"Error: No open PR found for specified branch '{args.branch}'." + elif not args.pull_number and branch_to_find_pr_for: # Auto-detect current branch ok, but no PR found + error_message = f"Error: Pull request number not specified and no open PR found for current branch '{branch_to_find_pr_for}'." + # The case where current_branch_for_pr_check (now branch_to_find_pr_for) is None (git branch fail) is caught and exited above. + sys.stderr.write(f"{error_message}{error_suffix}\n") + sys.exit(1) + + sys.stderr.write(f"Fetching overall reviews for PR #{pull_request_number} from {OWNER}/{REPO}...\n") + overall_reviews = get_pull_request_reviews(args.token, OWNER, REPO, pull_request_number) + + if overall_reviews is None: + sys.stderr.write(f"Error: Failed to fetch overall reviews due to an API or network issue.{error_suffix}\nPlease check logs for details.\n") + sys.exit(1) + + filtered_overall_reviews = [] + if overall_reviews: # If not None and not empty + for review in overall_reviews: + review_state = review.get("state") + if review_state == "DISMISSED" or review_state == "PENDING": + continue + + if args.since: + submitted_at_str = review.get("submitted_at") + if submitted_at_str: + try: + # Compatibility for Python < 3.11 + if sys.version_info < (3, 11): + dt_str_submitted = submitted_at_str.replace("Z", "+00:00") + else: + dt_str_submitted = submitted_at_str + submitted_dt = datetime.datetime.fromisoformat(dt_str_submitted) + + since_dt_str = args.since + if sys.version_info < (3, 11) and args.since.endswith("Z"): + since_dt_str = args.since.replace("Z", "+00:00") + since_dt = datetime.datetime.fromisoformat(since_dt_str) + + # Ensure 'since_dt' is timezone-aware if 'submitted_dt' is. + # GitHub timestamps are UTC. fromisoformat on Z or +00:00 makes them aware. + if submitted_dt.tzinfo and not since_dt.tzinfo: + since_dt = since_dt.replace(tzinfo=timezone.utc) # Assume since is UTC if not specified + + if submitted_dt < since_dt: + continue + except ValueError as ve: + sys.stderr.write(f"Warning: Could not parse review submitted_at timestamp '{submitted_at_str}' or --since timestamp '{args.since}': {ve}\n") + # If parsing fails, we might choose to include the review to be safe, or skip. Current: include. + + if review.get("state") == "COMMENTED" and not review.get("body", "").strip(): + continue + + filtered_overall_reviews.append(review) + + try: + filtered_overall_reviews.sort(key=lambda r: r.get("submitted_at", "")) + except Exception as e: # Broad exception for safety + sys.stderr.write(f"Warning: Could not sort overall reviews: {e}\n") + + if filtered_overall_reviews: + print("# Code Reviews\n\n") + # Use a temporary variable for accumulating latest timestamp within this specific block + temp_latest_overall_review_dt = None + for review in filtered_overall_reviews: + user = review.get("user", {}).get("login", "Unknown user") + submitted_at_str = review.get("submitted_at", "N/A") + state = review.get("state", "N/A") + body = review.get("body", "").strip() + + if submitted_at_str and submitted_at_str != "N/A": + try: + if sys.version_info < (3, 11): + dt_str_submitted = submitted_at_str.replace("Z", "+00:00") + else: + dt_str_submitted = submitted_at_str + current_review_submitted_dt = datetime.datetime.fromisoformat(dt_str_submitted) + if temp_latest_overall_review_dt is None or current_review_submitted_dt > temp_latest_overall_review_dt: + temp_latest_overall_review_dt = current_review_submitted_dt + except ValueError: + sys.stderr.write(f"Warning: Could not parse overall review submitted_at for --since suggestion: {submitted_at_str}\n") + + html_url = review.get("html_url", "N/A") + review_id = review.get("id", "N/A") + + print(f"## Review by: **{user}** (ID: `{review_id}`)\n") + print(f"* **Submitted At**: `{submitted_at_str}`") + print(f"* **State**: `{state}`") + print(f"* **URL**: <{html_url}>\n") + + if body: + print("\n### Comment:") # Changed heading + print(body) + print("\n---") + + # After processing all overall reviews in this block, update the main variable + if temp_latest_overall_review_dt: + latest_overall_review_activity_dt = temp_latest_overall_review_dt + print("\n") + + + sys.stderr.write(f"Fetching line comments for PR #{pull_request_number} from {OWNER}/{REPO}...\n") + if args.since: + sys.stderr.write(f"Filtering line comments updated since: {args.since}\n") + + comments = get_pull_request_review_comments( + args.token, + pull_request_number, + since=args.since + ) + + if comments is None: + sys.stderr.write(f"Error: Failed to fetch line comments due to an API or network issue.{error_suffix}\nPlease check logs for details.\n") + sys.exit(1) + # Note: The decision to exit if only line comments fail vs. if only overall reviews fail could be nuanced. + # For now, failure to fetch either is treated as a critical error for the script's purpose. + + # Handling for line comments + if not comments: + sys.stderr.write(f"No line comments found for PR #{pull_request_number} (or matching filters).\n") + # If filtered_overall_reviews is also empty, then overall_latest_activity_dt will be None, + # and no 'next command' suggestion will be printed. This is correct. + else: + print("# Review Comments\n\n") + + for comment in comments: + created_at_str = comment.get("created_at") + + current_pos = comment.get("position") + current_line = comment.get("line") + original_line = comment.get("original_line") + + status_text = "" + line_to_display = None + + if current_pos is None: + status_text = STATUS_IRRELEVANT + line_to_display = original_line + elif original_line is not None and current_line != original_line: + status_text = STATUS_OLD + line_to_display = current_line + else: + status_text = STATUS_CURRENT + line_to_display = current_line + + if line_to_display is None: + line_to_display = "N/A" + + if status_text == STATUS_IRRELEVANT and not args.include_irrelevant: + continue + if status_text == STATUS_OLD and args.exclude_old: + continue + + updated_at_str = comment.get("updated_at") + if updated_at_str: + try: + # Compatibility for Python < 3.11 which doesn't handle 'Z' suffix in fromisoformat + if sys.version_info < (3, 11): + dt_str_updated = updated_at_str.replace("Z", "+00:00") + else: + dt_str_updated = updated_at_str + current_comment_activity_dt = datetime.datetime.fromisoformat(dt_str_updated) + if latest_line_comment_activity_dt is None or current_comment_activity_dt > latest_line_comment_activity_dt: + latest_line_comment_activity_dt = current_comment_activity_dt + except ValueError: + sys.stderr.write(f"Warning: Could not parse line comment updated_at for --since suggestion: {updated_at_str}\n") + + user = comment.get("user", {}).get("login", "Unknown user") + path = comment.get("path", "N/A") + body = comment.get("body", "").strip() + + if not body: + continue + + processed_comments_count += 1 + + diff_hunk = comment.get("diff_hunk") + html_url = comment.get("html_url", "N/A") + comment_id = comment.get("id") + in_reply_to_id = comment.get("in_reply_to_id") + + print(f"## Comment by: **{user}** (ID: `{comment_id}`){f' (In Reply To: `{in_reply_to_id}`)' if in_reply_to_id else ''}\n") + if created_at_str: + print(f"* **Timestamp**: `{created_at_str}`") + print(f"* **Status**: `{status_text}`") + print(f"* **File**: `{path}`") + print(f"* **Line**: `{line_to_display}`") + print(f"* **URL**: <{html_url}>\n") + + print("\n### Context:") + print("```") + if diff_hunk and diff_hunk.strip(): + if args.context_lines == 0: + print(diff_hunk) + else: + hunk_lines = diff_hunk.split('\n') + if hunk_lines and hunk_lines[0].startswith("@@ "): + print(hunk_lines[0]) + hunk_lines = hunk_lines[1:] + print("\n".join(hunk_lines[-args.context_lines:])) + else: + print("(No diff hunk available for this comment)") + print("```") + + print("\n### Comment:") + print(body) + print("\n---") + + sys.stderr.write(f"\nPrinted {processed_comments_count} comments to stdout.\n") + + # Determine the overall latest activity timestamp + overall_latest_activity_dt = None + if latest_overall_review_activity_dt and latest_line_comment_activity_dt: + overall_latest_activity_dt = max(latest_overall_review_activity_dt, latest_line_comment_activity_dt) + elif latest_overall_review_activity_dt: + overall_latest_activity_dt = latest_overall_review_activity_dt + elif latest_line_comment_activity_dt: + overall_latest_activity_dt = latest_line_comment_activity_dt + + if overall_latest_activity_dt: + try: + next_since_dt = overall_latest_activity_dt.astimezone(timezone.utc) + timedelta(seconds=2) + next_since_str = next_since_dt.strftime('%Y-%m-%dT%H:%M:%SZ') + + new_cmd_args = [sys.executable, sys.argv[0]] + i = 1 + while i < len(sys.argv): + if sys.argv[i] == "--since": + i += 2 + continue + new_cmd_args.append(sys.argv[i]) + i += 1 + + new_cmd_args.extend(["--since", next_since_str]) + suggested_cmd = " ".join(new_cmd_args) + sys.stderr.write(f"\nTo get comments created after the last one in this batch, try:\n{suggested_cmd}\n") + except Exception as e: + sys.stderr.write(f"\nWarning: Could not generate next command suggestion: {e}\n") + +if __name__ == "__main__": + main()