-
Notifications
You must be signed in to change notification settings - Fork 56
feat: add beforeSend hook #255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,13 @@ | ||
| ## Next | ||
|
|
||
| - feat: add `beforeSend` callback to `PostHogConfig` for dropping or modifying events before they are sent to PostHog ([#255](https://github.com/PostHog/posthog-flutter/pull/255)) | ||
| - **Limitation**: | ||
| - Does NOT intercept native-initiated events such as: | ||
| - Session replay events (`$snapshot`) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, mention the sessionReplay flag |
||
| - Application lifecycle events (`Application Opened`, etc.) | ||
| - etc. | ||
|
Comment on lines
+7
to
+8
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. events captured by the captureApplicationLifecycleEvents config |
||
| - Only user-provided properties are available; system properties (like `$device_type`, `$session_id`) are added by the native SDK at a later stage. | ||
|
|
||
| # 5.11.1 | ||
|
|
||
| - fix: RichText, SelectableText, TextField labels and hints not being masked in session replay ([#251](https://github.com/PostHog/posthog-flutter/pull/251)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,11 @@ | ||
| import 'posthog_event.dart'; | ||
| import 'posthog_flutter_platform_interface.dart'; | ||
|
|
||
| /// Callback to intercept and modify events before they are sent to PostHog. | ||
| /// | ||
| /// Return a possibly modified event to send it, or return `null` to drop it. | ||
| typedef BeforeSendCallback = PostHogEvent? Function(PostHogEvent event); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think this type should be `FutureOr<PostHogEvent?> since people might need to use async methods to filter stuff
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure about this one since we've talked about eventually moving away from async method channels. This would complicate the capture pipeline potentially. Plus all the other sdks are sync hooks?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreed, but right now even our sdk is async, so if you call isFeatureFlag something is async, which maybe its an use case to be used for filtering things, so we need to make it FutureOr which allows to make it async or not. |
||
|
|
||
| enum PostHogPersonProfiles { never, always, identifiedOnly } | ||
|
|
||
| enum PostHogDataMode { wifi, cellular, any } | ||
|
|
@@ -52,11 +58,47 @@ class PostHogConfig { | |
| /// callback to access the loaded flag values. | ||
| OnFeatureFlagsCallback? onFeatureFlags; | ||
|
|
||
| /// Callback to intercept and modify events before they are sent to PostHog. | ||
| /// | ||
| /// This callback is invoked for events captured via Dart APIs: | ||
| /// - `Posthog().capture()` - custom events | ||
| /// - `Posthog().screen()` - screen events (event name will be `$screen`) | ||
| /// - `Posthog().captureException()` - exception events (event name will be `$exception`) | ||
| /// | ||
| /// Return a possibly modified event to send it, or return `null` to drop it. | ||
| /// | ||
| /// **Example:** | ||
| /// ```dart | ||
| /// config.beforeSend = (event) { | ||
| /// // Drop specific events | ||
| /// if (event.event == 'sensitive_event') { | ||
| /// return null; | ||
| /// } | ||
| /// // Modify event properties | ||
| /// event.properties ??= {}; | ||
| /// event.properties?['some_custom_field'] = 'new value'; | ||
| /// return event; | ||
| /// }; | ||
| /// ``` | ||
| /// | ||
| /// **Limitations:** | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see comments in the changelog |
||
| /// - This callback does NOT intercept native-initiated events such as: | ||
| /// - Session replay events (`$snapshot`) | ||
| /// - Application lifecycle events (`Application Opened`, etc.) | ||
| /// - Only user-provided properties are available; system properties | ||
| /// (like `$device_type`, `$session_id`) are added by the native SDK at a later stage. | ||
| /// | ||
| /// **Note:** | ||
| /// - This callback runs synchronously on the Dart side | ||
| /// - Exceptions in the callback will cause the event to be sent unchanged | ||
| BeforeSendCallback? beforeSend; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the other SDKs we allow one function or a list of tunction, we dont support the list of functions here
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah good point |
||
|
|
||
| // TODO: missing getAnonymousId, propertiesSanitizer, captureDeepLinks integrations | ||
|
|
||
| PostHogConfig( | ||
| this.apiKey, { | ||
| this.onFeatureFlags, | ||
| this.beforeSend, | ||
| }); | ||
|
|
||
| Map<String, dynamic> toMap() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| /// Represents an event that can be modified or dropped by the [BeforeSendCallback]. | ||
| /// | ||
| /// This class is used in the beforeSend callback to allow modification of events before they are sent to PostHog. | ||
| class PostHogEvent { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. at least on the other SDKs, this event has a specific map for $set and $set_once |
||
| /// The name of the event (e.g., 'button_clicked', '$screen', '$exception') | ||
| String event; | ||
|
|
||
| /// User-provided properties for this event. | ||
| /// | ||
| /// Note: System properties (like $device_type, $session_id, etc.) are added | ||
| /// by the native SDK at a later stage and are not available in this map. | ||
| Map<String, Object?>? properties; | ||
|
|
||
| PostHogEvent({ | ||
| required this.event, | ||
| this.properties, | ||
| }); | ||
|
|
||
| @override | ||
| String toString() { | ||
| return 'PostHogEvent(event: $event, properties: $properties)'; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import 'error_tracking/dart_exception_processor.dart'; | |
| import 'utils/property_normalizer.dart'; | ||
|
|
||
| import 'posthog_config.dart'; | ||
| import 'posthog_event.dart'; | ||
| import 'posthog_flutter_platform_interface.dart'; | ||
|
|
||
| /// An implementation of [PosthogFlutterPlatformInterface] that uses method channels. | ||
|
|
@@ -29,6 +30,31 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { | |
| /// Stored configuration for accessing inAppIncludes and other settings | ||
| PostHogConfig? _config; | ||
|
|
||
| /// Stored beforeSend callback for dropping/modifying events | ||
| BeforeSendCallback? _beforeSendCallback; | ||
|
|
||
| /// Applies the beforeSend callback to an event. | ||
| /// Returns the possibly modified event, or null if the event should be dropped. | ||
| PostHogEvent? _runBeforeSend( | ||
| String eventName, Map<String, Object>? properties) { | ||
| final event = PostHogEvent( | ||
| event: eventName, | ||
| properties: properties, | ||
| ); | ||
|
|
||
| if (_beforeSendCallback == null) { | ||
| return event; | ||
| } | ||
|
|
||
| try { | ||
| return _beforeSendCallback!(event); | ||
| } catch (e) { | ||
| printIfDebug('[PostHog] beforeSend callback threw exception: $e'); | ||
| // On exception, pass through unchanged | ||
| return event; | ||
| } | ||
| } | ||
|
|
||
| /// Native plugin calls to Flutter | ||
| /// | ||
| Future<dynamic> _handleMethodCall(MethodCall call) async { | ||
|
|
@@ -133,6 +159,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { | |
| } | ||
|
|
||
| _onFeatureFlagsCallback = config.onFeatureFlags; | ||
| _beforeSendCallback = config.beforeSend; | ||
|
|
||
| try { | ||
| await _methodChannel.invokeMethod('setup', config.toMap()); | ||
|
|
@@ -180,12 +207,21 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { | |
| return; | ||
| } | ||
|
|
||
| // Apply beforeSend callback | ||
| final processedEvent = _runBeforeSend(eventName, properties); | ||
| if (processedEvent == null) { | ||
| printIfDebug('[PostHog] Event dropped by beforeSend: $eventName'); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| final normalizedProperties = | ||
| properties != null ? PropertyNormalizer.normalize(properties) : null; | ||
| final normalizedProperties = processedEvent.properties != null | ||
| ? PropertyNormalizer.normalize( | ||
| processedEvent.properties!.cast<String, Object>()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. never use
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. create a CLAUDE.md if needed
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a null check right above but yes better avoid this |
||
| : null; | ||
|
|
||
| await _methodChannel.invokeMethod('capture', { | ||
| 'eventName': eventName, | ||
| 'eventName': processedEvent.event, | ||
| if (normalizedProperties != null) 'properties': normalizedProperties, | ||
| }); | ||
| } on PlatformException catch (exception) { | ||
|
|
@@ -202,12 +238,42 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { | |
| return; | ||
| } | ||
|
|
||
| // Add screenName as $screen_name property for beforeSend | ||
| final propsWithScreenName = <String, Object>{ | ||
| '\$screen_name': screenName, | ||
| ...?properties, | ||
| }; | ||
|
|
||
| // Apply beforeSend callback - screen events are captured as $screen | ||
| final processedEvent = _runBeforeSend('\$screen', propsWithScreenName); | ||
| if (processedEvent == null) { | ||
| printIfDebug('[PostHog] Screen event dropped by beforeSend: $screenName'); | ||
| return; | ||
| } | ||
|
|
||
| // If event name was changed, use regular capture() instead | ||
| if (processedEvent.event != '\$screen') { | ||
| await capture( | ||
| eventName: processedEvent.event, | ||
| properties: processedEvent.properties?.cast<String, Object>(), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Get the (possibly modified) screen name from properties and remove it | ||
| final finalScreenName = | ||
| processedEvent.properties?['\$screen_name'] as String? ?? screenName; | ||
| // It will be added back by native sdk | ||
| processedEvent.properties?.remove('\$screen_name'); | ||
|
|
||
| try { | ||
| final normalizedProperties = | ||
| properties != null ? PropertyNormalizer.normalize(properties) : null; | ||
| final normalizedProperties = processedEvent.properties?.isNotEmpty == true | ||
| ? PropertyNormalizer.normalize( | ||
| processedEvent.properties!.cast<String, Object>()) | ||
| : null; | ||
|
|
||
| await _methodChannel.invokeMethod('screen', { | ||
| 'screenName': screenName, | ||
| 'screenName': finalScreenName, | ||
| if (normalizedProperties != null) 'properties': normalizedProperties, | ||
| }); | ||
| } on PlatformException catch (exception) { | ||
|
|
@@ -456,7 +522,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { | |
| } | ||
|
|
||
| try { | ||
| final exceptionData = DartExceptionProcessor.processException( | ||
| final exceptionProps = DartExceptionProcessor.processException( | ||
| error: error, | ||
| stackTrace: stackTrace, | ||
| properties: properties, | ||
|
|
@@ -465,10 +531,30 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { | |
| inAppByDefault: _config?.errorTrackingConfig.inAppByDefault ?? true, | ||
| ); | ||
|
|
||
| // Apply beforeSend callback - exception events are captured as $exception | ||
| final processedEvent = | ||
| _runBeforeSend('\$exception', exceptionProps.cast<String, Object>()); | ||
| if (processedEvent == null) { | ||
| printIfDebug( | ||
| '[PostHog] Exception event dropped by beforeSend: ${error.runtimeType}'); | ||
| return; | ||
| } | ||
|
|
||
| // If event name was changed, use capture() instead | ||
| if (processedEvent.event != '\$exception') { | ||
| await capture( | ||
| eventName: processedEvent.event, | ||
| properties: processedEvent.properties?.cast<String, Object>(), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Add timestamp from Flutter side (will be used and removed from native plugins) | ||
| final timestamp = DateTime.now().millisecondsSinceEpoch; | ||
| final normalizedData = | ||
| PropertyNormalizer.normalize(exceptionData.cast<String, Object>()); | ||
| final normalizedData = processedEvent.properties != null | ||
| ? PropertyNormalizer.normalize( | ||
| processedEvent.properties!.cast<String, Object>()) | ||
| : <String, Object>{}; | ||
|
|
||
| await _methodChannel.invokeMethod('captureException', | ||
| {'timestamp': timestamp, 'properties': normalizedData}); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should mention events captured by sendFeatureFlagEvents