From 3b0991c5bc9f0713a6f0ee4f7bd63bfc91bf8edf Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Thu, 5 Feb 2026 12:37:06 -0500 Subject: [PATCH 1/8] feat: Add `getFeatureFlagResult` API --- .../posthog/flutter/PosthogFlutterPlugin.kt | 30 ++++++ ios/Classes/PosthogFlutterPlugin.swift | 27 +++++ lib/posthog_flutter.dart | 1 + lib/posthog_flutter_web.dart | 13 +++ lib/src/feature_flag_result.dart | 100 ++++++++++++++++++ lib/src/posthog.dart | 29 +++++ lib/src/posthog_flutter_io.dart | 24 +++++ .../posthog_flutter_platform_interface.dart | 9 ++ lib/src/posthog_flutter_web_handler.dart | 10 ++ 9 files changed, 243 insertions(+) create mode 100644 lib/src/feature_flag_result.dart diff --git a/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index fc5685c8..3006aa9c 100644 --- a/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -146,6 +146,10 @@ class PosthogFlutterPlugin : getFeatureFlagPayload(call, result) } + "getFeatureFlagResult" -> { + getFeatureFlagResult(call, result) + } + "register" -> { register(call, result) } @@ -367,6 +371,32 @@ class PosthogFlutterPlugin : } } + private fun getFeatureFlagResult( + call: MethodCall, + result: Result, + ) { + try { + val featureFlagKey: String = call.argument("key")!! + val sendEvent: Boolean = call.argument("sendEvent") ?: true + val flagResult = PostHog.getFeatureFlagResult(featureFlagKey, sendEvent) + + if (flagResult != null) { + result.success( + mapOf( + "key" to flagResult.key, + "enabled" to flagResult.enabled, + "variant" to flagResult.variant, + "payload" to flagResult.payload, + ), + ) + } else { + result.success(null) + } + } catch (e: Throwable) { + result.error("PosthogFlutterException", e.localizedMessage, null) + } + } + private fun identify( call: MethodCall, result: Result, diff --git a/ios/Classes/PosthogFlutterPlugin.swift b/ios/Classes/PosthogFlutterPlugin.swift index 918edbbd..ac42ada1 100644 --- a/ios/Classes/PosthogFlutterPlugin.swift +++ b/ios/Classes/PosthogFlutterPlugin.swift @@ -183,6 +183,8 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { isFeatureEnabled(call, result: result) case "getFeatureFlagPayload": getFeatureFlagPayload(call, result: result) + case "getFeatureFlagResult": + getFeatureFlagResult(call, result: result) case "identify": identify(call, result: result) case "capture": @@ -531,6 +533,31 @@ extension PosthogFlutterPlugin { } } + private func getFeatureFlagResult( + _ call: FlutterMethodCall, + result: @escaping FlutterResult + ) { + 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) + + if let flagResult { + result([ + "key": flagResult.key, + "enabled": flagResult.enabled, + "variant": flagResult.variant as Any, + "payload": flagResult.payload as Any + ]) + } else { + result(nil) + } + } else { + _badArgumentError(result) + } + } + private func identify( _ call: FlutterMethodCall, result: @escaping FlutterResult diff --git a/lib/posthog_flutter.dart b/lib/posthog_flutter.dart index dafab8b3..8b700ba9 100644 --- a/lib/posthog_flutter.dart +++ b/lib/posthog_flutter.dart @@ -1,5 +1,6 @@ library posthog_flutter; +export 'src/feature_flag_result.dart'; export 'src/posthog.dart'; export 'src/posthog_config.dart'; export 'src/posthog_event.dart'; diff --git a/lib/posthog_flutter_web.dart b/lib/posthog_flutter_web.dart index 99849151..09cd7fcb 100644 --- a/lib/posthog_flutter_web.dart +++ b/lib/posthog_flutter_web.dart @@ -9,6 +9,7 @@ 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/property_normalizer.dart'; +import 'src/feature_flag_result.dart'; import 'src/posthog_config.dart'; import 'src/posthog_flutter_platform_interface.dart'; import 'src/posthog_flutter_web_handler.dart'; @@ -229,6 +230,18 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { MethodCall('getFeatureFlagPayload', {'key': key})); } + @override + Future getFeatureFlagResult({ + required String key, + bool sendEvent = true, + }) async { + final result = await handleWebMethodCall(MethodCall( + 'getFeatureFlagResult', {'key': key, 'sendEvent': sendEvent})); + + // Web SDK returns: { key, enabled, variant, payload } + return PostHogFeatureFlagResult.fromMap(result, key); + } + @override Future flush() async { return handleWebMethodCall(const MethodCall('flush')); diff --git a/lib/src/feature_flag_result.dart b/lib/src/feature_flag_result.dart new file mode 100644 index 00000000..eea44d30 --- /dev/null +++ b/lib/src/feature_flag_result.dart @@ -0,0 +1,100 @@ +/// Represents the result of a feature flag evaluation. +/// +/// Contains the flag key, whether it's enabled, the variant (for multivariate flags), +/// and any associated payload. +class PostHogFeatureFlagResult { + /// The feature flag key. + final String key; + + /// Whether the flag is enabled. + /// + /// For boolean flags, this is the flag value. + /// For multivariate flags, this is true when the flag evaluates to any variant. + final bool enabled; + + /// The variant key for multivariate flags, or null for boolean flags. + final String? variant; + + /// The JSON payload associated with the flag, if any. + final Object? payload; + + const PostHogFeatureFlagResult({ + required this.key, + required this.enabled, + this.variant, + this.payload, + }); + + /// Creates a [PostHogFeatureFlagResult] from a raw flag value and payload. + /// + /// The [flagValue] can be: + /// - `null` or `false`: Flag is disabled + /// - `true`: Boolean flag is enabled + /// - `String`: Multivariate flag with the given variant + factory PostHogFeatureFlagResult.fromValueAndPayload( + String key, + Object? flagValue, + Object? payload, + ) { + if (flagValue == null || flagValue == false) { + return PostHogFeatureFlagResult( + key: key, + enabled: false, + variant: null, + payload: payload, + ); + } + + if (flagValue == true) { + return PostHogFeatureFlagResult( + key: key, + enabled: true, + variant: null, + payload: payload, + ); + } + + // Multivariate flag - value is the variant string + return PostHogFeatureFlagResult( + key: key, + enabled: true, + variant: flagValue.toString(), + payload: payload, + ); + } + + @override + String toString() { + return 'PostHogFeatureFlagResult(key: $key, enabled: $enabled, variant: $variant, payload: $payload)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PostHogFeatureFlagResult && + other.key == key && + other.enabled == enabled && + other.variant == variant && + other.payload == payload; + } + + @override + int get hashCode => Object.hash(key, enabled, variant, payload); + + /// Creates a [PostHogFeatureFlagResult] from a native SDK response map. + /// + /// The [map] should contain: key, enabled, variant, payload. + /// Falls back to [fallbackKey] if the map doesn't include a key. + /// Returns null if [result] is null or 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/lib/src/posthog.dart b/lib/src/posthog.dart index 6ac98230..bdb05de8 100644 --- a/lib/src/posthog.dart +++ b/lib/src/posthog.dart @@ -2,6 +2,7 @@ import 'package:meta/meta.dart'; import 'package:posthog_flutter/src/error_tracking/posthog_error_tracking_autocapture_integration.dart'; import 'package:posthog_flutter/src/error_tracking/posthog_exception.dart'; +import 'feature_flag_result.dart'; import 'posthog_config.dart'; import 'posthog_flutter_platform_interface.dart'; import 'posthog_observer.dart'; @@ -158,9 +159,37 @@ class Posthog { groupProperties: groupProperties, ); + /// Returns the feature flag value for the given key. + /// + /// Returns `null` if the flag doesn't exist. + /// For boolean flags, returns `true` or `false`. + /// For multivariate flags, returns the variant string. Future getFeatureFlag(String key) => _posthog.getFeatureFlag(key: key); + /// Returns the full feature flag result including value and payload. + /// + /// This is the canonical method for getting feature flag data. + /// Returns `null` if the flag doesn't exist. + /// + /// Set [sendEvent] to `false` to suppress the `$feature_flag_called` event. + /// This is useful when you only need the payload and don't want to emit the event. + /// + /// **Example:** + /// ```dart + /// final result = await Posthog().getFeatureFlagResult('my-flag'); + /// if (result != null && result.enabled) { + /// final variant = result.variant; // For multivariate flags + /// final payload = result.payload; // Associated payload data + /// } + /// ``` + Future getFeatureFlagResult(String key, + {bool sendEvent = true}) => + _posthog.getFeatureFlagResult(key: key, sendEvent: sendEvent); + + /// Returns the payload for a feature flag. + @Deprecated( + 'Use getFeatureFlagResult instead, which returns both value and payload.') Future getFeatureFlagPayload(String key) => _posthog.getFeatureFlagPayload(key: key); diff --git a/lib/src/posthog_flutter_io.dart b/lib/src/posthog_flutter_io.dart index c2187e88..12f090c7 100644 --- a/lib/src/posthog_flutter_io.dart +++ b/lib/src/posthog_flutter_io.dart @@ -13,6 +13,7 @@ import 'error_tracking/dart_exception_processor.dart'; import 'utils/capture_utils.dart'; import 'utils/property_normalizer.dart'; +import 'feature_flag_result.dart'; import 'posthog_config.dart'; import 'posthog_constants.dart'; import 'posthog_event.dart'; @@ -525,6 +526,29 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { } } + @override + Future getFeatureFlagResult({ + required String key, + bool sendEvent = true, + }) async { + if (!isSupportedPlatform()) { + return null; + } + + try { + final result = await _methodChannel.invokeMethod('getFeatureFlagResult', { + 'key': key, + 'sendEvent': sendEvent, + }); + + // Native returns: { key, enabled, variant, payload } + return PostHogFeatureFlagResult.fromMap(result, key); + } on PlatformException catch (exception) { + printIfDebug('Exception on getFeatureFlagResult: $exception'); + return null; + } + } + @override Future register(String key, Object value) async { if (!isSupportedPlatform()) { diff --git a/lib/src/posthog_flutter_platform_interface.dart b/lib/src/posthog_flutter_platform_interface.dart index 3a5223f3..841d6d33 100644 --- a/lib/src/posthog_flutter_platform_interface.dart +++ b/lib/src/posthog_flutter_platform_interface.dart @@ -1,5 +1,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'feature_flag_result.dart'; import 'posthog_config.dart'; import 'posthog_flutter_io.dart'; @@ -133,6 +134,14 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { 'getFeatureFlagPayload() has not been implemented.'); } + Future getFeatureFlagResult({ + required String key, + bool sendEvent = true, + }) { + throw UnimplementedError( + 'getFeatureFlagResult() has not been implemented.'); + } + Future flush() { throw UnimplementedError('flush() has not been implemented.'); } diff --git a/lib/src/posthog_flutter_web_handler.dart b/lib/src/posthog_flutter_web_handler.dart index e7a0d4b4..1259aa98 100644 --- a/lib/src/posthog_flutter_web_handler.dart +++ b/lib/src/posthog_flutter_web_handler.dart @@ -30,6 +30,7 @@ extension PostHogExtension on PostHog { external bool has_opted_out_capturing(); external JSAny? getFeatureFlag(JSAny key); external JSAny? getFeatureFlagPayload(JSAny key); + external JSAny? getFeatureFlagResult(JSAny key, [JSAny? options]); external void register(JSAny properties); external void unregister(JSAny key); // ignore: non_constant_identifier_names @@ -444,6 +445,15 @@ Future handleWebMethodCall(MethodCall call) async { stringToJSAny(key), ); return featureFlag?.dartify(); + case 'getFeatureFlagResult': + final key = args['key'] as String; + final sendEvent = args['sendEvent'] as bool? ?? true; + + final result = posthog?.getFeatureFlagResult( + stringToJSAny(key), + {'send_event': sendEvent}.jsify(), + ); + return result?.dartify(); case 'register': final key = args['key'] as String; final value = args['value']; From 1696980ec923c8b51630951df6b7f2e515ab5a6e Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Thu, 5 Feb 2026 12:38:41 -0500 Subject: [PATCH 2/8] test: getFeatureFlagResult --- ...sthog_flutter_platform_interface_fake.dart | 33 +++ test/posthog_test.dart | 240 ++++++++++++++++++ 2 files changed, 273 insertions(+) diff --git a/test/posthog_flutter_platform_interface_fake.dart b/test/posthog_flutter_platform_interface_fake.dart index 0545d375..38535a3b 100644 --- a/test/posthog_flutter_platform_interface_fake.dart +++ b/test/posthog_flutter_platform_interface_fake.dart @@ -1,3 +1,4 @@ +import 'package:posthog_flutter/src/feature_flag_result.dart'; import 'package:posthog_flutter/src/posthog_config.dart'; import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart'; @@ -20,6 +21,13 @@ class PosthogFlutterPlatformFake extends PosthogFlutterPlatformInterface { final List capturedExceptions = []; PostHogConfig? receivedConfig; + // Feature flag test data + final Map featureFlagValues = {}; + final Map featureFlagPayloads = {}; + + // Call tracking for getFeatureFlagResult + final List> getFeatureFlagResultCalls = []; + @override Future screen({ required String screenName, @@ -48,4 +56,29 @@ class PosthogFlutterPlatformFake extends PosthogFlutterPlatformInterface { properties: properties, )); } + + @override + Future getFeatureFlag({required String key}) async { + return featureFlagValues[key]; + } + + @override + Future getFeatureFlagPayload({required String key}) async { + return featureFlagPayloads[key]; + } + + @override + Future getFeatureFlagResult({ + required String key, + bool sendEvent = true, + }) async { + getFeatureFlagResultCalls.add({'key': key, 'sendEvent': sendEvent}); + + if (!featureFlagValues.containsKey(key)) { + return null; + } + final value = featureFlagValues[key]; + final payload = featureFlagPayloads[key]; + return PostHogFeatureFlagResult.fromValueAndPayload(key, value, payload); + } } diff --git a/test/posthog_test.dart b/test/posthog_test.dart index 07ac9e8d..91a56453 100644 --- a/test/posthog_test.dart +++ b/test/posthog_test.dart @@ -32,4 +32,244 @@ void main() { equals(testCallback)); }); }); + + group('getFeatureFlagResult', () { + late PosthogFlutterPlatformFake fakePlatformInterface; + + setUp(() { + fakePlatformInterface = PosthogFlutterPlatformFake(); + PosthogFlutterPlatformInterface.instance = fakePlatformInterface; + }); + + test('returns null for non-existent flag', () async { + final result = await Posthog().getFeatureFlagResult('non-existent'); + expect(result, isNull); + }); + + test('returns correct result for boolean flag (true)', () async { + fakePlatformInterface.featureFlagValues['bool-flag'] = true; + + final result = await Posthog().getFeatureFlagResult('bool-flag'); + + expect(result, isNotNull); + expect(result!.key, equals('bool-flag')); + expect(result.enabled, isTrue); + expect(result.variant, isNull); + expect(result.payload, isNull); + }); + + test('returns correct result for boolean flag (false)', () async { + fakePlatformInterface.featureFlagValues['disabled-flag'] = false; + + final result = await Posthog().getFeatureFlagResult('disabled-flag'); + + expect(result, isNotNull); + expect(result!.key, equals('disabled-flag')); + expect(result.enabled, isFalse); + expect(result.variant, isNull); + }); + + test('returns correct result for multivariate flag', () async { + fakePlatformInterface.featureFlagValues['multi-flag'] = 'variant-a'; + + final result = await Posthog().getFeatureFlagResult('multi-flag'); + + expect(result, isNotNull); + expect(result!.key, equals('multi-flag')); + expect(result.enabled, isTrue); + expect(result.variant, equals('variant-a')); + }); + + test('includes payload when present', () async { + fakePlatformInterface.featureFlagValues['flag-with-payload'] = true; + fakePlatformInterface.featureFlagPayloads['flag-with-payload'] = { + 'discount': 10, + 'message': 'Welcome!', + }; + + final result = await Posthog().getFeatureFlagResult('flag-with-payload'); + + expect(result, isNotNull); + expect(result!.payload, isNotNull); + expect(result.payload, isA()); + final payload = result.payload as Map; + expect(payload['discount'], equals(10)); + expect(payload['message'], equals('Welcome!')); + }); + + test('multivariate flag with payload', () async { + fakePlatformInterface.featureFlagValues['multi-with-payload'] = 'control'; + fakePlatformInterface.featureFlagPayloads['multi-with-payload'] = [ + 1, + 2, + 3 + ]; + + final result = await Posthog().getFeatureFlagResult('multi-with-payload'); + + expect(result, isNotNull); + expect(result!.enabled, isTrue); + expect(result.variant, equals('control')); + expect(result.payload, equals([1, 2, 3])); + }); + + test('returns result for flag with null value', () async { + // Flag exists but has null value - should return a result, not null + fakePlatformInterface.featureFlagValues['null-value-flag'] = null; + + final result = await Posthog().getFeatureFlagResult('null-value-flag'); + + expect(result, isNotNull); + expect(result!.key, equals('null-value-flag')); + expect(result.enabled, isFalse); + }); + + test('passes sendEvent=true by default', () async { + fakePlatformInterface.featureFlagValues['test'] = true; + + await Posthog().getFeatureFlagResult('test'); + + expect(fakePlatformInterface.getFeatureFlagResultCalls.last['sendEvent'], + isTrue); + }); + + test('passes sendEvent=false when specified', () async { + fakePlatformInterface.featureFlagValues['test'] = true; + + await Posthog().getFeatureFlagResult('test', sendEvent: false); + + expect(fakePlatformInterface.getFeatureFlagResultCalls.last['sendEvent'], + isFalse); + }); + }); + + group('PostHogFeatureFlagResult', () { + test('fromValueAndPayload with null value', () { + final result = PostHogFeatureFlagResult.fromValueAndPayload( + 'test-key', + null, + null, + ); + + expect(result.key, equals('test-key')); + expect(result.enabled, isFalse); + expect(result.variant, isNull); + }); + + test('fromValueAndPayload with boolean true', () { + final result = PostHogFeatureFlagResult.fromValueAndPayload( + 'test-key', + true, + {'data': 'value'}, + ); + + expect(result.key, equals('test-key')); + expect(result.enabled, isTrue); + expect(result.variant, isNull); + expect(result.payload, equals({'data': 'value'})); + }); + + test('fromValueAndPayload with string variant', () { + final result = PostHogFeatureFlagResult.fromValueAndPayload( + 'test-key', + 'variant-b', + 42, + ); + + expect(result.key, equals('test-key')); + expect(result.enabled, isTrue); + expect(result.variant, equals('variant-b')); + expect(result.payload, equals(42)); + }); + + test('equality', () { + final result1 = PostHogFeatureFlagResult( + key: 'flag', + enabled: true, + variant: 'a', + payload: 1, + ); + final result2 = PostHogFeatureFlagResult( + key: 'flag', + enabled: true, + variant: 'a', + payload: 1, + ); + final result3 = PostHogFeatureFlagResult( + key: 'flag', + enabled: true, + variant: 'b', + payload: 1, + ); + + expect(result1, equals(result2)); + expect(result1, isNot(equals(result3))); + }); + + test('toString', () { + final result = PostHogFeatureFlagResult( + key: 'my-flag', + enabled: true, + variant: 'test', + payload: null, + ); + + expect( + result.toString(), + equals( + 'PostHogFeatureFlagResult(key: my-flag, enabled: true, variant: test, payload: null)'), + ); + }); + + test('fromMap returns null for null input', () { + final result = PostHogFeatureFlagResult.fromMap(null, 'fallback'); + expect(result, isNull); + }); + + test('fromMap returns null for non-Map input', () { + final result = PostHogFeatureFlagResult.fromMap('not a map', 'fallback'); + expect(result, isNull); + }); + + test('fromMap parses valid map', () { + final map = { + 'key': 'my-flag', + 'enabled': true, + 'variant': 'test-variant', + 'payload': {'data': 123}, + }; + + final result = PostHogFeatureFlagResult.fromMap(map, 'fallback'); + + expect(result, isNotNull); + expect(result!.key, equals('my-flag')); + expect(result.enabled, isTrue); + expect(result.variant, equals('test-variant')); + expect(result.payload, equals({'data': 123})); + }); + + 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'); + + expect(result, isNotNull); + expect(result!.key, equals('fallback-key')); + }); + + test('fromMap defaults enabled to false when not in map', () { + final map = { + 'key': 'my-flag', + }; + + final result = PostHogFeatureFlagResult.fromMap(map, 'fallback'); + + expect(result, isNotNull); + expect(result!.enabled, isFalse); + }); + }); } From 9e6f10a043d908194248764e30e15539a3b9ed80 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Thu, 5 Feb 2026 12:39:03 -0500 Subject: [PATCH 3/8] chore: Update native dependencies --- android/build.gradle | 4 ++-- ios/posthog_flutter.podspec | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 7c20f520..b6a40db4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,8 +53,8 @@ android { dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.0.0' - // + Version 3.30.0 and the versions up to 4.0.0, not including 4.0.0 and higher - implementation 'com.posthog:posthog-android:[3.30.0,4.0.0]' + // + Version 3.31.0 and the versions up to 4.0.0, not including 4.0.0 and higher + implementation 'com.posthog:posthog-android:[3.31.0,4.0.0]' } testOptions { diff --git a/ios/posthog_flutter.podspec b/ios/posthog_flutter.podspec index 4f5f2e17..ecfc7b4f 100644 --- a/ios/posthog_flutter.podspec +++ b/ios/posthog_flutter.podspec @@ -21,8 +21,8 @@ Postog flutter plugin s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' - # ~> Version 3.38.0 up to, but not including, 4.0.0 - s.dependency 'PostHog', '>= 3.38.0', '< 4.0.0' + # ~> Version 3.40.0 up to, but not including, 4.0.0 + s.dependency 'PostHog', '>= 3.40.0', '< 4.0.0' s.ios.deployment_target = '13.0' # PH iOS SDK 3.0.0 requires >= 10.15 From bd03e3f5adede0375cbac35cfadf9452f6807e95 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Thu, 5 Feb 2026 12:59:25 -0500 Subject: [PATCH 4/8] chore: Update example with `getFeatureFlagResult` --- example/lib/main.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9092b410..d14d3722 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -490,6 +490,16 @@ class InitialScreenState extends State { }, child: const Text("getFeatureFlagPayload"), ), + ElevatedButton( + onPressed: () async { + final result = await _posthogFlutterPlugin + .getFeatureFlagResult("feature_name"); + setState(() { + _result = result?.toString(); + }); + }, + child: const Text("getFeatureFlagResult"), + ), ElevatedButton( onPressed: () async { await _posthogFlutterPlugin.reloadFeatureFlags(); From 483265ded8e2d70f36740c65cb32d08173fcb92d Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Thu, 5 Feb 2026 13:00:33 -0500 Subject: [PATCH 5/8] docs: Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa737379..c1aa1080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- feat: add `getFeatureFlagResult` API ([#279](https://github.com/PostHog/posthog-flutter/pull/279)) + # 5.13.0 - chore: add support for thumbs up/down surveys ([#257](https://github.com/PostHog/posthog-flutter/pull/257)) From 630388f94e9fe000afa2d6317f81bad974370678 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Mon, 9 Feb 2026 13:52:33 -0500 Subject: [PATCH 6/8] fix: Error if the flag key isn't present --- .../com/posthog/flutter/PosthogFlutterPlugin.kt | 6 +++++- .../posthog/flutter/PosthogFlutterPluginTest.kt | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index 3006aa9c..885c3e34 100644 --- a/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -376,7 +376,11 @@ class PosthogFlutterPlugin : result: Result, ) { try { - val featureFlagKey: String = call.argument("key")!! + val featureFlagKey = call.argument("key") + if (featureFlagKey.isNullOrEmpty()) { + result.error("PosthogFlutterException", "Missing argument: key", null) + return + } val sendEvent: Boolean = call.argument("sendEvent") ?: true val flagResult = PostHog.getFeatureFlagResult(featureFlagKey, sendEvent) diff --git a/android/src/test/kotlin/com/posthog/flutter/PosthogFlutterPluginTest.kt b/android/src/test/kotlin/com/posthog/flutter/PosthogFlutterPluginTest.kt index d1eb3418..c5ff4cad 100644 --- a/android/src/test/kotlin/com/posthog/flutter/PosthogFlutterPluginTest.kt +++ b/android/src/test/kotlin/com/posthog/flutter/PosthogFlutterPluginTest.kt @@ -39,4 +39,19 @@ internal class PosthogFlutterPluginTest { Mockito.verify(mockResult).success(true) } + + @Test + fun onMethodCall_getFeatureFlagResult_missingKey_returnsError() { + val plugin = PosthogFlutterPlugin() + + val call = MethodCall("getFeatureFlagResult", mapOf()) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).error( + Mockito.eq("PosthogFlutterException"), + Mockito.eq("Missing argument: key"), + Mockito.isNull() + ) + } } From 108f9eac2c9fe7ea8fe4e90b18a8b1041d58458b Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Mon, 9 Feb 2026 14:03:31 -0500 Subject: [PATCH 7/8] fix: Drop PostHogFeatureFlagResult.fromValueAndPayload It was replaced by `fromMap` --- lib/src/feature_flag_result.dart | 38 ------------------- ...sthog_flutter_platform_interface_fake.dart | 9 ++++- test/posthog_test.dart | 38 ------------------- 3 files changed, 8 insertions(+), 77 deletions(-) diff --git a/lib/src/feature_flag_result.dart b/lib/src/feature_flag_result.dart index eea44d30..924e8561 100644 --- a/lib/src/feature_flag_result.dart +++ b/lib/src/feature_flag_result.dart @@ -25,44 +25,6 @@ class PostHogFeatureFlagResult { this.payload, }); - /// Creates a [PostHogFeatureFlagResult] from a raw flag value and payload. - /// - /// The [flagValue] can be: - /// - `null` or `false`: Flag is disabled - /// - `true`: Boolean flag is enabled - /// - `String`: Multivariate flag with the given variant - factory PostHogFeatureFlagResult.fromValueAndPayload( - String key, - Object? flagValue, - Object? payload, - ) { - if (flagValue == null || flagValue == false) { - return PostHogFeatureFlagResult( - key: key, - enabled: false, - variant: null, - payload: payload, - ); - } - - if (flagValue == true) { - return PostHogFeatureFlagResult( - key: key, - enabled: true, - variant: null, - payload: payload, - ); - } - - // Multivariate flag - value is the variant string - return PostHogFeatureFlagResult( - key: key, - enabled: true, - variant: flagValue.toString(), - payload: payload, - ); - } - @override String toString() { return 'PostHogFeatureFlagResult(key: $key, enabled: $enabled, variant: $variant, payload: $payload)'; diff --git a/test/posthog_flutter_platform_interface_fake.dart b/test/posthog_flutter_platform_interface_fake.dart index 38535a3b..21ae12b7 100644 --- a/test/posthog_flutter_platform_interface_fake.dart +++ b/test/posthog_flutter_platform_interface_fake.dart @@ -79,6 +79,13 @@ class PosthogFlutterPlatformFake extends PosthogFlutterPlatformInterface { } final value = featureFlagValues[key]; final payload = featureFlagPayloads[key]; - return PostHogFeatureFlagResult.fromValueAndPayload(key, value, payload); + final enabled = value != null && value != false; + final variant = (value is String) ? value : null; + return PostHogFeatureFlagResult( + key: key, + enabled: enabled, + variant: variant, + payload: payload, + ); } } diff --git a/test/posthog_test.dart b/test/posthog_test.dart index 91a56453..e356efed 100644 --- a/test/posthog_test.dart +++ b/test/posthog_test.dart @@ -144,44 +144,6 @@ void main() { }); group('PostHogFeatureFlagResult', () { - test('fromValueAndPayload with null value', () { - final result = PostHogFeatureFlagResult.fromValueAndPayload( - 'test-key', - null, - null, - ); - - expect(result.key, equals('test-key')); - expect(result.enabled, isFalse); - expect(result.variant, isNull); - }); - - test('fromValueAndPayload with boolean true', () { - final result = PostHogFeatureFlagResult.fromValueAndPayload( - 'test-key', - true, - {'data': 'value'}, - ); - - expect(result.key, equals('test-key')); - expect(result.enabled, isTrue); - expect(result.variant, isNull); - expect(result.payload, equals({'data': 'value'})); - }); - - test('fromValueAndPayload with string variant', () { - final result = PostHogFeatureFlagResult.fromValueAndPayload( - 'test-key', - 'variant-b', - 42, - ); - - expect(result.key, equals('test-key')); - expect(result.enabled, isTrue); - expect(result.variant, equals('variant-b')); - expect(result.payload, equals(42)); - }); - test('equality', () { final result1 = PostHogFeatureFlagResult( key: 'flag', From 91b5f630d0fc6167e18ff9548c8403e2efe5a87a Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Mon, 9 Feb 2026 14:21:01 -0500 Subject: [PATCH 8/8] fix: Drop payload from equals/hashCode I'm not certain what the use case is to compare feature flag results, so I don't think it's worth deep comparing the result. Maybe we'll get feedback on this in the future and reconsider. --- lib/src/feature_flag_result.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/feature_flag_result.dart b/lib/src/feature_flag_result.dart index 924e8561..d2b43890 100644 --- a/lib/src/feature_flag_result.dart +++ b/lib/src/feature_flag_result.dart @@ -36,12 +36,11 @@ class PostHogFeatureFlagResult { return other is PostHogFeatureFlagResult && other.key == key && other.enabled == enabled && - other.variant == variant && - other.payload == payload; + other.variant == variant; } @override - int get hashCode => Object.hash(key, enabled, variant, payload); + int get hashCode => Object.hash(key, enabled, variant); /// Creates a [PostHogFeatureFlagResult] from a native SDK response map. ///