Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 49 additions & 46 deletions packages/plugins/plugin_firebase/lib/plugin_firebase.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
library analytics_plugin_firebase;

import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions, Firebase;
import 'package:segment_analytics/event.dart';
import 'package:segment_analytics/logger.dart';
import 'package:segment_analytics/plugin.dart';
import 'package:segment_analytics/map_transform.dart';

import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart'
show FirebaseOptions, Firebase;

export 'package:firebase_core/firebase_core.dart'
show FirebaseOptions, Firebase;

import 'package:segment_analytics/plugin.dart';
import 'package:segment_analytics_plugin_firebase/properties.dart';

export 'package:firebase_core/firebase_core.dart' show FirebaseOptions, Firebase;

class FirebaseDestination extends DestinationPlugin {
final Future<void> firebaseFuture;

Expand All @@ -27,15 +23,26 @@ class FirebaseDestination extends DestinationPlugin {

@override
Future<RawEvent?> identify(IdentifyEvent event) async {
// Set user ID if provided
if (event.userId != null) {
await FirebaseAnalytics.instance.setUserId(id: event.userId!);
await FirebaseAnalytics.instance.setUserId(id: event.userId);
}

// Set user properties from traits if provided
if (event.traits != null) {
await Future.wait(event.traits!.toJson().entries.map((entry) async {
await FirebaseAnalytics.instance
.setUserProperty(name: entry.key, value: entry.value.toString());
}));
// Transform and cast traits to the required format
final transformedTraits = recurseMapper(event.traits?.toJson(), mappings);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mappings was intended for product properties and contains thing like name -> itemName that might not be appropriate everywhere. Was there a mapping in particular you were looking to have here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right that the original mappings were introduced mainly for product properties (e.g. name -> itemName).

However, in this case the purpose of the mapping isn’t just renaming fields — it’s to align our event payload with Firebase Analytics’ expected schema and functionality.

Firebase defines specific parameter names and structures for things like items, item_name, item_id, etc. If we deviate from that or simplify it too much, we risk:

  • Breaking standard eCommerce reporting
  • Losing compatibility with built-in Firebase dashboards
  • Weakening automatic aggregation and funnel tracking
  • Making future integrations (BigQuery, GA4 linking) less reliable

So the mapping here should exist to match Firebase’s required/expected format, not to abstract or reduce it.

If we remove or oversimplify the mapping layer, we’re effectively weakening what Firebase already provides out of the box.

If there’s a specific field you feel shouldn’t be mapped this way, let me know and we can review it — but the intent here is to preserve Firebase functionality, not override it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our approach in our other firebase plugins is usually not to map screen and identify properties, and this change seems likely to break other users existing implementations. The other changes look good and I'd like to merge them.

I think the best solution here is for you branch the project and customize it for your case (which I think you're already doing) and we'll take the more general fixes.

If you want to modify this PR we can merge it, or I can branch it and make the changes myself. Whatever works for you.

Thanks!

Copy link
Copy Markdown
Author

@mohamedzakaria974 mohamedzakaria974 Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your reply, but it shouldn't break anything as it's missing the event parameters as it's already passed from the caller method.

It just adds the missing parts, and if the users don't pass it, it will remain works as you see in line 211 the ScreenEvent did not be touched

final userProperties = castParameterType(transformedTraits as Map<String, Object?>);

// Set each user property individually
for (final entry in userProperties.entries) {
await FirebaseAnalytics.instance.setUserProperty(
name: entry.key,
value: entry.value.toString(),
);
}
}

return event;
}

Expand All @@ -48,44 +55,37 @@ class FirebaseDestination extends DestinationPlugin {
switch (event.event) {
case 'Product Clicked':
if (!(properties.containsKey('list_id') ||
properties.containsKey('list_name') ||
properties.containsKey('list_name') ||
properties.containsKey('name') ||
properties.containsKey('itemId')) ) {
properties.containsKey('itemId'))) {
throw Exception("Missing properties: list_name, list_id, name and itemID");
}

AnalyticsEventItem itemClicked = AnalyticsEventItem(
itemName: properties['name'].toString(),
itemId: properties['itemId'].toString());
AnalyticsEventItem itemClicked =
AnalyticsEventItem(itemName: properties['name'].toString(), itemId: properties['itemId'].toString());

await FirebaseAnalytics.instance.logSelectItem(
itemListName: properties['list_name'].toString(),
itemListId: properties['list_id'].toString(),
items:[itemClicked],
itemListName: properties['list_name'].toString(),
itemListId: properties['list_id'].toString(),
items: [itemClicked],
);
break;
case 'Product Viewed':
await FirebaseAnalytics.instance.logViewItem(
currency: properties["currency"]?.toString(),
items: event.properties == null
? null
: [AnalyticsEventItemJson(event.properties!)],
items: event.properties == null ? null : [AnalyticsEventItemJson(event.properties!)],
value: double.tryParse(properties["value"].toString()));
break;
case 'Product Added':
await FirebaseAnalytics.instance.logAddToCart(
currency: properties["currency"]?.toString(),
items: event.properties == null
? null
: [AnalyticsEventItemJson(event.properties!)],
items: event.properties == null ? null : [AnalyticsEventItemJson(event.properties!)],
value: double.tryParse(properties["value"].toString()));
break;
case 'Product Removed':
await FirebaseAnalytics.instance.logRemoveFromCart(
currency: properties["currency"]?.toString(),
items: event.properties == null
? null
: [AnalyticsEventItemJson(event.properties!)],
items: event.properties == null ? null : [AnalyticsEventItemJson(event.properties!)],
value: double.tryParse(properties["value"].toString()));
break;
case 'Checkout Started':
Expand All @@ -100,7 +100,7 @@ class FirebaseDestination extends DestinationPlugin {
creativeName: properties["creativeName"]?.toString(),
creativeSlot: properties["creativeSlot"]?.toString(),
items: properties["items"] as List<AnalyticsEventItemJson>,
locationId: properties["locationdId"]?.toString(),
locationId: properties["locationId"]?.toString(),
promotionId: properties["promotionId"]?.toString(),
promotionName: properties["promotionName"]?.toString());
break;
Expand Down Expand Up @@ -147,12 +147,10 @@ class FirebaseDestination extends DestinationPlugin {
value: double.tryParse(properties["value"].toString()));
break;
case 'Cart Shared':
if (event.properties == null ||
event.properties!['products'] == null) {
if (event.properties == null || event.properties!['products'] == null) {
log("Error tracking event '${event.event}' for Firebase: products property must be a list of products");
} else if (event.properties!['products'] is List) {
await Future.wait(
(event.properties!['products'] as List).map((product) async {
await Future.wait((event.properties!['products'] as List).map((product) async {
final productProperties = mapProperties(product, mappings);
if (productProperties.containsKey("contentType") &&
productProperties.containsKey("itemId") &&
Expand Down Expand Up @@ -189,20 +187,18 @@ class FirebaseDestination extends DestinationPlugin {
searchTerm: properties["searchTerm"].toString(),
destination: properties["destination"]?.toString(),
endDate: properties["endDate"]?.toString(),
numberOfNights:
int.tryParse(properties["numberOfNights"].toString()),
numberOfPassengers:
int.tryParse(properties["numberOfPassengers"].toString()),
numberOfRooms:
int.tryParse(properties["numberOfRooms"].toString()),
numberOfNights: int.tryParse(properties["numberOfNights"].toString()),
numberOfPassengers: int.tryParse(properties["numberOfPassengers"].toString()),
numberOfRooms: int.tryParse(properties["numberOfRooms"].toString()),
origin: properties["origin"]?.toString(),
startDate: properties["startDate"]?.toString(),
travelClass: properties["travelClass"]?.toString());
break;
default:
await FirebaseAnalytics.instance.logEvent(
name: sanitizeEventName(event.event),
parameters: castParameterType(properties));
name: sanitizeEventName(event.event),
parameters: castParameterType(properties),
);
break;
}
} catch (error) {
Expand All @@ -213,8 +209,15 @@ class FirebaseDestination extends DestinationPlugin {

@override
Future<RawEvent?> screen(ScreenEvent event) async {
FirebaseAnalytics.instance
.logScreenView(screenClass: event.name, screenName: event.name);
// Transform and cast properties to the required format
final transformedProperties = recurseMapper(event.properties, mappings);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mappings here too we might not want, and also missing a null check.

final parameters = castParameterType(transformedProperties as Map<String, Object?>);

FirebaseAnalytics.instance.logScreenView(
screenClass: event.name,
screenName: event.name,
parameters: parameters,
);
return event;
}

Expand Down
60 changes: 37 additions & 23 deletions packages/plugins/plugin_firebase/lib/properties.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,42 @@ Map<String, Object> castParameterType(Map<String, Object?> properties) {
class AnalyticsEventItemJson extends AnalyticsEventItem {
AnalyticsEventItemJson(Map<String, Object?> json)
: super(
affiliation: json['affiliation'].toString(),
currency: json['currency'].toString(),
coupon: json['coupon'].toString(),
creativeName: json['creativeName'].toString(),
creativeSlot: json['creativeSlot'].toString(),
discount: num.tryParse(json['discount'].toString()),
index: int.tryParse(json['index'].toString()),
itemBrand: json['itemBrand'].toString(),
itemCategory: json['itemCategory'].toString(),
itemCategory2: json['itemCategory2'].toString(),
itemCategory3: json['itemCategory3'].toString(),
itemCategory4: json['itemCategory4'].toString(),
itemCategory5: json['itemCategory5'].toString(),
itemId: json['itemId'].toString(),
itemListId: json['itemListId'].toString(),
itemListName: json['itemListName'].toString(),
itemName: json['itemName'].toString(),
itemVariant: json['itemVariant'].toString(),
locationId: json['locationId'].toString(),
price: num.tryParse(json['price'].toString()),
promotionId: json['promotionId'].toString(),
promotionName: json['promotionName'].toString(),
quantity: int.tryParse(json['quantity'].toString()),
affiliation: json['affiliation']?.toString(),
currency: json['currency']?.toString(),
coupon: json['coupon']?.toString(),
creativeName: json['creativeName']?.toString(),
creativeSlot: json['creativeSlot']?.toString(),
discount: _parseNum(json['discount']),
index: _parseInt(json['index']),
itemBrand: json['itemBrand']?.toString(),
itemCategory: json['itemCategory']?.toString(),
itemCategory2: json['itemCategory2']?.toString(),
itemCategory3: json['itemCategory3']?.toString(),
itemCategory4: json['itemCategory4']?.toString(),
itemCategory5: json['itemCategory5']?.toString(),
itemId: json['itemId']?.toString(),
itemListId: json['itemListId']?.toString(),
itemListName: json['itemListName']?.toString(),
itemName: json['itemName']?.toString(),
itemVariant: json['itemVariant']?.toString(),
locationId: json['locationId']?.toString(),
price: _parseNum(json['price']),
promotionId: json['promotionId']?.toString(),
promotionName: json['promotionName']?.toString(),
quantity: _parseInt(json['quantity']),
);

// Helper to safely parse num values
static num? _parseNum(Object? value) {
if (value == null) return null;
if (value is num) return value;
return num.tryParse(value.toString());
}

// Helper to safely parse int values
static int? _parseInt(Object? value) {
if (value == null) return null;
if (value is int) return value;
return int.tryParse(value.toString());
}
}
14 changes: 7 additions & 7 deletions packages/plugins/plugin_firebase/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: segment_analytics_plugin_firebase
description: The hassle-free way to add Segment analytics to your Flutter app.
version: 1.0.1
version: 1.1.0
homepage: https://github.com/segmentio/analytics_flutter#readme
repository: https://github.com/segmentio/analytics_flutter/tree/main/packages/plugins/plugin_firebase#readme
issue_tracker: https://github.com/segmentio/analytics_flutter/issues
Expand All @@ -10,15 +10,15 @@ environment:
flutter: ">=3.16.0"

dependencies:
firebase_analytics: ^11.3.3
firebase_core: ^3.6.0
firebase_analytics: ^12.0.4
firebase_core: ^4.2.1
flutter:
sdk: flutter
json_annotation: ^4.8.0
segment_analytics: ^1.1.1
json_annotation: ^4.9.0
segment_analytics: ^1.1.10

dev_dependencies:
build_runner: ^2.3.3
build_runner: ^2.10.3
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter_lints: ^6.0.0