diff --git a/.changeset/v6-major-release.md b/.changeset/v6-major-release.md new file mode 100644 index 00000000..9d30fb0e --- /dev/null +++ b/.changeset/v6-major-release.md @@ -0,0 +1,17 @@ +--- +"posthog_flutter": major +--- + +- **Breaking:** bump the minimum iOS deployment target from 13.0 to 15.0 and macOS from 10.15 to 11.0 — see the [migration guide](https://github.com/PostHog/posthog-flutter/blob/main/posthog_flutter/MIGRATION.md) +- **Breaking:** remove CocoaPods support on iOS and macOS — install through Swift Package Manager, which needs Flutter 3.41 or later and doesn't support add-to-app modules yet +- **Breaking:** change `getFeatureFlagResult()` to follow `PostHogConfig.sendFeatureFlagEvent` on iOS, macOS, Android, Windows and Linux when `sendFeatureFlagEvent` is omitted — pass `sendFeatureFlagEvent: true` to keep capturing `$feature_flag_called` +- **Breaking:** rename `getFeatureFlagResult(sendEvent:)` to `getFeatureFlagResult(sendFeatureFlagEvent:)` +- **Breaking:** remove `PostHogConfig.apiKey` and the `com.posthog.posthog.API_KEY` manifest and Info.plist key — use `projectToken` and `com.posthog.posthog.PROJECT_TOKEN` +- **Breaking:** remove `Posthog().enable()` and `Posthog().disable()` — use `optIn()` and `optOut()` +- **Breaking:** remove `PostHogConfig.sendFeatureFlagEvents` — use `sendFeatureFlagEvent` +- **Breaking:** remove `Posthog().getFeatureFlagPayload()` — use `getFeatureFlagResult(key, sendFeatureFlagEvent: false)` and read `payload` +- **Breaking:** remove the `PostHogSessionReplayConfig.debouncerDelay` setter — use `throttleDelay` +- **Breaking:** remove `PostHogDataMode.cellular` — use `PostHogDataMode.any` +- **Breaking:** remove `toMap()` from the config classes and `PostHogFeatureFlagResult.fromMap()` from the public API +- **Breaking:** rename `enable()` and `disable()` to `optIn()` and `optOut()` on `PosthogFlutterPlatformInterface`, `PosthogFlutterIO` and `PosthogFlutterWeb` +- Declare Android `minSdkVersion` 23 to match `posthog-android` diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8e9a6e5d..8b0b0f9c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: runner: ubuntu-latest - language: swift build-mode: manual - runner: macos-15-xlarge + runner: macos-26 - language: actions build-mode: none runner: ubuntu-latest @@ -45,7 +45,7 @@ jobs: if: matrix.language == 'swift' uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: - xcode-version: '16.4' + xcode-version: '26.4' - name: Install dependencies (Swift) if: matrix.language == 'swift' diff --git a/api/posthog_flutter.api.json b/api/posthog_flutter.api.json index 5c5fd73a..87102cdb 100644 --- a/api/posthog_flutter.api.json +++ b/api/posthog_flutter.api.json @@ -13,7 +13,7 @@ "packageApi": { "androidPlatformConstraints": { "compileSdkVersion": null, - "minSdkVersion": 21, + "minSdkVersion": 23, "targetSdkVersion": null }, "executableDeclarations": [ @@ -657,7 +657,7 @@ "isDeprecated": false, "isExperimental": false, "isStatic": false, - "name": "disable", + "name": "optOut", "parameters": [], "relativePath": "lib/src/posthog_flutter_platform_interface.dart", "returnTypeName": "Future", @@ -669,7 +669,7 @@ "isDeprecated": false, "isExperimental": false, "isStatic": false, - "name": "enable", + "name": "optIn", "parameters": [], "relativePath": "lib/src/posthog_flutter_platform_interface.dart", "returnTypeName": "Future", @@ -986,28 +986,6 @@ "type": "method", "typeParameterNames": [] }, - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "getFeatureFlagPayload", - "parameters": [ - { - "isDeprecated": false, - "isExperimental": false, - "isNamed": true, - "isRequired": true, - "name": "key", - "relativePath": "lib/src/posthog_flutter_platform_interface.dart", - "typeName": "String" - } - ], - "relativePath": "lib/src/posthog_flutter_platform_interface.dart", - "returnTypeName": "Future", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -1029,9 +1007,9 @@ "isExperimental": false, "isNamed": true, "isRequired": false, - "name": "sendEvent", + "name": "sendFeatureFlagEvent", "relativePath": "lib/src/posthog_flutter_platform_interface.dart", - "typeName": "bool" + "typeName": "bool?" } ], "relativePath": "lib/src/posthog_flutter_platform_interface.dart", @@ -1732,7 +1710,7 @@ "isDeprecated": false, "isExperimental": false, "isStatic": false, - "name": "disable", + "name": "optOut", "parameters": [], "relativePath": "lib/src/posthog_flutter_platform_interface.dart", "returnTypeName": "Future", @@ -1744,7 +1722,7 @@ "isDeprecated": false, "isExperimental": false, "isStatic": false, - "name": "enable", + "name": "optIn", "parameters": [], "relativePath": "lib/src/posthog_flutter_platform_interface.dart", "returnTypeName": "Future", @@ -2061,28 +2039,6 @@ "type": "method", "typeParameterNames": [] }, - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "getFeatureFlagPayload", - "parameters": [ - { - "isDeprecated": false, - "isExperimental": false, - "isNamed": true, - "isRequired": true, - "name": "key", - "relativePath": "lib/src/posthog_flutter_platform_interface.dart", - "typeName": "String" - } - ], - "relativePath": "lib/src/posthog_flutter_platform_interface.dart", - "returnTypeName": "Future", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -2104,9 +2060,9 @@ "isExperimental": false, "isNamed": true, "isRequired": false, - "name": "sendEvent", + "name": "sendFeatureFlagEvent", "relativePath": "lib/src/posthog_flutter_platform_interface.dart", - "typeName": "bool" + "typeName": "bool?" } ], "relativePath": "lib/src/posthog_flutter_platform_interface.dart", @@ -2322,18 +2278,6 @@ "posthog_flutter.dart" ], "executableDeclarations": [ - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "toMap", - "parameters": [], - "relativePath": "lib/src/posthog_config.dart", - "returnTypeName": "Map", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -2387,17 +2331,6 @@ "relativePath": "lib/src/posthog_config.dart", "typeName": "String" }, - { - "entryPoints": [], - "isDeprecated": true, - "isExperimental": false, - "isReadable": true, - "isStatic": false, - "isWriteable": false, - "name": "apiKey", - "relativePath": "lib/src/posthog_config.dart", - "typeName": "String" - }, { "entryPoints": [], "isDeprecated": false, @@ -2464,17 +2397,6 @@ "relativePath": "lib/src/posthog_config.dart", "typeName": "bool" }, - { - "entryPoints": [], - "isDeprecated": true, - "isExperimental": false, - "isReadable": true, - "isStatic": false, - "isWriteable": true, - "name": "sendFeatureFlagEvents", - "relativePath": "lib/src/posthog_config.dart", - "typeName": "bool" - }, { "entryPoints": [], "isDeprecated": false, @@ -2723,18 +2645,6 @@ "posthog_flutter.dart" ], "executableDeclarations": [ - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "toMap", - "parameters": [], - "relativePath": "lib/src/posthog_config.dart", - "returnTypeName": "Map", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -2925,18 +2835,6 @@ "posthog_flutter.dart" ], "executableDeclarations": [ - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "toMap", - "parameters": [], - "relativePath": "lib/src/posthog_config.dart", - "returnTypeName": "Map", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -3006,17 +2904,6 @@ "relativePath": "lib/src/posthog_config.dart", "typeName": "PostHogTextMask Function(String, Widget)?" }, - { - "entryPoints": [], - "isDeprecated": true, - "isExperimental": false, - "isReadable": false, - "isStatic": false, - "isWriteable": true, - "name": "debouncerDelay", - "relativePath": "lib/src/posthog_config.dart", - "typeName": "Duration" - }, { "entryPoints": [], "isDeprecated": false, @@ -3186,17 +3073,6 @@ "relativePath": "lib/src/posthog_config.dart", "typeName": "PostHogDataMode" }, - { - "entryPoints": [], - "isDeprecated": true, - "isExperimental": false, - "isReadable": true, - "isStatic": true, - "isWriteable": false, - "name": "cellular", - "relativePath": "lib/src/posthog_config.dart", - "typeName": "PostHogDataMode" - }, { "entryPoints": [], "isDeprecated": false, @@ -3237,18 +3113,6 @@ "posthog_flutter.dart" ], "executableDeclarations": [ - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "toMap", - "parameters": [], - "relativePath": "lib/src/posthog_config.dart", - "returnTypeName": "Map", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -3390,18 +3254,6 @@ "posthog_flutter.dart" ], "executableDeclarations": [ - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "toMap", - "parameters": [], - "relativePath": "lib/src/posthog_config.dart", - "returnTypeName": "Map", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -3455,18 +3307,6 @@ "posthog_flutter.dart" ], "executableDeclarations": [ - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "toMap", - "parameters": [], - "relativePath": "lib/src/posthog_config.dart", - "returnTypeName": "Map", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -3619,18 +3459,6 @@ "posthog_flutter.dart" ], "executableDeclarations": [ - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": false, - "name": "toMap", - "parameters": [], - "relativePath": "lib/src/posthog_config.dart", - "returnTypeName": "Map", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -3839,37 +3667,6 @@ "posthog_flutter.dart" ], "executableDeclarations": [ - { - "entryPoints": [], - "isDeprecated": false, - "isExperimental": false, - "isStatic": true, - "name": "fromMap", - "parameters": [ - { - "isDeprecated": false, - "isExperimental": false, - "isNamed": false, - "isRequired": true, - "name": "result", - "relativePath": "lib/src/feature_flag_result.dart", - "typeName": "Object?" - }, - { - "isDeprecated": false, - "isExperimental": false, - "isNamed": false, - "isRequired": true, - "name": "fallbackKey", - "relativePath": "lib/src/feature_flag_result.dart", - "typeName": "String" - } - ], - "relativePath": "lib/src/feature_flag_result.dart", - "returnTypeName": "PostHogFeatureFlagResult?", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -4599,30 +4396,6 @@ "type": "method", "typeParameterNames": [] }, - { - "entryPoints": [], - "isDeprecated": true, - "isExperimental": false, - "isStatic": false, - "name": "disable", - "parameters": [], - "relativePath": "lib/src/posthog.dart", - "returnTypeName": "Future", - "type": "method", - "typeParameterNames": [] - }, - { - "entryPoints": [], - "isDeprecated": true, - "isExperimental": false, - "isStatic": false, - "name": "enable", - "parameters": [], - "relativePath": "lib/src/posthog.dart", - "returnTypeName": "Future", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, @@ -5070,9 +4843,9 @@ "isExperimental": false, "isNamed": true, "isRequired": false, - "name": "sendEvent", + "name": "sendFeatureFlagEvent", "relativePath": "lib/src/posthog.dart", - "typeName": "bool" + "typeName": "bool?" } ], "relativePath": "lib/src/posthog.dart", @@ -5092,28 +4865,6 @@ "type": "method", "typeParameterNames": [] }, - { - "entryPoints": [], - "isDeprecated": true, - "isExperimental": false, - "isStatic": false, - "name": "getFeatureFlagPayload", - "parameters": [ - { - "isDeprecated": false, - "isExperimental": false, - "isNamed": false, - "isRequired": true, - "name": "key", - "relativePath": "lib/src/posthog.dart", - "typeName": "String" - } - ], - "relativePath": "lib/src/posthog.dart", - "returnTypeName": "Future", - "type": "method", - "typeParameterNames": [] - }, { "entryPoints": [], "isDeprecated": false, diff --git a/example/ios/Podfile b/example/ios/Podfile index 266a1cd0..e20ea2b7 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '14.0' +platform :ios, '15.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6356c2c8..70f8356f 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -33,6 +33,6 @@ SPEC CHECKSUMS: google_maps_flutter_ios: 17552876e72723da1d41accc22f03b5f7afbde69 GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d -PODFILE CHECKSUM: 3c864ae2f1db9f17d3418f22aa4b78eac050c10b +PODFILE CHECKSUM: 583e7c5826ead1663c2849ec27bee3d398ceebac COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index b6975b16..535d541f 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -478,7 +478,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -497,7 +497,7 @@ DEVELOPMENT_TEAM = PNC2XCH2XP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -518,7 +518,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.posthogFlutterExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -537,7 +537,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.posthogFlutterExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -554,7 +554,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.posthogFlutterExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -610,7 +610,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -659,7 +659,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -680,7 +680,7 @@ DEVELOPMENT_TEAM = PNC2XCH2XP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -704,7 +704,7 @@ DEVELOPMENT_TEAM = PNC2XCH2XP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/example/macos/Podfile b/example/macos/Podfile index b52666a1..dbccf89c 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 9d359e12..36151c7c 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -11,6 +11,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 -PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 +PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009 COCOAPODS: 1.16.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index f6c36aa8..c0dda7a1 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -549,7 +549,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -628,7 +628,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -675,7 +675,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/posthog_flutter/MIGRATION.md b/posthog_flutter/MIGRATION.md new file mode 100644 index 00000000..0ba88922 --- /dev/null +++ b/posthog_flutter/MIGRATION.md @@ -0,0 +1,224 @@ +# Migrating from posthog_flutter 5.x to 6.0 + +## Requirements + +| | 5.x | 6.0 | +| --- | --- | --- | +| iOS deployment target | 13.0 | 15.0 | +| macOS deployment target | 10.15 | 11.0 | +| Android `minSdkVersion` declared by the plugin | 21 | 23 | +| iOS and macOS dependency manager | CocoaPods or Swift Package Manager | Swift Package Manager only | +| Flutter for iOS and macOS builds | 3.27 | 3.41 | + +The Dart minimum (`>=3.6.0`) is unchanged. Android, web, Windows and Linux builds aren't affected by the Swift Package Manager requirement. + +### iOS and macOS + +Raise your app's deployment targets. A lower target fails the build. Set **Minimum Deployments** for the `Runner` target in Xcode (**Runner > General > Minimum Deployments**), which sets `IPHONEOS_DEPLOYMENT_TARGET` and `MACOSX_DEPLOYMENT_TARGET`. + +If you keep a `Podfile` for other plugins, raise its platform too: + +```ruby +# ios/Podfile +platform :ios, '15.0' + +# macos/Podfile +platform :osx, '11.0' +``` + +### Move from CocoaPods to Swift Package Manager + +posthog_flutter 6.0 has no podspec. On iOS and macOS, it installs only through Swift Package Manager. + +**If you ignore this, the build stops.** With Swift Package Manager turned off, `flutter build` exits with `Plugin posthog_flutter is only compatible with Swift Package Manager`. + +1. Use Flutter 3.41 or later. The plugin's `Package.swift` depends on the `FlutterFramework` package, which Flutter generates starting with 3.41. +2. Turn Swift Package Manager on if it's off. It's on by default from Flutter 3.44. On 3.41 to 3.43, or if you turned it off, run: + + ```sh + flutter config --enable-swift-package-manager + ``` + +3. Run your app. Flutter adds Swift Package Manager to the Xcode project on its own. + +Other plugins that only support CocoaPods keep working: Flutter installs them through your `Podfile` and installs posthog_flutter as a Swift package. Keep the `Podfile` in that case. + +If every plugin in your app supports Swift Package Manager, you can remove CocoaPods. This is optional. Follow [Flutter's steps](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers#how-to-remove-cocoapods-integration). + +**Add-to-app:** Flutter doesn't support Swift Package Manager in Flutter modules yet ([flutter/flutter#146957](https://github.com/flutter/flutter/issues/146957)), and its build stops for a plugin without a podspec. If you embed Flutter as a module in an iOS or macOS app, stay on posthog_flutter 5.x until Flutter adds support. + +### Android + +`posthog-android` has required `minSdkVersion` 23 since 3.39.0, so an app on 21 or 22 already could not build with this plugin. The plugin now declares the same value, and you have nothing to change. + +### Native SDKs + + + +The native dependency ranges are unchanged by this release: `posthog-ios` `>= 3.92.3, < 4.0.0` and `posthog-android` `[3.72.9, 4.0.0)`. + +## Breaking API changes + +These all fail at compile time unless noted otherwise. + +### `getFeatureFlagResult(sendEvent:)` is now `sendFeatureFlagEvent:` + +The parameter is also now `bool?`. See [the default change](#getfeatureflagresult-follows-your-config) below. + +```dart +// Before +await Posthog().getFeatureFlagResult('my-flag', sendEvent: false); + +// After +await Posthog().getFeatureFlagResult('my-flag', sendFeatureFlagEvent: false); +``` + +### `PostHogConfig.apiKey` is removed + +Read `projectToken` instead. The constructor already takes the token as its first argument. + +```dart +// Before +final token = config.apiKey; + +// After +final token = config.projectToken; +``` + +### `com.posthog.posthog.API_KEY` is removed + +Apps that start the SDK from `AndroidManifest.xml` or `Info.plist` must rename the key. **This does not fail at build time.** With only the old key, the SDK logs an error and does not start, so the app sends no data. + +```xml + + + + + +``` + +```xml + +com.posthog.posthog.API_KEY + + + +com.posthog.posthog.PROJECT_TOKEN + +``` + +### `Posthog().enable()` and `disable()` are removed + +```dart +// Before +await Posthog().disable(); +await Posthog().enable(); + +// After +await Posthog().optOut(); +await Posthog().optIn(); +``` + +### `PostHogConfig.sendFeatureFlagEvents` is removed + +```dart +// Before +config.sendFeatureFlagEvents = false; + +// After +config.sendFeatureFlagEvent = false; +``` + +### `Posthog().getFeatureFlagPayload()` is removed + +`getFeatureFlagPayload()` never captured `$feature_flag_called` on iOS, macOS, Android, Windows or Linux. Pass `sendFeatureFlagEvent: false` to keep it that way. Without it, the replacement call captures the event when your config allows it. + +```dart +// Before +final payload = await Posthog().getFeatureFlagPayload('my-flag'); + +// After +final result = await Posthog().getFeatureFlagResult( + 'my-flag', + sendFeatureFlagEvent: false, +); +final payload = result?.payload; +``` + +### `PostHogSessionReplayConfig.debouncerDelay` is removed + +```dart +// Before +config.sessionReplayConfig.debouncerDelay = const Duration(seconds: 2); + +// After +config.sessionReplayConfig.throttleDelay = const Duration(seconds: 2); +``` + +### `PostHogDataMode.cellular` is removed + +It always behaved like `any`, so sending does not change. + +```dart +// Before +config.dataMode = PostHogDataMode.cellular; + +// After +config.dataMode = PostHogDataMode.any; +``` + +### `toMap()` and `PostHogFeatureFlagResult.fromMap()` are removed + +They serialized the internal platform-channel format and have no replacement. `toMap()` is gone from `PostHogConfig`, `PostHogBootstrapConfig`, `PostHogLogsConfig`, `PostHogRageClickConfig`, `PostHogSessionReplayConfig`, `PostHogErrorTrackingConfig` and `PostHogExceptionStepsConfig`. To build a result, for example in a test, call the `PostHogFeatureFlagResult` constructor, which stays public. + +### Platform interface changes + +These affect only code that calls, extends or fakes `PosthogFlutterPlatformInterface`, `PosthogFlutterIO` or `PosthogFlutterWeb` directly, such as test mocks. `Posthog().optIn()` and `optOut()` are unaffected. + +- `enable()` and `disable()` are now `optIn()` and `optOut()`. +- `getFeatureFlagPayload()` is removed. +- `getFeatureFlagResult({required String key, bool sendEvent = true})` is now `getFeatureFlagResult({required String key, bool? sendFeatureFlagEvent})`. + +```dart +// Before +@override +Future disable() async {} + +// After +@override +Future optOut() async {} +``` + +## Behavior changes + +### `getFeatureFlagResult()` follows your config + +**This is a silent change with no compile error.** When you omit `sendFeatureFlagEvent`, `getFeatureFlagResult()` now uses `PostHogConfig.sendFeatureFlagEvent` instead of always capturing `$feature_flag_called`. This matches `getFeatureFlag()` and `isFeatureEnabled()`. + +It only affects apps that set `config.sendFeatureFlagEvent = false`. Those apps stop sending `$feature_flag_called` from `getFeatureFlagResult()`, so insights and experiments that rely on that event can receive less data. To keep the event for a call: + +```dart +await Posthog().getFeatureFlagResult('my-flag', sendFeatureFlagEvent: true); +``` + +On web, `PostHogConfig.sendFeatureFlagEvent` is not applied to posthog-js, so an omitted value keeps the posthog-js default, which captures the event. Pass `sendFeatureFlagEvent: false` to suppress it on web. + +## Upgrade checklist + +- [ ] Use Flutter 3.41 or later and make sure Swift Package Manager is on. +- [ ] Don't upgrade yet if you embed Flutter as an iOS or macOS add-to-app module. +- [ ] Raise iOS to 15.0 and macOS to 11.0 in your Xcode projects, and in any `Podfile` you keep. +- [ ] Rename `com.posthog.posthog.API_KEY` to `com.posthog.posthog.PROJECT_TOKEN` in `AndroidManifest.xml` and `Info.plist`. +- [ ] If you set `sendFeatureFlagEvent = false`, check whether anything relies on `$feature_flag_called` from `getFeatureFlagResult()`. +- [ ] Add `sendFeatureFlagEvent: false` wherever you replace `getFeatureFlagPayload()`. +- [ ] Fix the remaining compile errors using the sections above, including any test fakes of the platform classes. diff --git a/posthog_flutter/android/build.gradle b/posthog_flutter/android/build.gradle index fbff207c..2b32019b 100644 --- a/posthog_flutter/android/build.gradle +++ b/posthog_flutter/android/build.gradle @@ -58,7 +58,7 @@ android { } defaultConfig { - minSdkVersion 21 + minSdkVersion 23 } dependencies { diff --git a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index be04dccd..912e6182 100644 --- a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -187,21 +187,10 @@ class PosthogFlutterPlugin : return } - val projectToken = - ( - bundle.getString("com.posthog.posthog.PROJECT_TOKEN") - ?: bundle.getString("com.posthog.posthog.API_KEY") - )?.trim() - - if (!bundle.containsKey("com.posthog.posthog.PROJECT_TOKEN") && bundle.containsKey("com.posthog.posthog.API_KEY")) { - Log.w( - "PostHog", - "com.posthog.posthog.API_KEY is deprecated and will be removed in the next major version. Use com.posthog.posthog.PROJECT_TOKEN instead!", - ) - } + val projectToken = bundle.getString("com.posthog.posthog.PROJECT_TOKEN")?.trim() if (projectToken.isNullOrEmpty()) { - Log.e("PostHog", "Either com.posthog.posthog.PROJECT_TOKEN or com.posthog.posthog.API_KEY must be provided!") + Log.e("PostHog", "com.posthog.posthog.PROJECT_TOKEN must be provided!") return } @@ -226,7 +215,6 @@ class PosthogFlutterPlugin : val posthogConfig = mutableMapOf() posthogConfig["projectToken"] = projectToken - posthogConfig["apiKey"] = projectToken posthogConfig["host"] = host posthogConfig["captureApplicationLifecycleEvents"] = captureApplicationLifecycleEvents posthogConfig["debug"] = debug @@ -332,10 +320,6 @@ class PosthogFlutterPlugin : getFeatureFlag(call, result) } - "getFeatureFlagPayload" -> { - getFeatureFlagPayload(call, result) - } - "getFeatureFlagResult" -> { getFeatureFlagResult(call, result) } @@ -574,19 +558,9 @@ class PosthogFlutterPlugin : } private fun setupPostHog(posthogConfig: Map) { - val projectToken = - ( - (posthogConfig["projectToken"] as String?) - ?: (posthogConfig["apiKey"] as String?) - )?.trim() - if (!posthogConfig.containsKey("projectToken") && posthogConfig.containsKey("apiKey")) { - Log.w( - "PostHog", - "apiKey is deprecated and will be removed in the next major version. Use projectToken instead!", - ) - } + val projectToken = (posthogConfig["projectToken"] as String?)?.trim() if (projectToken.isNullOrEmpty()) { - Log.e("PostHog", "Either projectToken or apiKey must be provided!") + Log.e("PostHog", "projectToken must be provided!") return } @@ -1711,19 +1685,6 @@ class PosthogFlutterPlugin : } } - private fun getFeatureFlagPayload( - call: MethodCall, - result: Result, - ) { - try { - val featureFlagKey: String = call.argument("key")!! - val flag = PostHog.getFeatureFlagResult(featureFlagKey, sendFeatureFlagEvent = false)?.payload - result.success(flag) - } catch (e: Throwable) { - result.error("PosthogFlutterException", e.localizedMessage, null) - } - } - private fun getFeatureFlagResult( call: MethodCall, result: Result, @@ -1734,7 +1695,7 @@ class PosthogFlutterPlugin : result.error("PosthogFlutterException", "Missing argument: key", null) return } - val sendEvent: Boolean = call.argument("sendEvent") ?: true + val sendEvent: Boolean? = call.argument("sendEvent") val flagResult = PostHog.getFeatureFlagResult(featureFlagKey, sendEvent) if (flagResult != null) { diff --git a/posthog_flutter/darwin/posthog_flutter.podspec b/posthog_flutter/darwin/posthog_flutter.podspec index f99fd00d..9f0bba96 100644 --- a/posthog_flutter/darwin/posthog_flutter.podspec +++ b/posthog_flutter/darwin/posthog_flutter.podspec @@ -23,9 +23,8 @@ Postog flutter plugin s.dependency 'PostHog', '>= 3.92.3', '< 4.0.0' - s.ios.deployment_target = '13.0' - # PH iOS SDK 3.0.0 requires >= 10.15 - s.osx.deployment_target = '10.15' + s.ios.deployment_target = '15.0' + s.osx.deployment_target = '11.0' # Flutter.framework does not contain a i386 slice. s.ios.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/posthog_flutter/darwin/posthog_flutter/Package.swift b/posthog_flutter/darwin/posthog_flutter/Package.swift index c34f1946..e726f05d 100644 --- a/posthog_flutter/darwin/posthog_flutter/Package.swift +++ b/posthog_flutter/darwin/posthog_flutter/Package.swift @@ -6,8 +6,8 @@ import PackageDescription let package = Package( name: "posthog_flutter", platforms: [ - .iOS("13.0"), - .macOS("10.15"), + .iOS("15.0"), + .macOS("11.0"), ], products: [ .library(name: "posthog-flutter", targets: ["posthog_flutter"]), diff --git a/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift b/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift index 002e2433..3d32f8a3 100644 --- a/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift +++ b/posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift @@ -104,9 +104,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { /// a later `FlutterEngine` registers — that setup() has already run, so use the plist key there. private static func prewarmPushNotificationOpenCapture() { guard plistCapturePushNotificationOpened else { return } - if #available(iOS 14.0, macOS 11.0, *) { - PostHogSDK.prewarmPushNotificationOpenCapture() - } + PostHogSDK.prewarmPushNotificationOpenCapture() } private static var plistCapturePushNotificationOpened: Bool { @@ -120,17 +118,11 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { return } - let projectToken = ((Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.PROJECT_TOKEN") as? String) ?? (Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.API_KEY") as? String) ?? "") + let projectToken = (Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.PROJECT_TOKEN") as? String ?? "") .trimmingCharacters(in: .whitespacesAndNewlines) - if Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.PROJECT_TOKEN") == nil, - Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.API_KEY") != nil - { - print("[PostHog] com.posthog.posthog.API_KEY is deprecated and will be removed in the next major version. Use com.posthog.posthog.PROJECT_TOKEN instead!") - } - if projectToken.isEmpty { - print("[PostHog] Either com.posthog.posthog.PROJECT_TOKEN or com.posthog.posthog.API_KEY must be provided!") + print("[PostHog] com.posthog.posthog.PROJECT_TOKEN must be provided!") return } @@ -144,7 +136,6 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { setupPostHog([ "projectToken": projectToken, - "apiKey": projectToken, "host": host, "captureApplicationLifecycleEvents": captureApplicationLifecycleEvents, "debug": debug, @@ -179,13 +170,10 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { print("[PostHog] Plugin instance not found!") return } - let projectToken = ((posthogConfig["projectToken"] as? String) ?? (posthogConfig["apiKey"] as? String) ?? "") + let projectToken = (posthogConfig["projectToken"] as? String ?? "") .trimmingCharacters(in: .whitespacesAndNewlines) - if posthogConfig["projectToken"] == nil, posthogConfig["apiKey"] != nil { - print("[PostHog] apiKey is deprecated and will be removed in the next major version. Use projectToken instead!") - } if projectToken.isEmpty { - print("[PostHog] Either projectToken or apiKey must be provided!") + print("[PostHog] projectToken must be provided!") return } @@ -256,7 +244,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { switch dataMode { case "wifi": config.dataMode = .wifi - case "any", "cellular": + case "any": config.dataMode = .any default: break @@ -311,13 +299,11 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { } // configure surveys - if #available(iOS 15.0, *) { - let surveys: Bool = posthogConfig["surveys"] as? Bool ?? false - config.surveys = surveys - if surveys { - // if surveys are enabled, assign this instance as the survey delegate (we'll take over rendering) - config.surveysConfig.surveysDelegate = instance - } + let surveys: Bool = posthogConfig["surveys"] as? Bool ?? false + config.surveys = surveys + if surveys { + // if surveys are enabled, assign this instance as the survey delegate (we'll take over rendering) + config.surveysConfig.surveysDelegate = instance } #endif @@ -482,8 +468,6 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { getFeatureFlag(call, result: result) case "isFeatureEnabled": isFeatureEnabled(call, result: result) - case "getFeatureFlagPayload": - getFeatureFlagPayload(call, result: result) case "getFeatureFlagResult": getFeatureFlagResult(call, result: result) case "getAllFeatureFlags": @@ -1394,20 +1378,6 @@ extension PosthogFlutterPlugin { } } - private func getFeatureFlagPayload( - _ call: FlutterMethodCall, - result: @escaping FlutterResult - ) { - if let args = call.arguments as? [String: Any], - let featureFlagKey = args["key"] as? String - { - let value = PostHogSDK.shared.getFeatureFlagResult(featureFlagKey, sendFeatureFlagEvent: false)?.payload - result(value) - } else { - _badArgumentError(result) - } - } - private func getFeatureFlagResult( _ call: FlutterMethodCall, result: @escaping FlutterResult @@ -1415,8 +1385,13 @@ extension PosthogFlutterPlugin { if let args = call.arguments as? [String: Any], let featureFlagKey = args["key"] as? String { - let sendEvent = args["sendEvent"] as? Bool ?? true - let flagResult = PostHogSDK.shared.getFeatureFlagResult(featureFlagKey, sendFeatureFlagEvent: sendEvent) + // A missing sendEvent follows config.sendFeatureFlagEvent. + let flagResult: PostHogFeatureFlagResult? + if let sendEvent = args["sendEvent"] as? Bool { + flagResult = PostHogSDK.shared.getFeatureFlagResult(featureFlagKey, sendFeatureFlagEvent: sendEvent) + } else { + flagResult = PostHogSDK.shared.getFeatureFlagResult(featureFlagKey) + } if let flagResult { result([ diff --git a/posthog_flutter/lib/posthog_flutter_web.dart b/posthog_flutter/lib/posthog_flutter_web.dart index b8e2ba4a..045f88ca 100644 --- a/posthog_flutter/lib/posthog_flutter_web.dart +++ b/posthog_flutter/lib/posthog_flutter_web.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:posthog_flutter/src/error_tracking/dart_exception_processor.dart'; import 'package:posthog_flutter/src/util/logging.dart'; +import 'package:posthog_flutter/src/utils/channel_serialization.dart'; import 'package:posthog_flutter/src/utils/property_normalizer.dart'; import 'src/feature_flag_result.dart'; @@ -353,7 +354,8 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { () => Future.value(), (_) async { final result = flagVariant == null - ? await getFeatureFlagResult(key: flag, sendEvent: false) + ? await getFeatureFlagResult( + key: flag, sendFeatureFlagEvent: false) : null; final captured = featureEvent( event, flag, flagVariant ?? result?.variant ?? result?.enabled); @@ -377,12 +379,12 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { } @override - Future disable() async { + Future optOut() async { return _call(const MethodCall('disable')); } @override - Future enable() async { + Future optIn() async { return _call(const MethodCall('enable')); } @@ -492,24 +494,20 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { })); } - @override - Future getFeatureFlagPayload({required String key}) async { - return _call( - MethodCall('getFeatureFlagPayload', {'key': key}), - ); - } - @override Future getFeatureFlagResult({ required String key, - bool sendEvent = true, + bool? sendFeatureFlagEvent, }) async { final result = await _call( - MethodCall('getFeatureFlagResult', {'key': key, 'sendEvent': sendEvent}), + MethodCall('getFeatureFlagResult', { + 'key': key, + if (sendFeatureFlagEvent != null) 'sendEvent': sendFeatureFlagEvent, + }), ); // Web SDK returns: { key, enabled, variant, payload } - return PostHogFeatureFlagResult.fromMap(result, key); + return featureFlagResultFromMap(result, key); } @override @@ -517,7 +515,7 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { final results = await _call(const MethodCall('getAllFeatureFlags')); if (results is! List) return null; return results - .map((result) => PostHogFeatureFlagResult.fromMap(result, '')) + .map((result) => featureFlagResultFromMap(result, '')) .nonNulls .toList(); } diff --git a/posthog_flutter/lib/src/core/posthog_core.dart b/posthog_flutter/lib/src/core/posthog_core.dart index 43f7ab84..62f9b566 100644 --- a/posthog_flutter/lib/src/core/posthog_core.dart +++ b/posthog_flutter/lib/src/core/posthog_core.dart @@ -771,10 +771,11 @@ abstract class PostHogCore extends PostHogCoreStateless { } /// Gets the result for a specific feature flag, capturing - /// `$feature_flag_called` when [sendEvent] is true. + /// `$feature_flag_called` when [sendEvent] is true, or when it is null and + /// `sendFeatureFlagEvent` is enabled in the config. PostHogFeatureFlagResult? getFeatureFlagResult( String key, { - required bool sendEvent, + bool? sendEvent, }) { final storedDetails = _getStoredFlagDetails(); final details = storedDetails?.toResponse(); @@ -783,7 +784,7 @@ abstract class PostHogCore extends PostHogCoreStateless { true; final featureFlag = details?.flags[key]; final flagValue = getFeatureFlagValue(featureFlag); - final shouldSendEvent = sendEvent && + final shouldSendEvent = (sendEvent ?? _sendFeatureFlagEvent) && !optedOut && !(_flagCallReported[key]?.contains(flagValue) ?? false); diff --git a/posthog_flutter/lib/src/feature_flag_result.dart b/posthog_flutter/lib/src/feature_flag_result.dart index 9a85fef5..14298af1 100644 --- a/posthog_flutter/lib/src/feature_flag_result.dart +++ b/posthog_flutter/lib/src/feature_flag_result.dart @@ -53,23 +53,4 @@ class PostHogFeatureFlagResult { @override int get hashCode => Object.hash(key, enabled, variant); - - /// Creates a [PostHogFeatureFlagResult] from a native SDK response map. - /// - /// The [result] should be a map containing `key`, `enabled`, `variant`, and - /// `payload` entries. Falls back to [fallbackKey] if the map does not include - /// a key. - /// - /// Returns `null` if [result] is `null` or is not a [Map]. - static PostHogFeatureFlagResult? fromMap(Object? result, String fallbackKey) { - if (result == null) return null; - if (result is! Map) return null; - - return PostHogFeatureFlagResult( - key: result['key'] as String? ?? fallbackKey, - enabled: result['enabled'] as bool? ?? false, - variant: result['variant'] as String?, - payload: result['payload'], - ); - } } diff --git a/posthog_flutter/lib/src/posthog.dart b/posthog_flutter/lib/src/posthog.dart index e16f36d1..7465914b 100644 --- a/posthog_flutter/lib/src/posthog.dart +++ b/posthog_flutter/lib/src/posthog.dart @@ -409,7 +409,7 @@ class Posthog { // Native opt-out stops replay itself. A native stopSessionRecording() here // would record that the app stopped replay, so native opt-in would not // restart it. - await _posthog.disable(); + await _posthog.optOut(); } /// Opts the current user back in to data collection. @@ -424,7 +424,7 @@ class Posthog { _installFlutterIntegrations(config); } - await _posthog.enable(); + await _posthog.optIn(); final restartReplay = _restartSessionReplayOnEnable; _restartSessionReplayOnEnable = false; if (restartReplay) { @@ -433,18 +433,6 @@ class Posthog { } } - /// Opts the current user out of data collection. - @Deprecated( - 'Use optOut() instead. This will be removed in the next major version.', - ) - Future disable() => optOut(); - - /// Opts the current user back in to data collection. - @Deprecated( - 'Use optIn() instead. This will be removed in the next major version.', - ) - Future enable() => optIn(); - /// Returns whether the current user has opted out of data collection. Future isOptOut() => _posthog.isOptOut(); @@ -797,9 +785,10 @@ class Posthog { /// This is the canonical method for getting feature flag data. /// Returns `null` if the flag does not exist or cannot be loaded. /// - /// Set [sendEvent] to `false` to suppress the `$feature_flag_called` event. - /// This is useful when you only need the payload and do not want to emit the - /// event. + /// [sendFeatureFlagEvent] controls whether this call captures the + /// `$feature_flag_called` event. Defaults to `null`, which follows + /// [PostHogConfig.sendFeatureFlagEvent]. Pass `false` when you only need the + /// payload, or `true` to capture the event even when the config turns it off. /// /// **Example:** /// ```dart @@ -811,9 +800,10 @@ class Posthog { /// ``` Future getFeatureFlagResult( String key, { - bool sendEvent = true, + bool? sendFeatureFlagEvent, }) => - _posthog.getFeatureFlagResult(key: key, sendEvent: sendEvent); + _posthog.getFeatureFlagResult( + key: key, sendFeatureFlagEvent: sendFeatureFlagEvent); /// Returns the results of all currently loaded feature flags. /// @@ -838,16 +828,6 @@ class Posthog { Future?> getAllFeatureFlags() => _posthog.getAllFeatureFlags(); - /// Returns the payload for the feature flag [key]. - /// - /// Returns `null` when the flag does not exist, has no payload, or the payload - /// cannot be loaded. - @Deprecated( - 'Use getFeatureFlagResult instead, which returns both value and payload.', - ) - Future getFeatureFlagPayload(String key) => - _posthog.getFeatureFlagPayload(key: key); - /// Captures a `$feature_view` event when the user sees the feature behind /// the feature flag [flag]. /// diff --git a/posthog_flutter/lib/src/posthog_config.dart b/posthog_flutter/lib/src/posthog_config.dart index de3bae99..221d5e0a 100644 --- a/posthog_flutter/lib/src/posthog_config.dart +++ b/posthog_flutter/lib/src/posthog_config.dart @@ -7,7 +7,6 @@ import 'posthog.dart'; import 'posthog_event.dart'; import 'posthog_flutter_platform_interface.dart'; import 'replay/mask/posthog_text_mask.dart'; -import 'util/logging.dart'; /// Callback to intercept and modify events before they are sent to PostHog. /// @@ -72,17 +71,6 @@ enum PostHogDataMode { /// Send data only on Wi-Fi connections. wifi, - /// Send data on any available connection, same as [any]. - /// - /// Despite its name, this never restricted sending to cellular connections. - /// It will be removed in the next major version. - /// - /// ```dart - /// config.dataMode = PostHogDataMode.any; - /// ``` - @Deprecated('Behaves like `any`. Use `PostHogDataMode.any` instead.') - cellular, - /// Send data on any available connection. any, } @@ -98,16 +86,8 @@ class PostHogConfig { /// /// You can find it at: /// https://us.posthog.com/settings/project-details#variables - /// - /// This field was formerly named [apiKey]. final String projectToken; - /// Deprecated alias for [projectToken]. - @Deprecated( - 'Deprecated in favor of [projectToken]. This will be removed in the next major version.', - ) - String get apiKey => projectToken; - String _host = _defaultHost; /// The PostHog ingestion host. @@ -143,17 +123,6 @@ class PostHogConfig { /// Defaults to `true`. var sendFeatureFlagEvent = true; - /// Deprecated alias for [sendFeatureFlagEvent]. - @Deprecated( - 'Use sendFeatureFlagEvent instead. This will be removed in the next major version.', - ) - bool get sendFeatureFlagEvents => sendFeatureFlagEvent; - - @Deprecated( - 'Use sendFeatureFlagEvent instead. This will be removed in the next major version.', - ) - set sendFeatureFlagEvents(bool value) => sendFeatureFlagEvent = value; - /// Whether feature flags are loaded when the SDK starts. /// /// Defaults to `true`, which means every SDK start issues a `/flags` request. @@ -462,44 +431,6 @@ class PostHogConfig { final trimmedHost = host.trim(); return trimmedHost.isEmpty ? _defaultHost : trimmedHost; } - - /// Converts this configuration to a platform-channel map. - /// - /// Returns the values consumed by the Android, Apple, and web implementations. - Map toMap() { - return { - 'projectToken': projectToken, - 'apiKey': projectToken, - 'host': host, - 'flushAt': flushAt, - 'maxQueueSize': maxQueueSize, - 'maxBatchSize': maxBatchSize, - 'flushInterval': flushInterval.inSeconds, - 'sendFeatureFlagEvents': sendFeatureFlagEvent, - 'preloadFeatureFlags': preloadFeatureFlags, - if (evaluationContexts != null) 'evaluationContexts': evaluationContexts, - 'captureApplicationLifecycleEvents': captureApplicationLifecycleEvents, - 'rageClickConfig': rageClickConfig.toMap(), - 'debug': debug, - 'optOut': optOut, - 'surveys': surveys, - 'personProfiles': personProfiles.name, - 'compression': compression.name, - 'sessionReplay': sessionReplay, - 'dataMode': dataMode.name, - 'sessionReplayConfig': sessionReplayConfig.toMap(), - 'errorTrackingConfig': errorTrackingConfig.toMap(), - 'logs': logsConfig.toMap(), - 'capturePushNotificationSubscriptions': - capturePushNotificationSubscriptions, - 'capturePushNotificationOpened': capturePushNotificationOpened, - // A closure can't cross the channel. This tells native whether to install - // a bridging provider at all — installing one the host didn't ask for - // would change how the native SDK handles a 401 on the subscription call. - 'pushIdentityProviderEnabled': pushIdentityProvider != null, - if (bootstrap != null) 'bootstrap': bootstrap!.toMap(), - }; - } } /// Pre-seeded identity and feature-flag state applied on the very first SDK @@ -556,34 +487,6 @@ class PostHogBootstrapConfig { /// JSON payloads paired with [featureFlags], keyed by flag key. Each value is /// the already-decoded payload (map, list, string, number, `null`, ...). final Map? featureFlagPayloads; - - /// Converts this configuration to a platform-channel map. - /// - /// Only the dimensions that were set are included; [isIdentifiedId] is always - /// sent so the native SDK doesn't have to infer it. - Map toMap() { - final flags = featureFlags; - if (flags != null) { - for (final entry in flags.entries) { - // Only bool/String are served (see [featureFlags]); the native SDKs drop - // anything else silently, so warn instead of leaving no trace. - if (entry.value is! bool && entry.value is! String) { - printIfDebug( - '[PostHog] bootstrap featureFlags["${entry.key}"] is ' - '${entry.value.runtimeType}; only bool and String values are served, ' - 'so this entry will be ignored.', - ); - } - } - } - return { - if (distinctId != null) 'distinctId': distinctId, - 'isIdentifiedId': isIdentifiedId, - if (featureFlags != null) 'featureFlags': featureFlags, - if (featureFlagPayloads != null) - 'featureFlagPayloads': featureFlagPayloads, - }; - } } /// Configuration for the logs subsystem. @@ -688,36 +591,6 @@ class PostHogLogsConfig { /// record. Keep anything sensitive out of those fields; put it in [body] or /// [PostHogLogRecord.attributes], which a callback can scrub or drop. List beforeSend = []; - - /// Converts the identity and tuning options to a platform-channel map. - /// - /// Only fields the user set are included, so unset fields keep the native - /// default. [beforeSend] is intentionally omitted: it runs in Dart and never - /// crosses the platform channel. - Map toMap() { - return { - if (serviceName != null) 'serviceName': serviceName, - if (serviceVersion != null) 'serviceVersion': serviceVersion, - if (environment != null) 'environment': environment, - if (resourceAttributes.isNotEmpty) - 'resourceAttributes': resourceAttributes, - if (flushInterval != null) - 'flushIntervalSeconds': _wholeSeconds(flushInterval!), - if (flushAt != null) 'flushAt': flushAt, - if (maxBatchSize != null) 'maxBatchSize': maxBatchSize, - if (maxBufferSize != null) 'maxBufferSize': maxBufferSize, - if (rateCapMaxLogs != null) 'rateCapMaxLogs': rateCapMaxLogs, - if (rateCapWindow != null) - 'rateCapWindowSeconds': _wholeSeconds(rateCapWindow!), - }; - } - - /// The native flush interval and rate-cap window are whole seconds. A - /// sub-second [Duration] truncates to `0`, which the native SDK treats as - /// "disabled" (rate cap) or continuous flushing — surprising for a caller who - /// set, say, 500ms. Floor at 1s, the smallest value the native API can honor. - static int _wholeSeconds(Duration duration) => - duration.inSeconds < 1 ? 1 : duration.inSeconds; } /// Configuration for rage-click autocapture on iOS and Mac Catalyst. @@ -747,17 +620,6 @@ class PostHogRageClickConfig { /// /// Defaults to `3`. var minimumTapCount = 3; - - /// Converts this rage-click configuration to a platform-channel map. - Map toMap() { - return { - 'enabled': enabled, - 'thresholdPoints': thresholdPoints, - 'timeoutInterval': - timeoutInterval.inMicroseconds / Duration.microsecondsPerSecond, - 'minimumTapCount': minimumTapCount, - }; - } } /// Pixel format for Android native-screen captures in session replay. @@ -907,17 +769,6 @@ class PostHogSessionReplayConfig { /// Default: null. PostHogTextMaskPolicy? textMaskPolicy; - /// Deprecated setter that forwards assigned values to [throttleDelay]. - /// - /// Debouncer delay used to reduce the number of snapshots captured and reduce - /// performance impact. This is used for capturing the view as a screenshot. - /// The lower the number, the more snapshots will be captured but higher the - /// performance impact. Defaults to 1s. - @Deprecated('Deprecated in favor of [throttleDelay] from v4.8.0.') - set debouncerDelay(Duration debouncerDelay) { - throttleDelay = debouncerDelay; - } - /// Throttling delay used to reduce the number of snapshots captured and reduce /// performance impact. /// @@ -1050,26 +901,6 @@ class PostHogSessionReplayConfig { /// /// Default: false. var verifyScreenshotMaskAlignment = false; - - /// Converts this session replay configuration to a platform-channel map. - /// - /// Returns values consumed by the Android and Apple session replay - /// implementations. - Map toMap() { - return { - 'captureTouches': captureTouches, - 'maskAllImages': maskAllImages, - 'maskAllTexts': maskAllTexts, - 'throttleDelayMs': throttleDelay.inMilliseconds, - 'maskAllPlatformViews': maskAllPlatformViews, - 'captureNativeScreens': captureNativeScreens, - 'verifyScreenshotMaskAlignment': verifyScreenshotMaskAlignment, - 'screenshotScale': screenshotScale, - 'screenshotCompressionQuality': screenshotCompressionQuality, - 'screenshotColorMode': screenshotColorMode.name, - if (sampleRate != null) 'sampleRate': sampleRate, - }; - } } /// Configuration for PostHog error tracking and exception capture. @@ -1248,25 +1079,6 @@ class PostHogErrorTrackingConfig { /// /// Record steps with `Posthog().addExceptionStep()`. final exceptionSteps = PostHogExceptionStepsConfig(); - - /// Converts this error tracking configuration to a platform-channel map. - /// - /// Returns values consumed by the Android, Apple, and Dart exception capture - /// implementations. - Map toMap() { - return { - 'inAppIncludes': inAppIncludes, - 'inAppExcludes': inAppExcludes, - 'inAppByDefault': inAppByDefault, - 'captureFlutterErrors': captureFlutterErrors, - 'captureSilentFlutterErrors': captureSilentFlutterErrors, - 'capturePlatformDispatcherErrors': capturePlatformDispatcherErrors, - 'captureNativeExceptions': captureNativeExceptions, - 'captureNativeCrashes': captureNativeCrashes, - 'captureIsolateErrors': captureIsolateErrors, - 'exceptionSteps': exceptionSteps.toMap(), - }; - } } /// Configuration for exception steps. @@ -1304,12 +1116,4 @@ class PostHogExceptionStepsConfig { /// until the total fits. A single step larger than the budget is rejected /// outright. Defaults to `32768` (32 KiB). var maxBytes = 32768; - - /// Converts this configuration to a platform-channel map. - Map toMap() { - return { - 'enabled': enabled, - 'maxBytes': maxBytes, - }; - } } diff --git a/posthog_flutter/lib/src/posthog_flutter_desktop.dart b/posthog_flutter/lib/src/posthog_flutter_desktop.dart index b69255ee..cf883e54 100644 --- a/posthog_flutter/lib/src/posthog_flutter_desktop.dart +++ b/posthog_flutter/lib/src/posthog_flutter_desktop.dart @@ -411,13 +411,13 @@ class PosthogFlutterDesktop extends PosthogFlutterPlatformInterface { Future reset() => _guard('reset', (client) => client.reset()); @override - Future disable() => _guard('disable', (client) { + Future optOut() => _guard('optOut', (client) { client.optOut(); _appLifecycle?.stop(); }); @override - Future enable() => _guard('enable', (client) { + Future optIn() => _guard('optIn', (client) { client.optIn(); // Opting in is not an app open: an active app is reported as opened // on its next activation. @@ -509,19 +509,16 @@ class PosthogFlutterDesktop extends PosthogFlutterPlatformInterface { (client) => client.getFeatureFlag(key, sendEvent: sendFeatureFlagEvent)); - @override - Future getFeatureFlagPayload({required String key}) => _guardWith( - 'getFeatureFlagPayload', - null, - (client) => client.getFeatureFlagResult(key, sendEvent: false)?.payload); - @override Future getFeatureFlagResult({ required String key, - bool sendEvent = true, + bool? sendFeatureFlagEvent, }) => - _guardWith('getFeatureFlagResult', null, - (client) => client.getFeatureFlagResult(key, sendEvent: sendEvent)); + _guardWith( + 'getFeatureFlagResult', + null, + (client) => client.getFeatureFlagResult(key, + sendEvent: sendFeatureFlagEvent)); @override Future?> getAllFeatureFlags() => _guardWith( diff --git a/posthog_flutter/lib/src/posthog_flutter_io.dart b/posthog_flutter/lib/src/posthog_flutter_io.dart index f5d53517..75905040 100644 --- a/posthog_flutter/lib/src/posthog_flutter_io.dart +++ b/posthog_flutter/lib/src/posthog_flutter_io.dart @@ -13,6 +13,7 @@ import 'error_tracking/dart_exception_processor.dart'; import 'utils/before_send.dart'; import 'utils/call_order.dart'; import 'utils/capture_utils.dart'; +import 'utils/channel_serialization.dart'; import 'utils/flutter_version.dart'; import 'utils/property_normalizer.dart'; @@ -533,7 +534,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { } @override - Future disable() async { + Future optOut() async { if (!isSupportedPlatform()) { return; } @@ -546,7 +547,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { } @override - Future enable() async { + Future optIn() async { if (!isSupportedPlatform()) { return; } @@ -733,26 +734,10 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { } } - @override - Future getFeatureFlagPayload({required String key}) async { - if (!isSupportedPlatform()) { - return null; - } - - try { - return await _invoke('getFeatureFlagPayload', { - 'key': key, - }); - } on PlatformException catch (exception) { - printIfDebug('Exeption on getFeatureFlagPayload: $exception'); - return null; - } - } - @override Future getFeatureFlagResult({ required String key, - bool sendEvent = true, + bool? sendFeatureFlagEvent, }) async { if (!isSupportedPlatform()) { return null; @@ -761,11 +746,11 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { try { final result = await _invoke('getFeatureFlagResult', { 'key': key, - 'sendEvent': sendEvent, + if (sendFeatureFlagEvent != null) 'sendEvent': sendFeatureFlagEvent, }); // Native returns: { key, enabled, variant, payload } - return PostHogFeatureFlagResult.fromMap(result, key); + return featureFlagResultFromMap(result, key); } on PlatformException catch (exception) { printIfDebug('Exception on getFeatureFlagResult: $exception'); return null; @@ -781,7 +766,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { try { final results = await _invoke('getAllFeatureFlags') as List?; return results - ?.map((result) => PostHogFeatureFlagResult.fromMap(result, '')) + ?.map((result) => featureFlagResultFromMap(result, '')) .nonNulls .toList(); } on PlatformException catch (exception) { diff --git a/posthog_flutter/lib/src/posthog_flutter_platform_interface.dart b/posthog_flutter/lib/src/posthog_flutter_platform_interface.dart index 565e8011..a775d5f7 100644 --- a/posthog_flutter/lib/src/posthog_flutter_platform_interface.dart +++ b/posthog_flutter/lib/src/posthog_flutter_platform_interface.dart @@ -145,12 +145,12 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { throw UnimplementedError('reset() has not been implemented.'); } - Future disable() { - throw UnimplementedError('disable() has not been implemented.'); + Future optOut() { + throw UnimplementedError('optOut() has not been implemented.'); } - Future enable() { - throw UnimplementedError('enable() has not been implemented.'); + Future optIn() { + throw UnimplementedError('optIn() has not been implemented.'); } Future isOptOut() { @@ -223,15 +223,9 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { throw UnimplementedError('getFeatureFlag() has not been implemented.'); } - Future getFeatureFlagPayload({required String key}) { - throw UnimplementedError( - 'getFeatureFlagPayload() has not been implemented.', - ); - } - Future getFeatureFlagResult({ required String key, - bool sendEvent = true, + bool? sendFeatureFlagEvent, }) { throw UnimplementedError( 'getFeatureFlagResult() has not been implemented.', diff --git a/posthog_flutter/lib/src/posthog_flutter_web_handler.dart b/posthog_flutter/lib/src/posthog_flutter_web_handler.dart index 139976d8..d35dd08d 100644 --- a/posthog_flutter/lib/src/posthog_flutter_web_handler.dart +++ b/posthog_flutter/lib/src/posthog_flutter_web_handler.dart @@ -57,7 +57,6 @@ extension PostHogExtension on PostHog { // ignore: non_constant_identifier_names external bool has_opted_out_capturing(); external JSAny? getFeatureFlag(JSAny key, [JSAny? options]); - external JSAny? getFeatureFlagPayload(JSAny key); external JSAny? getFeatureFlagResult(JSAny key, [JSAny? options]); // Added in posthog-js 1.395.0; the call site guards with try/catch. external JSAny? getAllFeatureFlags(); @@ -378,18 +377,13 @@ Future handleWebMethodCall(MethodCall call) async { final featureFlag = posthog?.getFeatureFlag(stringToJSAny(key), _flagOptions(args)); return featureFlag?.dartify(); - case 'getFeatureFlagPayload': - final key = args['key'] as String; - - final featureFlag = posthog?.getFeatureFlagPayload(stringToJSAny(key)); - return featureFlag?.dartify(); case 'getFeatureFlagResult': final key = args['key'] as String; - final sendEvent = args['sendEvent'] as bool? ?? true; + final sendEvent = args['sendEvent'] as bool?; final result = posthog?.getFeatureFlagResult( stringToJSAny(key), - {'send_event': sendEvent}.jsify(), + {if (sendEvent != null) 'send_event': sendEvent}.jsify(), ); return result?.dartify(); case 'getAllFeatureFlags': diff --git a/posthog_flutter/lib/src/utils/channel_serialization.dart b/posthog_flutter/lib/src/utils/channel_serialization.dart new file mode 100644 index 00000000..8fc7f2ec --- /dev/null +++ b/posthog_flutter/lib/src/utils/channel_serialization.dart @@ -0,0 +1,172 @@ +import '../feature_flag_result.dart'; +import '../posthog_config.dart'; +import '../util/logging.dart'; + +extension PostHogConfigSerialization on PostHogConfig { + Map toMap() { + return { + 'projectToken': projectToken, + 'host': host, + 'flushAt': flushAt, + 'maxQueueSize': maxQueueSize, + 'maxBatchSize': maxBatchSize, + 'flushInterval': flushInterval.inSeconds, + 'sendFeatureFlagEvents': sendFeatureFlagEvent, + 'preloadFeatureFlags': preloadFeatureFlags, + if (evaluationContexts != null) 'evaluationContexts': evaluationContexts, + 'captureApplicationLifecycleEvents': captureApplicationLifecycleEvents, + 'rageClickConfig': rageClickConfig.toMap(), + 'debug': debug, + 'optOut': optOut, + 'surveys': surveys, + 'personProfiles': personProfiles.name, + 'compression': compression.name, + 'sessionReplay': sessionReplay, + 'dataMode': dataMode.name, + 'sessionReplayConfig': sessionReplayConfig.toMap(), + 'errorTrackingConfig': errorTrackingConfig.toMap(), + 'logs': logsConfig.toMap(), + 'capturePushNotificationSubscriptions': + capturePushNotificationSubscriptions, + 'capturePushNotificationOpened': capturePushNotificationOpened, + // A closure can't cross the channel. This tells native whether to install + // a bridging provider at all — installing one the host didn't ask for + // would change how the native SDK handles a 401 on the subscription call. + 'pushIdentityProviderEnabled': pushIdentityProvider != null, + if (bootstrap != null) 'bootstrap': bootstrap!.toMap(), + }; + } +} + +extension PostHogBootstrapConfigSerialization on PostHogBootstrapConfig { + /// Only the dimensions that were set are included; [isIdentifiedId] is always + /// sent so the native SDK doesn't have to infer it. + Map toMap() { + final flags = featureFlags; + if (flags != null) { + for (final entry in flags.entries) { + // Only bool/String are served (see [featureFlags]); the native SDKs drop + // anything else silently, so warn instead of leaving no trace. + if (entry.value is! bool && entry.value is! String) { + printIfDebug( + '[PostHog] bootstrap featureFlags["${entry.key}"] is ' + '${entry.value.runtimeType}; only bool and String values are served, ' + 'so this entry will be ignored.', + ); + } + } + } + return { + if (distinctId != null) 'distinctId': distinctId, + 'isIdentifiedId': isIdentifiedId, + if (featureFlags != null) 'featureFlags': featureFlags, + if (featureFlagPayloads != null) + 'featureFlagPayloads': featureFlagPayloads, + }; + } +} + +extension PostHogLogsConfigSerialization on PostHogLogsConfig { + /// Only fields the user set are included, so unset fields keep the native + /// default. [beforeSend] is intentionally omitted: it runs in Dart and never + /// crosses the platform channel. + Map toMap() { + return { + if (serviceName != null) 'serviceName': serviceName, + if (serviceVersion != null) 'serviceVersion': serviceVersion, + if (environment != null) 'environment': environment, + if (resourceAttributes.isNotEmpty) + 'resourceAttributes': resourceAttributes, + if (flushInterval != null) + 'flushIntervalSeconds': _wholeSeconds(flushInterval!), + if (flushAt != null) 'flushAt': flushAt, + if (maxBatchSize != null) 'maxBatchSize': maxBatchSize, + if (maxBufferSize != null) 'maxBufferSize': maxBufferSize, + if (rateCapMaxLogs != null) 'rateCapMaxLogs': rateCapMaxLogs, + if (rateCapWindow != null) + 'rateCapWindowSeconds': _wholeSeconds(rateCapWindow!), + }; + } + + /// The native flush interval and rate-cap window are whole seconds. A + /// sub-second [Duration] truncates to `0`, which the native SDK treats as + /// "disabled" (rate cap) or continuous flushing — surprising for a caller who + /// set, say, 500ms. Floor at 1s, the smallest value the native API can honor. + static int _wholeSeconds(Duration duration) => + duration.inSeconds < 1 ? 1 : duration.inSeconds; +} + +extension PostHogRageClickConfigSerialization on PostHogRageClickConfig { + Map toMap() { + return { + 'enabled': enabled, + 'thresholdPoints': thresholdPoints, + 'timeoutInterval': + timeoutInterval.inMicroseconds / Duration.microsecondsPerSecond, + 'minimumTapCount': minimumTapCount, + }; + } +} + +extension PostHogSessionReplayConfigSerialization + on PostHogSessionReplayConfig { + Map toMap() { + return { + 'captureTouches': captureTouches, + 'maskAllImages': maskAllImages, + 'maskAllTexts': maskAllTexts, + 'throttleDelayMs': throttleDelay.inMilliseconds, + 'maskAllPlatformViews': maskAllPlatformViews, + 'captureNativeScreens': captureNativeScreens, + 'verifyScreenshotMaskAlignment': verifyScreenshotMaskAlignment, + 'screenshotScale': screenshotScale, + 'screenshotCompressionQuality': screenshotCompressionQuality, + 'screenshotColorMode': screenshotColorMode.name, + if (sampleRate != null) 'sampleRate': sampleRate, + }; + } +} + +extension PostHogErrorTrackingConfigSerialization + on PostHogErrorTrackingConfig { + Map toMap() { + return { + 'inAppIncludes': inAppIncludes, + 'inAppExcludes': inAppExcludes, + 'inAppByDefault': inAppByDefault, + 'captureFlutterErrors': captureFlutterErrors, + 'captureSilentFlutterErrors': captureSilentFlutterErrors, + 'capturePlatformDispatcherErrors': capturePlatformDispatcherErrors, + 'captureNativeExceptions': captureNativeExceptions, + 'captureNativeCrashes': captureNativeCrashes, + 'captureIsolateErrors': captureIsolateErrors, + 'exceptionSteps': exceptionSteps.toMap(), + }; + } +} + +extension PostHogExceptionStepsConfigSerialization + on PostHogExceptionStepsConfig { + Map toMap() { + return { + 'enabled': enabled, + 'maxBytes': maxBytes, + }; + } +} + +/// Returns `null` if [result] is not a [Map]. Falls back to [fallbackKey] when +/// the map has no `key`. +PostHogFeatureFlagResult? featureFlagResultFromMap( + Object? result, + String fallbackKey, +) { + if (result is! Map) return null; + + return PostHogFeatureFlagResult( + key: result['key'] as String? ?? fallbackKey, + enabled: result['enabled'] as bool? ?? false, + variant: result['variant'] as String?, + payload: result['payload'], + ); +} diff --git a/posthog_flutter/test/posthog_desktop_transport_test.dart b/posthog_flutter/test/posthog_desktop_transport_test.dart index 2c8dbfbd..c36cfae2 100644 --- a/posthog_flutter/test/posthog_desktop_transport_test.dart +++ b/posthog_flutter/test/posthog_desktop_transport_test.dart @@ -58,10 +58,10 @@ void main() { ); await IOOverrides.runZoned(() async { await Posthog().setup(config()..preloadFeatureFlags = false); - await Posthog().disable(); + await Posthog().optOut(); expect(await Posthog().isOptOut(), isTrue); await Posthog().capture(eventName: 'blocked event'); - await Posthog().enable(); + await Posthog().optIn(); await Posthog().capture(eventName: 'memory event'); await Posthog().flush(); expect(server.eventNames, ['memory event']); @@ -118,10 +118,10 @@ void main() { expect(personProperties[r'$device_type'], 'Desktop'); expect(personProperties[r'$app_version'], '1.2.3'); - await Posthog().disable(); + await Posthog().optOut(); expect(await Posthog().isOptOut(), isTrue); await Posthog().capture(eventName: 'blocked event'); - await Posthog().enable(); + await Posthog().optIn(); await Posthog().capture(eventName: 'allowed event'); await server.waitForEvent('allowed event'); diff --git a/posthog_flutter/test/posthog_flutter_dart_test.dart b/posthog_flutter/test/posthog_flutter_dart_test.dart index 4f7a8698..1883d664 100644 --- a/posthog_flutter/test/posthog_flutter_dart_test.dart +++ b/posthog_flutter/test/posthog_flutter_dart_test.dart @@ -78,7 +78,7 @@ void main() { PosthogFlutterDart.registerWith(); var platform = PosthogFlutterPlatformInterface.instance; await platform.setup(configFor(server)); - await platform.disable(); + await platform.optOut(); await platform.close(); PosthogFlutterDart.registerWith(); diff --git a/posthog_flutter/test/posthog_flutter_desktop_test.dart b/posthog_flutter/test/posthog_flutter_desktop_test.dart index 9f404c1f..36517dd8 100644 --- a/posthog_flutter/test/posthog_flutter_desktop_test.dart +++ b/posthog_flutter/test/posthog_flutter_desktop_test.dart @@ -89,8 +89,6 @@ void main() { final nullReadsByName = Function(PosthogFlutterDesktop)>{ 'getFeatureFlag': (platform) => platform.getFeatureFlag(key: 'flag'), - 'getFeatureFlagPayload': (platform) => - platform.getFeatureFlagPayload(key: 'flag'), 'getFeatureFlagResult': (platform) => platform.getFeatureFlagResult(key: 'flag'), 'getAllFeatureFlags': (platform) => platform.getAllFeatureFlags(), @@ -133,7 +131,7 @@ void main() { await platform.setup(config); await platform.capture(eventName: 'disabled event'); await platform.identify(userId: 'disabled user'); - await platform.enable(); + await platform.optIn(); await platform.reloadFeatureFlags(); await platform.flush(); expect(await platform.getFeatureFlag(key: 'flag'), isNull); @@ -247,9 +245,9 @@ void main() { ]; final platform = await setUpPlatform(config); - await platform.disable(); + await platform.optOut(); final blocked = platform.capture(eventName: 'blocked capture'); - await platform.enable(); + await platform.optIn(); releaseHook.complete(); await blocked; await platform.capture(eventName: 'sentinel event'); @@ -345,9 +343,9 @@ void main() { ]; final platform = await setUpPlatform(config); - await platform.disable(); + await platform.optOut(); await platform.screen(screenName: 'Hidden'); - await platform.enable(); + await platform.optIn(); await platform.capture(eventName: 'sentinel event'); final event = await server.waitForEvent('sentinel event'); @@ -377,10 +375,10 @@ void main() { final screen = platform.screen(screenName: 'Blocked'); await hookStarted.future; - await platform.disable(); + await platform.optOut(); releaseHook.complete(); await screen; - await platform.enable(); + await platform.optIn(); await platform.capture(eventName: 'sentinel event'); final event = await server.waitForEvent('sentinel event'); @@ -677,9 +675,9 @@ void main() { ]; final platform = await setUpPlatform(config); - await platform.disable(); + await platform.optOut(); final blocked = platform.captureException(error: StateError('blocked')); - await platform.enable(); + await platform.optIn(); releaseHook.complete(); await blocked; await platform.capture(eventName: 'sentinel event'); @@ -918,9 +916,9 @@ void main() { final server = await LocalPostHogServer.start(); final platform = await setUpPlatform(configFor(server)); - await platform.disable(); + await platform.optOut(); await platform.addExceptionStep('while opted out'); - await platform.enable(); + await platform.optIn(); await platform.captureException(error: StateError('opted in')); final event = await server.waitForEvent(r'$exception'); @@ -1053,7 +1051,7 @@ void main() { final platform = launch(createAppDirectory()); await platform.setup(configFor(server)..optOut = true); - await platform.enable(); + await platform.optIn(); await server.waitForEvent('Application Installed'); }); @@ -1092,7 +1090,7 @@ void main() { final firstLaunch = createPlatform(appDirectory: appDirectory); await firstLaunch.setup(configFor(server)); final distinctId = await firstLaunch.getDistinctId(); - await firstLaunch.disable(); + await firstLaunch.optOut(); await firstLaunch.close(); final nextLaunch = createPlatform(appDirectory: appDirectory); @@ -1133,12 +1131,12 @@ void main() { final platform = createPlatform(resolveAppDirectory: () => release.future); final setup = platform.setup(configFor(server)); - final disable = platform.disable(); + final disable = platform.optOut(); final capture = platform.capture(eventName: 'disabled event'); release.complete(directory.path); await Future.wait([setup, disable, capture]); expect(await platform.isOptOut(), isTrue); - await platform.enable(); + await platform.optIn(); await platform.capture(eventName: 'enabled event'); await server.waitForEvent('enabled event'); expect(server.eventNames, ['enabled event']); @@ -1149,7 +1147,7 @@ void main() { final directory = createAppDirectory(); final previousLaunch = createPlatform(appDirectory: directory); await previousLaunch.setup(configFor(server)); - await previousLaunch.disable(); + await previousLaunch.optOut(); await previousLaunch.close(); final release = Completer(); @@ -1160,7 +1158,7 @@ void main() { release.complete(directory.path); await Future.wait([setup, capture]); expect(await platform.isOptOut(), isTrue); - await platform.enable(); + await platform.optIn(); await platform.capture(eventName: 'enabled event'); await server.waitForEvent('enabled event'); expect(server.eventNames, ['enabled event']); @@ -1273,7 +1271,7 @@ void main() { final platform = createPlatform(); final setup = platform.setup(configFor(server)); - final disable = platform.disable(); + final disable = platform.optOut(); await Future.wait([setup, disable]); expect(await platform.isOptOut(), isTrue); @@ -1647,7 +1645,11 @@ void main() { expect(await platform.getFeatureFlag(key: 'beta'), isTrue); expect(await platform.getFeatureFlag(key: 'checkout'), 'variant-b'); expect( - await platform.getFeatureFlagPayload(key: 'beta'), + (await platform.getFeatureFlagResult( + key: 'beta', + sendFeatureFlagEvent: false, + )) + ?.payload, {'color': 'blue'}, ); expect(server.flagsRequests, isEmpty); @@ -1719,7 +1721,8 @@ void main() { ); }); - test(r'getFeatureFlagPayload does not capture $feature_flag_called', + test( + r'getFeatureFlagResult with sendFeatureFlagEvent false does not capture $feature_flag_called', () async { final server = await LocalPostHogServer.start(); server.respond = (_) => PostHogResponse.json(_variantFlagResponse()); @@ -1731,10 +1734,11 @@ void main() { final platform = await setUpPlatform(config); await flagsLoaded.future; - final payload = await platform.getFeatureFlagPayload( + final silent = await platform.getFeatureFlagResult( key: 'variant-flag', + sendFeatureFlagEvent: false, ); - expect(payload, {'color': 'blue'}); + expect(silent?.payload, {'color': 'blue'}); await platform.capture(eventName: 'sentinel event'); await server.waitForEvent('sentinel event'); @@ -1887,6 +1891,33 @@ void main() { expect(server.eventNames, isNot(contains(r'$feature_flag_called'))); }); + test( + r'getFeatureFlagResult follows config.sendFeatureFlagEvent unless overridden', + () async { + final server = await LocalPostHogServer.start(); + server.respond = (_) => PostHogResponse.json(_variantFlagResponse()); + final flagsLoaded = Completer(); + final config = configFor(server) + ..sendFeatureFlagEvent = false + ..onFeatureFlags = () { + if (!flagsLoaded.isCompleted) flagsLoaded.complete(); + }; + final platform = await setUpPlatform(config); + await flagsLoaded.future; + + expect( + (await platform.getFeatureFlagResult(key: 'variant-flag'))?.variant, + 'test-variant'); + await platform.capture(eventName: 'sentinel event'); + await server.waitForEvent('sentinel event'); + expect(server.eventNames, isNot(contains(r'$feature_flag_called'))); + + await platform.getFeatureFlagResult( + key: 'variant-flag', sendFeatureFlagEvent: true); + final called = await server.waitForEvent(r'$feature_flag_called'); + expect(_propertiesOf(called)[r'$feature_flag'], 'variant-flag'); + }); + test('onFeatureFlags reports the flags cached by the previous launch', () async { final server = await LocalPostHogServer.start(); @@ -1904,7 +1935,10 @@ void main() { await platform.setup(configFor(server) ..preloadFeatureFlags = false ..onFeatureFlags = () { - payloads.add(platform.getFeatureFlagPayload(key: 'variant-flag')); + payloads.add(platform + .getFeatureFlagResult( + key: 'variant-flag', sendFeatureFlagEvent: false) + .then((result) => result?.payload)); if (!announced.isCompleted) announced.complete(); }); await announced.future; diff --git a/posthog_flutter/test/posthog_flutter_io_test.dart b/posthog_flutter/test/posthog_flutter_io_test.dart index 9d0810ae..6bbf65eb 100644 --- a/posthog_flutter/test/posthog_flutter_io_test.dart +++ b/posthog_flutter/test/posthog_flutter_io_test.dart @@ -68,7 +68,7 @@ void main() { }; }); final result = await posthogFlutterIO.getFeatureFlagResult( - key: 'flag', sendEvent: false); + key: 'flag', sendFeatureFlagEvent: false); expect(result, isNotNull); expect(result!.key, 'flag'); expect(result.enabled, enabled); @@ -80,7 +80,7 @@ void main() { expect( await posthogFlutterIO.getFeatureFlagResult(key: 'missing'), isNull); expect(log.single.method, 'getFeatureFlagResult'); - expect(log.single.arguments, {'key': 'missing', 'sendEvent': true}); + expect(log.single.arguments, {'key': 'missing'}); }); }); @@ -216,14 +216,14 @@ isolate_dso_base: 7000000000 }, ); - test('setup sends projectToken and deprecated apiKey alias', () async { + test('setup sends projectToken', () async { testConfig = PostHogConfig(' \n test_project_token\t '); await posthogFlutterIO.setup(testConfig); final call = log.firstWhere((c) => c.method == 'setup'); final args = Map.from(call.arguments as Map); expect(args['projectToken'], equals('test_project_token')); - expect(args['apiKey'], equals('test_project_token')); + expect(args.containsKey('apiKey'), isFalse); }); test( diff --git a/posthog_flutter/test/posthog_flutter_platform_interface_fake.dart b/posthog_flutter/test/posthog_flutter_platform_interface_fake.dart index 64083a7e..b71a0bfe 100644 --- a/posthog_flutter/test/posthog_flutter_platform_interface_fake.dart +++ b/posthog_flutter/test/posthog_flutter_platform_interface_fake.dart @@ -95,7 +95,6 @@ class PosthogFlutterPlatformFake extends PosthogFlutterPlatformInterface { // Feature flag test data final Map featureFlagValues = {}; - final Map featureFlagPayloads = {}; final List> flagReadCalls = []; @@ -231,10 +230,10 @@ class PosthogFlutterPlatformFake extends PosthogFlutterPlatformInterface { } @override - Future disable() async {} + Future optOut() async {} @override - Future enable() async {} + Future optIn() async {} @override Future close() async { @@ -294,19 +293,15 @@ class PosthogFlutterPlatformFake extends PosthogFlutterPlatformInterface { return featureFlagValues[key] == true; } - @override - Future getFeatureFlagPayload({required String key}) async { - return featureFlagPayloads[key]; - } - PostHogFeatureFlagResult? featureFlagResult; @override Future getFeatureFlagResult({ required String key, - bool sendEvent = true, + bool? sendFeatureFlagEvent, }) async { - getFeatureFlagResultCalls.add({'key': key, 'sendEvent': sendEvent}); + getFeatureFlagResultCalls + .add({'key': key, 'sendFeatureFlagEvent': sendFeatureFlagEvent}); return featureFlagResult; } diff --git a/posthog_flutter/test/posthog_logs_test.dart b/posthog_flutter/test/posthog_logs_test.dart index 8d0d46f3..624822a8 100644 --- a/posthog_flutter/test/posthog_logs_test.dart +++ b/posthog_flutter/test/posthog_logs_test.dart @@ -1,6 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart'; +import 'package:posthog_flutter/src/utils/channel_serialization.dart'; import 'posthog_flutter_platform_interface_fake.dart'; diff --git a/posthog_flutter/test/posthog_test.dart b/posthog_flutter/test/posthog_test.dart index 727d2f39..a3d9f7ad 100644 --- a/posthog_flutter/test/posthog_test.dart +++ b/posthog_flutter/test/posthog_test.dart @@ -8,6 +8,7 @@ import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart'; import 'package:posthog_flutter/src/posthog_internal_events.dart'; import 'package:posthog_flutter/src/replay/mask/posthog_mask_controller.dart'; import 'package:posthog_flutter/src/replay/screenshot/screenshot_capturer.dart'; +import 'package:posthog_flutter/src/utils/channel_serialization.dart'; import 'posthog_flutter_platform_interface_fake.dart'; @@ -214,16 +215,6 @@ void main() { }); group('PostHogConfig', () { - test('sendFeatureFlagEvents forwards to sendFeatureFlagEvent', () { - final config = PostHogConfig('test_project_token'); - - // ignore: deprecated_member_use_from_same_package - config.sendFeatureFlagEvents = false; - - expect(config.sendFeatureFlagEvent, isFalse); - expect(config.toMap()['sendFeatureFlagEvents'], isFalse); - }); - test('toMap forwards evaluationContexts only when set', () { final config = PostHogConfig('test_project_token'); expect(config.toMap().containsKey('evaluationContexts'), isFalse); @@ -237,10 +228,8 @@ void main() { config.host = ' \nhttps://eu.i.posthog.com/\t '; expect(config.projectToken, equals('test_project_token')); - expect(config.apiKey, equals('test_project_token')); expect(config.host, equals('https://eu.i.posthog.com/')); expect(config.toMap()['projectToken'], equals('test_project_token')); - expect(config.toMap()['apiKey'], equals('test_project_token')); expect(config.toMap()['host'], equals('https://eu.i.posthog.com/')); }); @@ -491,7 +480,7 @@ void main() { final result = await Posthog().getFeatureFlagResult('non-existent'); expect(result, isNull); expect(fakePlatformInterface.getFeatureFlagResultCalls, [ - {'key': 'non-existent', 'sendEvent': true} + {'key': 'non-existent', 'sendFeatureFlagEvent': null} ]); }); @@ -506,24 +495,26 @@ void main() { expect( await Posthog().getFeatureFlagResult('variant-flag'), same(expected)); expect(fakePlatformInterface.getFeatureFlagResultCalls, [ - {'key': 'variant-flag', 'sendEvent': true} + {'key': 'variant-flag', 'sendFeatureFlagEvent': null} ]); }); - test('passes sendEvent=true by default', () async { + test('leaves sendFeatureFlagEvent to the config by default', () async { await Posthog().getFeatureFlagResult('test'); expect( - fakePlatformInterface.getFeatureFlagResultCalls.last['sendEvent'], - isTrue, + fakePlatformInterface + .getFeatureFlagResultCalls.last['sendFeatureFlagEvent'], + isNull, ); }); - test('passes sendEvent=false when specified', () async { - await Posthog().getFeatureFlagResult('test', sendEvent: false); + test('passes sendFeatureFlagEvent=false when specified', () async { + await Posthog().getFeatureFlagResult('test', sendFeatureFlagEvent: false); expect( - fakePlatformInterface.getFeatureFlagResultCalls.last['sendEvent'], + fakePlatformInterface + .getFeatureFlagResultCalls.last['sendFeatureFlagEvent'], isFalse, ); }); @@ -627,12 +618,12 @@ void main() { }); test('fromMap returns null for null input', () { - final result = PostHogFeatureFlagResult.fromMap(null, 'fallback'); + final result = featureFlagResultFromMap(null, 'fallback'); expect(result, isNull); }); test('fromMap returns null for non-Map input', () { - final result = PostHogFeatureFlagResult.fromMap('not a map', 'fallback'); + final result = featureFlagResultFromMap('not a map', 'fallback'); expect(result, isNull); }); @@ -644,7 +635,7 @@ void main() { 'payload': {'data': 123}, }; - final result = PostHogFeatureFlagResult.fromMap(map, 'fallback'); + final result = featureFlagResultFromMap(map, 'fallback'); expect(result, isNotNull); expect(result!.key, equals('my-flag')); @@ -656,7 +647,7 @@ void main() { test('fromMap uses fallback key when map key is null', () { final map = {'enabled': true, 'variant': null, 'payload': null}; - final result = PostHogFeatureFlagResult.fromMap(map, 'fallback-key'); + final result = featureFlagResultFromMap(map, 'fallback-key'); expect(result, isNotNull); expect(result!.key, equals('fallback-key')); @@ -665,7 +656,7 @@ void main() { test('fromMap defaults enabled to false when not in map', () { final map = {'key': 'my-flag'}; - final result = PostHogFeatureFlagResult.fromMap(map, 'fallback'); + final result = featureFlagResultFromMap(map, 'fallback'); expect(result, isNotNull); expect(result!.enabled, isFalse); diff --git a/posthog_flutter/test/replay_capture_touches_test.dart b/posthog_flutter/test/replay_capture_touches_test.dart index 485c5cb2..1a4f9d1d 100644 --- a/posthog_flutter/test/replay_capture_touches_test.dart +++ b/posthog_flutter/test/replay_capture_touches_test.dart @@ -4,6 +4,7 @@ import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/posthog_flutter_io.dart'; import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart'; import 'package:posthog_flutter/src/posthog_internal_events.dart'; +import 'package:posthog_flutter/src/utils/channel_serialization.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/posthog_flutter/test/replay_screenshot_config_test.dart b/posthog_flutter/test/replay_screenshot_config_test.dart index edc745a2..d562fd4f 100644 --- a/posthog_flutter/test/replay_screenshot_config_test.dart +++ b/posthog_flutter/test/replay_screenshot_config_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:posthog_flutter/posthog_flutter.dart'; +import 'package:posthog_flutter/src/utils/channel_serialization.dart'; void main() { test('screenshot controls preserve native defaults', () { diff --git a/posthog_flutter/test/snapshots/setup_request.json b/posthog_flutter/test/snapshots/setup_request.json index 58543998..367138b4 100644 --- a/posthog_flutter/test/snapshots/setup_request.json +++ b/posthog_flutter/test/snapshots/setup_request.json @@ -3,7 +3,6 @@ "method": "setup", "arguments": { "projectToken": "snapshot_project_token", - "apiKey": "snapshot_project_token", "host": "https://eu.i.posthog.com", "flushAt": 12, "maxQueueSize": 800, diff --git a/posthog_flutter/test/web_before_send_test.dart b/posthog_flutter/test/web_before_send_test.dart index 1bb87206..2debc618 100644 --- a/posthog_flutter/test/web_before_send_test.dart +++ b/posthog_flutter/test/web_before_send_test.dart @@ -122,8 +122,8 @@ void main() { 'register': (web) => web.register('plan', 'pro'), 'unregister': (web) => web.unregister('plan'), 'group': (web) => web.group(groupType: 'company', groupKey: 'acme'), - 'opt_in_capturing': (web) => web.enable(), - 'opt_out_capturing': (web) => web.disable(), + 'opt_in_capturing': (web) => web.optIn(), + 'opt_out_capturing': (web) => web.optOut(), }; for (final MapEntry(key: method, value: call) in followUps.entries) { test('$method waits for events whose beforeSend is still running',