Skip to content

Commit fcd46ba

Browse files
authored
feat: Add getFeatureFlagResult API (#279)
* feat: Add `getFeatureFlagResult` API * test: getFeatureFlagResult * chore: Update native dependencies * chore: Update example with `getFeatureFlagResult` * docs: Update CHANGELOG * fix: Error if the flag key isn't present * fix: Drop PostHogFeatureFlagResult.fromValueAndPayload It was replaced by `fromMap` * 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.
1 parent 84d1c9b commit fcd46ba

16 files changed

+481
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Next
22

3+
- feat: add `getFeatureFlagResult` API ([#279](https://github.com/PostHog/posthog-flutter/pull/279))
4+
35
# 5.13.0
46

57
- chore: add support for thumbs up/down surveys ([#257](https://github.com/PostHog/posthog-flutter/pull/257))

android/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ android {
5353
dependencies {
5454
testImplementation 'org.jetbrains.kotlin:kotlin-test'
5555
testImplementation 'org.mockito:mockito-core:5.0.0'
56-
// + Version 3.30.0 and the versions up to 4.0.0, not including 4.0.0 and higher
57-
implementation 'com.posthog:posthog-android:[3.30.0,4.0.0]'
56+
// + Version 3.31.0 and the versions up to 4.0.0, not including 4.0.0 and higher
57+
implementation 'com.posthog:posthog-android:[3.31.0,4.0.0]'
5858
}
5959

6060
testOptions {

android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ class PosthogFlutterPlugin :
146146
getFeatureFlagPayload(call, result)
147147
}
148148

149+
"getFeatureFlagResult" -> {
150+
getFeatureFlagResult(call, result)
151+
}
152+
149153
"register" -> {
150154
register(call, result)
151155
}
@@ -367,6 +371,36 @@ class PosthogFlutterPlugin :
367371
}
368372
}
369373

374+
private fun getFeatureFlagResult(
375+
call: MethodCall,
376+
result: Result,
377+
) {
378+
try {
379+
val featureFlagKey = call.argument<String>("key")
380+
if (featureFlagKey.isNullOrEmpty()) {
381+
result.error("PosthogFlutterException", "Missing argument: key", null)
382+
return
383+
}
384+
val sendEvent: Boolean = call.argument("sendEvent") ?: true
385+
val flagResult = PostHog.getFeatureFlagResult(featureFlagKey, sendEvent)
386+
387+
if (flagResult != null) {
388+
result.success(
389+
mapOf(
390+
"key" to flagResult.key,
391+
"enabled" to flagResult.enabled,
392+
"variant" to flagResult.variant,
393+
"payload" to flagResult.payload,
394+
),
395+
)
396+
} else {
397+
result.success(null)
398+
}
399+
} catch (e: Throwable) {
400+
result.error("PosthogFlutterException", e.localizedMessage, null)
401+
}
402+
}
403+
370404
private fun identify(
371405
call: MethodCall,
372406
result: Result,

android/src/test/kotlin/com/posthog/flutter/PosthogFlutterPluginTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,19 @@ internal class PosthogFlutterPluginTest {
3939

4040
Mockito.verify(mockResult).success(true)
4141
}
42+
43+
@Test
44+
fun onMethodCall_getFeatureFlagResult_missingKey_returnsError() {
45+
val plugin = PosthogFlutterPlugin()
46+
47+
val call = MethodCall("getFeatureFlagResult", mapOf<String, Any>())
48+
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
49+
plugin.onMethodCall(call, mockResult)
50+
51+
Mockito.verify(mockResult).error(
52+
Mockito.eq("PosthogFlutterException"),
53+
Mockito.eq("Missing argument: key"),
54+
Mockito.isNull()
55+
)
56+
}
4257
}

example/lib/main.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,16 @@ class InitialScreenState extends State<InitialScreen> {
490490
},
491491
child: const Text("getFeatureFlagPayload"),
492492
),
493+
ElevatedButton(
494+
onPressed: () async {
495+
final result = await _posthogFlutterPlugin
496+
.getFeatureFlagResult("feature_name");
497+
setState(() {
498+
_result = result?.toString();
499+
});
500+
},
501+
child: const Text("getFeatureFlagResult"),
502+
),
493503
ElevatedButton(
494504
onPressed: () async {
495505
await _posthogFlutterPlugin.reloadFeatureFlags();

ios/Classes/PosthogFlutterPlugin.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
183183
isFeatureEnabled(call, result: result)
184184
case "getFeatureFlagPayload":
185185
getFeatureFlagPayload(call, result: result)
186+
case "getFeatureFlagResult":
187+
getFeatureFlagResult(call, result: result)
186188
case "identify":
187189
identify(call, result: result)
188190
case "capture":
@@ -531,6 +533,31 @@ extension PosthogFlutterPlugin {
531533
}
532534
}
533535

536+
private func getFeatureFlagResult(
537+
_ call: FlutterMethodCall,
538+
result: @escaping FlutterResult
539+
) {
540+
if let args = call.arguments as? [String: Any],
541+
let featureFlagKey = args["key"] as? String
542+
{
543+
let sendEvent = args["sendEvent"] as? Bool ?? true
544+
let flagResult = PostHogSDK.shared.getFeatureFlagResult(featureFlagKey, sendFeatureFlagEvent: sendEvent)
545+
546+
if let flagResult {
547+
result([
548+
"key": flagResult.key,
549+
"enabled": flagResult.enabled,
550+
"variant": flagResult.variant as Any,
551+
"payload": flagResult.payload as Any
552+
])
553+
} else {
554+
result(nil)
555+
}
556+
} else {
557+
_badArgumentError(result)
558+
}
559+
}
560+
534561
private func identify(
535562
_ call: FlutterMethodCall,
536563
result: @escaping FlutterResult

ios/posthog_flutter.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Postog flutter plugin
2121
s.ios.dependency 'Flutter'
2222
s.osx.dependency 'FlutterMacOS'
2323

24-
# ~> Version 3.38.0 up to, but not including, 4.0.0
25-
s.dependency 'PostHog', '>= 3.38.0', '< 4.0.0'
24+
# ~> Version 3.40.0 up to, but not including, 4.0.0
25+
s.dependency 'PostHog', '>= 3.40.0', '< 4.0.0'
2626

2727
s.ios.deployment_target = '13.0'
2828
# PH iOS SDK 3.0.0 requires >= 10.15

lib/posthog_flutter.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
library posthog_flutter;
22

3+
export 'src/feature_flag_result.dart';
34
export 'src/posthog.dart';
45
export 'src/posthog_config.dart';
56
export 'src/posthog_event.dart';

lib/posthog_flutter_web.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:posthog_flutter/src/error_tracking/dart_exception_processor.dart
99
import 'package:posthog_flutter/src/util/logging.dart';
1010
import 'package:posthog_flutter/src/utils/property_normalizer.dart';
1111

12+
import 'src/feature_flag_result.dart';
1213
import 'src/posthog_config.dart';
1314
import 'src/posthog_flutter_platform_interface.dart';
1415
import 'src/posthog_flutter_web_handler.dart';
@@ -229,6 +230,18 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
229230
MethodCall('getFeatureFlagPayload', {'key': key}));
230231
}
231232

233+
@override
234+
Future<PostHogFeatureFlagResult?> getFeatureFlagResult({
235+
required String key,
236+
bool sendEvent = true,
237+
}) async {
238+
final result = await handleWebMethodCall(MethodCall(
239+
'getFeatureFlagResult', {'key': key, 'sendEvent': sendEvent}));
240+
241+
// Web SDK returns: { key, enabled, variant, payload }
242+
return PostHogFeatureFlagResult.fromMap(result, key);
243+
}
244+
232245
@override
233246
Future<void> flush() async {
234247
return handleWebMethodCall(const MethodCall('flush'));

lib/src/feature_flag_result.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/// Represents the result of a feature flag evaluation.
2+
///
3+
/// Contains the flag key, whether it's enabled, the variant (for multivariate flags),
4+
/// and any associated payload.
5+
class PostHogFeatureFlagResult {
6+
/// The feature flag key.
7+
final String key;
8+
9+
/// Whether the flag is enabled.
10+
///
11+
/// For boolean flags, this is the flag value.
12+
/// For multivariate flags, this is true when the flag evaluates to any variant.
13+
final bool enabled;
14+
15+
/// The variant key for multivariate flags, or null for boolean flags.
16+
final String? variant;
17+
18+
/// The JSON payload associated with the flag, if any.
19+
final Object? payload;
20+
21+
const PostHogFeatureFlagResult({
22+
required this.key,
23+
required this.enabled,
24+
this.variant,
25+
this.payload,
26+
});
27+
28+
@override
29+
String toString() {
30+
return 'PostHogFeatureFlagResult(key: $key, enabled: $enabled, variant: $variant, payload: $payload)';
31+
}
32+
33+
@override
34+
bool operator ==(Object other) {
35+
if (identical(this, other)) return true;
36+
return other is PostHogFeatureFlagResult &&
37+
other.key == key &&
38+
other.enabled == enabled &&
39+
other.variant == variant;
40+
}
41+
42+
@override
43+
int get hashCode => Object.hash(key, enabled, variant);
44+
45+
/// Creates a [PostHogFeatureFlagResult] from a native SDK response map.
46+
///
47+
/// The [map] should contain: key, enabled, variant, payload.
48+
/// Falls back to [fallbackKey] if the map doesn't include a key.
49+
/// Returns null if [result] is null or not a Map.
50+
static PostHogFeatureFlagResult? fromMap(Object? result, String fallbackKey) {
51+
if (result == null) return null;
52+
if (result is! Map) return null;
53+
54+
return PostHogFeatureFlagResult(
55+
key: result['key'] as String? ?? fallbackKey,
56+
enabled: result['enabled'] as bool? ?? false,
57+
variant: result['variant'] as String?,
58+
payload: result['payload'],
59+
);
60+
}
61+
}

0 commit comments

Comments
 (0)