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-on-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog_flutter": minor
---

Send `$flutter_version` only on the `Application Installed` and `Application Updated` events on iOS, macOS and Android instead of on every event, and stop sending it on web
29 changes: 29 additions & 0 deletions example/ios/RunnerTests/RunnerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ class RunnerTests: XCTestCase {
XCTAssertNil(records.last?.spanId)
}

func testFlutterVersionAddedOnlyToInstallAndUpdateEvents() throws {
var flutterVersions: [String: String] = [:]
let config = PostHogConfig(projectToken: "flutter-version-test", host: "http://127.0.0.1:1")
config.preloadFeatureFlags = false
config.captureApplicationLifecycleEvents = false
config.setBeforeSend(
try XCTUnwrap(PosthogFlutterPlugin.flutterVersionBeforeSend(["flutterVersion": "3.35.0"])),
{ event in
flutterVersions[event.event] = event.properties["$flutter_version"] as? String ?? "none"
return nil
}
)
PostHogSDK.shared.close()
PostHogSDK.shared.setup(config)
defer { PostHogSDK.shared.close() }

for event in ["Application Installed", "Application Updated", "Application Opened", "custom"] {
PostHogSDK.shared.capture(event)
}

XCTAssertEqual(flutterVersions, [
"Application Installed": "3.35.0",
"Application Updated": "3.35.0",
"Application Opened": "none",
"custom": "none",
])
XCTAssertNil(PosthogFlutterPlugin.flutterVersionBeforeSend([:]))
}

func testCaptureLogMissingBodyReturnsError() {
let plugin = PosthogFlutterPlugin()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import com.posthog.PersonProfiles
import com.posthog.PostHog
import com.posthog.PostHogBeforeSend
import com.posthog.PostHogBootstrapConfig
import com.posthog.PostHogCompression
import com.posthog.PostHogConfig
Expand Down Expand Up @@ -808,6 +809,21 @@ class PosthogFlutterPlugin :
sdkName = "posthog-flutter"
sdkVersion = postHogVersion

// Native lifecycle events never pass through Dart, so the version Dart
// reports at setup is added here, once per app build.
posthogConfig.getIfNotNull<String>("flutterVersion") { flutterVersion ->
if (flutterVersion.isNotEmpty()) {
addBeforeSend(
PostHogBeforeSend { event ->
if (event.event == "Application Installed" || event.event == "Application Updated") {
event.properties?.put("\$flutter_version", flutterVersion)
}
event
},
)
}
}

onFeatureFlags =
PostHogOnFeatureFlags {
Log.i("PostHogFlutter", "Android onFeatureFlags triggered. Notifying Dart.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.content.Intent
import android.os.BadParcelableException
import com.google.firebase.FirebaseApp
import com.posthog.PostHogCompression
import com.posthog.PostHogEvent
import com.posthog.android.replay.PostHogScreenshotColorMode
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
Expand Down Expand Up @@ -224,6 +225,28 @@ internal class PosthogFlutterPluginTest {
assertEquals(PostHogCompression.NONE, assertNotNull(plugin.lastBuiltConfig).compression)
}

@Test
fun setup_flutterVersion_addedOnlyToInstallAndUpdateEvents() {
val plugin = PosthogFlutterPlugin()
attach(plugin, Mockito.mock(BinaryMessenger::class.java))
plugin.onMethodCall(
MethodCall("setup", mapOf("projectToken" to "test-token", "flutterVersion" to "3.35.0")),
Mockito.mock(MethodChannel.Result::class.java),
)
val beforeSend = assertNotNull(plugin.lastBuiltConfig).beforeSendList

fun propertiesOf(name: String): Map<String, Any>? =
beforeSend
.fold(PostHogEvent(name, "id", properties = mutableMapOf())) { event, hook ->
assertNotNull(hook.run(event))
}.properties

assertEquals("3.35.0", propertiesOf("Application Installed")?.get("\$flutter_version"))
assertEquals("3.35.0", propertiesOf("Application Updated")?.get("\$flutter_version"))
assertNull(propertiesOf("Application Opened")?.get("\$flutter_version"))
assertNull(propertiesOf("custom")?.get("\$flutter_version"))
}

@Test
fun setup_screenshotControls_preserveDefaults() {
val plugin = PosthogFlutterPlugin()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,24 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
])
}

/// Returns a `beforeSend` block that adds `$flutter_version` to the
/// `Application Installed` and `Application Updated` events, or `nil` when
/// setup carries no `flutterVersion` (Info.plist auto-init, Flutter < 3.32).
///
/// Native lifecycle events never pass through Dart, so the version Dart
/// reports at setup is added here, once per app build.
static func flutterVersionBeforeSend(_ posthogConfig: [String: Any]) -> BeforeSendBlock? {
guard let flutterVersion = posthogConfig["flutterVersion"] as? String, !flutterVersion.isEmpty else {
return nil
}
return { event in
if event.event == "Application Installed" || event.event == "Application Updated" {
event.properties["$flutter_version"] = flutterVersion
}
return event
}
}

// `anchor` is the engine that called setup(); the static `instance` is only the
// Info.plist auto-setup fallback. Resolving from `instance` would bind the mint
// route to whichever engine registered last.
Expand Down Expand Up @@ -436,6 +454,10 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
}
}

if let flutterVersionBeforeSend = flutterVersionBeforeSend(posthogConfig) {
config.setBeforeSend(flutterVersionBeforeSend)
}

// Update SDK name and version
postHogSdkName = "posthog-flutter"
postHogVersion = postHogFlutterVersion
Expand Down
25 changes: 4 additions & 21 deletions posthog_flutter/lib/posthog_flutter_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ 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 @@ -169,22 +168,6 @@ 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 @@ -269,15 +252,15 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {

final processedEvent = await _runBeforeSend(
PostHogEventName.screen,
withFlutterVersion(propsWithScreenName),
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 @@ -508,7 +491,7 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {

final processedEvent = await _runBeforeSend(
PostHogEventName.exception,
withFlutterVersion(exceptionData.cast<String, Object>()),
exceptionData.cast<String, Object>(),
);
if (processedEvent == null) {
printIfDebug(
Expand All @@ -518,7 +501,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
43 changes: 16 additions & 27 deletions posthog_flutter/lib/src/posthog_flutter_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,10 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
_beforeSendCallbacks = config.beforeSend;

try {
await _methodChannel.invokeMethod('setup', config.toMap());
await _methodChannel.invokeMethod('setup', {
...config.toMap(),
...flutterVersionSetupArguments(),
});
} on PlatformException catch (exception) {
printIfDebug('Exeption on setup: $exception');
}
Expand Down Expand Up @@ -290,22 +293,6 @@ 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 @@ -379,7 +366,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
// Apply beforeSend callback - screen events are captured as $screen
final processedEvent = await _runBeforeSend(
PostHogEventName.screen,
withFlutterVersion(propsWithScreenName),
propsWithScreenName,
);
if (processedEvent == null) {
printIfDebug('[PostHog] Screen event dropped by beforeSend: $screenName');
Expand All @@ -388,7 +375,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 @@ -403,9 +390,10 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
processedEvent.properties?.remove(PostHogPropertyName.screenName);

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

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

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

await _methodChannel.invokeMethod('captureException', {
Expand Down
18 changes: 7 additions & 11 deletions posthog_flutter/lib/src/utils/flutter_version.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@
// 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
/// Returns the `flutterVersion` setup argument, or an empty map when the build
/// doesn't report a Flutter version (Flutter < 3.32).
Map<String, String> flutterVersionProperties({
///
/// The native plugins add it as `$flutter_version` to the
/// `Application Installed` and `Application Updated` events the native SDKs
/// capture, since the version only changes with a new app build.
Map<String, String> flutterVersionSetupArguments({
String flutterVersion = _flutterVersion,
}) {
return {
if (flutterVersion.isNotEmpty) r'$flutter_version': flutterVersion,
if (flutterVersion.isNotEmpty) 'flutterVersion': 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};
}
11 changes: 6 additions & 5 deletions posthog_flutter/test/event_shape_snapshot_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -264,14 +264,15 @@ 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;
if (arguments is! Map || !arguments.containsKey('flutterVersion')) {
return arguments;
}
final withoutVersion = Map<String, Object?>.from(arguments);
expect(
properties.remove(r'$flutter_version'),
withoutVersion.remove('flutterVersion'),
const String.fromEnvironment('FLUTTER_VERSION'),
);
return {...arguments, 'properties': properties};
return withoutVersion;
}

Future<void> _expectSnapshot(String name, List<MethodCall> calls) async {
Expand Down
Loading
Loading