Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/flutter-version-property.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog_flutter": minor
---

Add `$flutter_version` (for example `3.32.0`) to events captured through `capture`, `screen` and `captureException`, including the event `beforeSend` receives. Requires apps built with Flutter 3.32 or later; it's omitted on older Flutter versions and on events the native iOS/Android SDKs or posthog-js capture on their own, such as lifecycle events.
25 changes: 21 additions & 4 deletions posthog_flutter/lib/posthog_flutter_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'src/posthog_flutter_web_handler.dart';
import 'src/utils/before_send.dart';
import 'src/replay/web/web_canvas_mask_provider.dart';
import 'src/utils/capture_utils.dart';
import 'src/utils/flutter_version.dart';

/// A web implementation of the PosthogFlutterPlatform of the PosthogFlutter plugin.
class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
Expand Down Expand Up @@ -168,6 +169,22 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
Map<String, Object>? properties,
Map<String, Object>? userProperties,
Map<String, Object>? userPropertiesSetOnce,
}) {
return _capture(
eventName: eventName,
properties: withFlutterVersion(properties),
userProperties: userProperties,
userPropertiesSetOnce: userPropertiesSetOnce,
);
}

/// Captures [properties] as given. Callers add `$flutter_version` first;
/// renamed screen/exception events skip it so a beforeSend removal sticks.
Future<void> _capture({
required String eventName,
Map<String, Object>? properties,
Map<String, Object>? userProperties,
Map<String, Object>? userPropertiesSetOnce,
}) async {
final processedEvent = await _runBeforeSend(
eventName,
Expand Down Expand Up @@ -252,15 +269,15 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {

final processedEvent = await _runBeforeSend(
PostHogEventName.screen,
propsWithScreenName,
withFlutterVersion(propsWithScreenName),
);
if (processedEvent == null) {
printIfDebug('[PostHog] Screen event dropped by beforeSend: $screenName');
return;
}

if (processedEvent.event != PostHogEventName.screen) {
await capture(
await _capture(
eventName: processedEvent.event,
properties: processedEvent.properties?.cast<String, Object>(),
);
Expand Down Expand Up @@ -491,7 +508,7 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {

final processedEvent = await _runBeforeSend(
PostHogEventName.exception,
exceptionData.cast<String, Object>(),
withFlutterVersion(exceptionData.cast<String, Object>()),
);
if (processedEvent == null) {
printIfDebug(
Expand All @@ -501,7 +518,7 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
}

if (processedEvent.event != PostHogEventName.exception) {
await capture(
await _capture(
eventName: processedEvent.event,
properties: processedEvent.properties?.cast<String, Object>(),
);
Expand Down
39 changes: 27 additions & 12 deletions posthog_flutter/lib/src/posthog_flutter_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'surveys/models/survey_callbacks.dart';
import 'error_tracking/dart_exception_processor.dart';
import 'utils/before_send.dart';
import 'utils/capture_utils.dart';
import 'utils/flutter_version.dart';
import 'utils/property_normalizer.dart';

import 'feature_flag_result.dart';
Expand Down Expand Up @@ -289,6 +290,22 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
Map<String, Object>? properties,
Map<String, Object>? userProperties,
Map<String, Object>? userPropertiesSetOnce,
}) {
return _capture(
eventName: eventName,
properties: withFlutterVersion(properties),
userProperties: userProperties,
userPropertiesSetOnce: userPropertiesSetOnce,
);
}

/// Captures [properties] as given. Callers add `$flutter_version` first;
/// renamed screen/exception events skip it so a beforeSend removal sticks.
Future<void> _capture({
required String eventName,
Map<String, Object>? properties,
Map<String, Object>? userProperties,
Map<String, Object>? userPropertiesSetOnce,
}) async {
if (!isSupportedPlatform()) {
return;
Expand Down Expand Up @@ -362,7 +379,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
// Apply beforeSend callback - screen events are captured as $screen
final processedEvent = await _runBeforeSend(
PostHogEventName.screen,
propsWithScreenName,
withFlutterVersion(propsWithScreenName),
);
if (processedEvent == null) {
printIfDebug('[PostHog] Screen event dropped by beforeSend: $screenName');
Expand All @@ -371,7 +388,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {

// If event name was changed, use regular capture() instead
if (processedEvent.event != PostHogEventName.screen) {
await capture(
await _capture(
eventName: processedEvent.event,
properties: processedEvent.properties?.cast<String, Object>(),
);
Expand All @@ -386,10 +403,9 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
processedEvent.properties?.remove(PostHogPropertyName.screenName);

try {
final normalizedProperties = processedEvent.properties?.isNotEmpty == true
? PropertyNormalizer.normalize(
processedEvent.properties!.cast<String, Object>(),
)
final eventProperties = processedEvent.properties?.cast<String, Object>();
final normalizedProperties = eventProperties?.isNotEmpty == true
? PropertyNormalizer.normalize(eventProperties!)
: null;

await _methodChannel.invokeMethod('screen', {
Expand Down Expand Up @@ -771,7 +787,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
// Apply beforeSend callback - exception events are captured as $exception
final processedEvent = await _runBeforeSend(
PostHogEventName.exception,
exceptionProps.cast<String, Object>(),
withFlutterVersion(exceptionProps.cast<String, Object>()),
);
if (processedEvent == null) {
printIfDebug(
Expand All @@ -782,7 +798,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {

// If event name was changed, use capture() instead
if (processedEvent.event != PostHogEventName.exception) {
await capture(
await _capture(
eventName: processedEvent.event,
properties: processedEvent.properties?.cast<String, Object>(),
);
Expand All @@ -791,10 +807,9 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {

// Add timestamp from Flutter side (will be used and removed from native plugins)
final timestamp = DateTime.now().millisecondsSinceEpoch;
final normalizedData = processedEvent.properties != null
? PropertyNormalizer.normalize(
processedEvent.properties!.cast<String, Object>(),
)
final eventProperties = processedEvent.properties?.cast<String, Object>();
final normalizedData = eventProperties != null
? PropertyNormalizer.normalize(eventProperties)
: <String, Object>{};

await _methodChannel.invokeMethod('captureException', {
Expand Down
22 changes: 22 additions & 0 deletions posthog_flutter/lib/src/utils/flutter_version.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Flutter 3.32+ compiles its own version into every build as this define. It
// backs `FlutterVersion.version`, which can't be referenced directly because
// that class is missing from the older Flutter versions this package supports.
const _flutterVersion = String.fromEnvironment('FLUTTER_VERSION');

/// Returns the `$flutter_version` property, or an empty map when the build
/// doesn't report a Flutter version (Flutter < 3.32).
Map<String, String> flutterVersionProperties({
String flutterVersion = _flutterVersion,
}) {
return {
if (flutterVersion.isNotEmpty) r'$flutter_version': flutterVersion,
};
}

/// Adds the `$flutter_version` property to [properties], replacing any value
/// the caller set. `beforeSend` runs afterwards and can still change it.
Map<String, Object>? withFlutterVersion(Map<String, Object>? properties) {
final versionProperties = flutterVersionProperties();
if (versionProperties.isEmpty) return properties;
return {...?properties, ...versionProperties};
}
15 changes: 14 additions & 1 deletion posthog_flutter/test/event_shape_snapshot_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -261,13 +261,26 @@ void main() {
});
}

// The Flutter version depends on the SDK running the tests, and Flutter < 3.32
// doesn't report one, so it's checked here and left out of the snapshots.
Object? _withoutFlutterVersion(Object? arguments) {
if (arguments is! Map || arguments['properties'] is! Map) return arguments;
final properties = Map<String, Object?>.from(arguments['properties'] as Map);
if (!properties.containsKey(r'$flutter_version')) return arguments;
expect(
properties.remove(r'$flutter_version'),
const String.fromEnvironment('FLUTTER_VERSION'),
);
return {...arguments, 'properties': properties};
}

Future<void> _expectSnapshot(String name, List<MethodCall> calls) async {
final snapshotFile = File('test/snapshots/$name');
final actual = calls
.map(
(call) => <String, Object?>{
'method': call.method,
'arguments': call.arguments,
'arguments': _withoutFlutterVersion(call.arguments),
},
)
.toList();
Expand Down
Loading
Loading