diff --git a/analytics/src/analytics_desktop.cc b/analytics/src/analytics_desktop.cc new file mode 100644 index 000000000..636ecb8e2 --- /dev/null +++ b/analytics/src/analytics_desktop.cc @@ -0,0 +1,373 @@ +// 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. + +#include "analytics/src/windows/analytics_windows.h" +#include "app/src/include/firebase/app.h" +#include "analytics/src/include/firebase/analytics.h" +#include "analytics/src/common/analytics_common.h" +#include "common/src/include/firebase/variant.h" +#include "app/src/include/firebase/future.h" +#include "app/src/include/firebase/log.h" +#include "app/src/future_manager.h" // For FutureData + +#include +#include +#include + +namespace firebase { +namespace analytics { + +// Future data for analytics. +// This is initialized in `Initialize()` and cleaned up in `Terminate()`. +static FutureData* g_future_data = nullptr; + +// Initializes the Analytics desktop API. +// This function must be called before any other Analytics methods. +void Initialize(const App& app) { + // The 'app' parameter is not directly used by the underlying Google Analytics C API + // for Windows for global initialization. It's included for API consistency + // with other Firebase platforms. + (void)app; + + if (g_future_data) { + LogWarning("Analytics: Initialize() called when already initialized."); + } else { + g_future_data = new FutureData(internal::kAnalyticsFnCount); + } +} + +// Terminates the Analytics desktop API. +// Call this function when Analytics is no longer needed to free up resources. +void Terminate() { + if (g_future_data) { + delete g_future_data; + g_future_data = nullptr; + } else { + LogWarning("Analytics: Terminate() called when not initialized or already terminated."); + } +} + +static void ConvertParametersToGAParams( + const Parameter* parameters, + size_t number_of_parameters, + GoogleAnalytics_EventParameters* c_event_params) { + if (!parameters || number_of_parameters == 0 || !c_event_params) { + return; + } + + for (size_t i = 0; i < number_of_parameters; ++i) { + const Parameter& param = parameters[i]; + if (param.name == nullptr || param.name[0] == '\0') { + LogError("Analytics: Parameter name cannot be null or empty."); + continue; + } + + if (param.value.is_int64()) { + GoogleAnalytics_EventParameters_InsertInt(c_event_params, param.name, + param.value.int64_value()); + } else if (param.value.is_double()) { + GoogleAnalytics_EventParameters_InsertDouble(c_event_params, param.name, + param.value.double_value()); + } else if (param.value.is_string()) { + GoogleAnalytics_EventParameters_InsertString( + c_event_params, param.name, param.value.string_value()); + } else if (param.value.is_vector()) { + // Vector types for top-level event parameters are not supported on Desktop. + // Only specific complex types (like a map processed into an ItemVector) are handled. + LogError("Analytics: Parameter '%s' has type Vector, which is unsupported for event parameters on Desktop. Skipping.", param.name); + continue; // Skip this parameter + } else if (param.value.is_map()) { + // This block handles parameters that are maps. + // Each key-value pair from the input map is converted into a distinct GoogleAnalytics_Item. + // In each such GoogleAnalytics_Item, the original key from the map is used directly + // as the property key, and the original value (which must be a primitive) + // is set as the property's value. + // All these GoogleAnalytics_Items are then bundled into a single + // GoogleAnalytics_ItemVector, which is associated with the original parameter's name. + const std::map& user_map = + param.value.map_value(); + if (user_map.empty()) { + LogWarning("Analytics: Parameter '%s' is an empty map. Skipping.", param.name); + continue; // Skip this parameter + } + + GoogleAnalytics_ItemVector* c_item_vector = + GoogleAnalytics_ItemVector_Create(); + if (!c_item_vector) { + LogError("Analytics: Failed to create ItemVector for map parameter '%s'.", param.name); + continue; // Skip this parameter + } + + bool item_vector_populated = false; + for (const auto& entry : user_map) { + const std::string& key_from_map = entry.first; + const firebase::Variant& value_from_map = entry.second; + + GoogleAnalytics_Item* c_item = GoogleAnalytics_Item_Create(); + if (!c_item) { + LogError("Analytics: Failed to create Item for key '%s' in map parameter '%s'.", key_from_map.c_str(), param.name); + continue; // Skip this key-value pair, try next one in map + } + + bool successfully_set_property = false; + if (value_from_map.is_int64()) { + GoogleAnalytics_Item_InsertInt(c_item, key_from_map.c_str(), value_from_map.int64_value()); + successfully_set_property = true; + } else if (value_from_map.is_double()) { + GoogleAnalytics_Item_InsertDouble(c_item, key_from_map.c_str(), value_from_map.double_value()); + successfully_set_property = true; + } else if (value_from_map.is_string()) { + GoogleAnalytics_Item_InsertString(c_item, key_from_map.c_str(), value_from_map.string_value()); + successfully_set_property = true; + } else { + LogWarning("Analytics: Value for key '%s' in map parameter '%s' has an unsupported Variant type. This key-value pair will be skipped.", key_from_map.c_str(), param.name); + // successfully_set_property remains false + } + + if (successfully_set_property) { + GoogleAnalytics_ItemVector_InsertItem(c_item_vector, c_item); + // c_item is now owned by c_item_vector + item_vector_populated = true; // Mark that the vector has at least one item + } else { + // If no property was set (e.g., value type was unsupported), destroy the created c_item. + GoogleAnalytics_Item_Destroy(c_item); + } + } + + if (item_vector_populated) { + GoogleAnalytics_EventParameters_InsertItemVector( + c_event_params, param.name, c_item_vector); + // c_item_vector is now owned by c_event_params + } else { + // If no items were successfully created and added (e.g., all values in map were unsupported types) + GoogleAnalytics_ItemVector_Destroy(c_item_vector); + LogWarning("Analytics: Map parameter '%s' resulted in an empty ItemVector; no valid key-value pairs found or all values had unsupported types. This map parameter was skipped.", param.name); + } + } else { + LogWarning("Analytics: Unsupported variant type for parameter '%s'.", param.name); + } + } +} + +// Logs an event with the given name and parameters. +void LogEvent(const char* name, + const Parameter* parameters, + size_t number_of_parameters) { + if (name == nullptr || name[0] == '\0') { + LogError("Analytics: Event name cannot be null or empty."); + return; + } + + GoogleAnalytics_EventParameters* c_event_params = nullptr; + if (parameters != nullptr && number_of_parameters > 0) { + c_event_params = GoogleAnalytics_EventParameters_Create(); + if (!c_event_params) { + LogError("Analytics: Failed to create event parameters map for event '%s'.", name); + return; + } + ConvertParametersToGAParams(parameters, number_of_parameters, c_event_params); + } + + GoogleAnalytics_LogEvent(name, c_event_params); + // GoogleAnalytics_LogEvent is expected to handle the lifecycle of c_event_params if non-null. +} + +// Sets a user property to the given value. +// +// Up to 25 user property names are supported. Once set, user property values +// persist throughout the app lifecycle and across sessions. +// +// @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` or an empty string will +// clear the user property. Must be UTF-8 encoded if not nullptr. +void SetUserProperty(const char* name, const char* property) { + if (name == nullptr || name[0] == '\0') { + LogError("Analytics: User property name cannot be null or empty."); + return; + } + // The C API GoogleAnalytics_SetUserProperty allows value to be nullptr to remove the property. + // If value is an empty string, it might also be treated as clearing by some backends, + // or it might be an invalid value. The C API doc says: + // "Setting the value to `nullptr` remove the user property." + // For consistency, we pass it as is. + GoogleAnalytics_SetUserProperty(name, property); +} + +// 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 if not nullptr, and no more than 256 +// characters long, and UTF-8 encoded. Setting user_id to `nullptr` removes +// the user ID. +void SetUserId(const char* user_id) { + // The C API GoogleAnalytics_SetUserId allows user_id to be nullptr to clear the user ID. + // The C API documentation also mentions: "The user ID must be non-empty and + // no more than 256 characters long". + // We'll pass nullptr as is. If user_id is an empty string "", this might be + // an issue for the underlying C API or backend if it expects non-empty. + // However, the Firebase API typically allows passing "" to clear some fields, + // or it's treated as an invalid value. For SetUserId, `nullptr` is the standard + // clear mechanism. An empty string might be an invalid ID. + // For now, we are not adding extra validation for empty string beyond what C API does. + // Consider adding a check for empty string if Firebase spec requires it. + // e.g., if (user_id != nullptr && user_id[0] == '\0') { /* log error */ return; } + GoogleAnalytics_SetUserId(user_id); +} + +// 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); +} + +// Clears all analytics data for this app from the device and resets the app +// instance ID. +void ResetAnalyticsData() { + GoogleAnalytics_ResetAnalyticsData(); +} + +// --- Stub Implementations for Unsupported Features --- + +void SetConsent(const std::map& consent_settings) { + // Not supported by the Windows C API. + (void)consent_settings; // Mark as unused + LogWarning("Analytics: SetConsent() is not supported and has no effect on Desktop."); +} + +void LogEvent(const char* name) { + LogEvent(name, nullptr, 0); +} + +void LogEvent(const char* name, const char* parameter_name, + const char* parameter_value) { + if (parameter_name == nullptr) { + LogEvent(name, nullptr, 0); + return; + } + Parameter param(parameter_name, parameter_value); + LogEvent(name, ¶m, 1); +} + +void LogEvent(const char* name, const char* parameter_name, + const double parameter_value) { + if (parameter_name == nullptr) { + LogEvent(name, nullptr, 0); + return; + } + Parameter param(parameter_name, parameter_value); + LogEvent(name, ¶m, 1); +} + +void LogEvent(const char* name, const char* parameter_name, + const int64_t parameter_value) { + if (parameter_name == nullptr) { + LogEvent(name, nullptr, 0); + return; + } + Parameter param(parameter_name, parameter_value); + LogEvent(name, ¶m, 1); +} + +void LogEvent(const char* name, const char* parameter_name, + const int parameter_value) { + if (parameter_name == nullptr) { + LogEvent(name, nullptr, 0); + return; + } + Parameter param(parameter_name, static_cast(parameter_value)); + LogEvent(name, ¶m, 1); +} + +void InitiateOnDeviceConversionMeasurementWithEmailAddress( + const char* email_address) { + (void)email_address; + LogWarning("Analytics: InitiateOnDeviceConversionMeasurementWithEmailAddress() is not supported and has no effect on Desktop."); +} + +void InitiateOnDeviceConversionMeasurementWithPhoneNumber( + const char* phone_number) { + (void)phone_number; + LogWarning("Analytics: InitiateOnDeviceConversionMeasurementWithPhoneNumber() is not supported and has no effect on Desktop."); +} + +void InitiateOnDeviceConversionMeasurementWithHashedEmailAddress( + std::vector hashed_email_address) { + (void)hashed_email_address; + LogWarning("Analytics: InitiateOnDeviceConversionMeasurementWithHashedEmailAddress() is not supported and has no effect on Desktop."); +} + +void InitiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + std::vector hashed_phone_number) { + (void)hashed_phone_number; + LogWarning("Analytics: InitiateOnDeviceConversionMeasurementWithHashedPhoneNumber() is not supported and has no effect on Desktop."); +} + +void SetSessionTimeoutDuration(int64_t milliseconds) { + (void)milliseconds; + LogWarning("Analytics: SetSessionTimeoutDuration() is not supported and has no effect on Desktop."); +} + +Future GetAnalyticsInstanceId() { + LogWarning("Analytics: GetAnalyticsInstanceId() is not supported on Desktop."); + if (!g_future_data) { + LogError("Analytics: API not initialized; call Initialize() first."); + static firebase::Future invalid_future; // Default invalid + if (!g_future_data) return invalid_future; // Or some other error future + } + const auto handle = + g_future_data->CreateFuture(internal::kAnalyticsFn_GetAnalyticsInstanceId, nullptr); + g_future_data->CompleteFuture(handle, 0 /* error_code */, nullptr /* error_message_string */); + return g_future_data->GetFuture(handle); +} + +Future GetAnalyticsInstanceIdLastResult() { + if (!g_future_data) { + LogError("Analytics: API not initialized; call Initialize() first."); + static firebase::Future invalid_future; + return invalid_future; + } + return g_future_data->LastResult(internal::kAnalyticsFn_GetAnalyticsInstanceId); +} + +Future GetSessionId() { + LogWarning("Analytics: GetSessionId() is not supported on Desktop."); + if (!g_future_data) { + LogError("Analytics: API not initialized; call Initialize() first."); + static firebase::Future invalid_future; + return invalid_future; + } + const auto handle = + g_future_data->CreateFuture(internal::kAnalyticsFn_GetSessionId, nullptr); + g_future_data->CompleteFuture(handle, 0 /* error_code */, nullptr /* error_message_string */); + return g_future_data->GetFuture(handle); +} + +Future GetSessionIdLastResult() { + if (!g_future_data) { + LogError("Analytics: API not initialized; call Initialize() first."); + static firebase::Future invalid_future; + return invalid_future; + } + return g_future_data->LastResult(internal::kAnalyticsFn_GetSessionId); +} + +} // namespace analytics +} // namespace firebase