Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ class PosthogFlutterPlugin :
getFeatureFlagPayload(call, result)
}

"getFeatureFlagResult" -> {
getFeatureFlagResult(call, result)
}

"register" -> {
register(call, result)
}
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,16 @@ class InitialScreenState extends State<InitialScreen> {
},
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();
Expand Down
27 changes: 27 additions & 0 deletions ios/Classes/PosthogFlutterPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions ios/posthog_flutter.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/posthog_flutter.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
13 changes: 13 additions & 0 deletions lib/posthog_flutter_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -229,6 +230,18 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
MethodCall('getFeatureFlagPayload', {'key': key}));
}

@override
Future<PostHogFeatureFlagResult?> 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<void> flush() async {
return handleWebMethodCall(const MethodCall('flush'));
Expand Down
100 changes: 100 additions & 0 deletions lib/src/feature_flag_result.dart
Original file line number Diff line number Diff line change
@@ -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'],
);
}
}
29 changes: 29 additions & 0 deletions lib/src/posthog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Object?> 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<PostHogFeatureFlagResult?> 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<Object?> getFeatureFlagPayload(String key) =>
_posthog.getFeatureFlagPayload(key: key);

Expand Down
24 changes: 24 additions & 0 deletions lib/src/posthog_flutter_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -525,6 +526,29 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
}
}

@override
Future<PostHogFeatureFlagResult?> 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<void> register(String key, Object value) async {
if (!isSupportedPlatform()) {
Expand Down
9 changes: 9 additions & 0 deletions lib/src/posthog_flutter_platform_interface.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -133,6 +134,14 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface {
'getFeatureFlagPayload() has not been implemented.');
}

Future<PostHogFeatureFlagResult?> getFeatureFlagResult({
required String key,
bool sendEvent = true,
}) {
throw UnimplementedError(
'getFeatureFlagResult() has not been implemented.');
}

Future<void> flush() {
throw UnimplementedError('flush() has not been implemented.');
}
Expand Down
10 changes: 10 additions & 0 deletions lib/src/posthog_flutter_web_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -444,6 +445,15 @@ Future<dynamic> 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'];
Expand Down
Loading
Loading