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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/v6-flags-loaded-result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"posthog_flutter": major
---

- **Breaking:** `PostHogConfig.onFeatureFlags` now receives a `PostHogFeatureFlagsLoaded` with the enabled flags, their values and whether loading failed. Update callbacks from `() { ... }` to `(loaded) { ... }` (or `(_) { ... }`)
- **Breaking:** `reloadFeatureFlags()` now returns a `Future<PostHogFeatureFlagsLoaded>` with the reloaded flags
93 changes: 88 additions & 5 deletions api/posthog_flutter.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@
"name": "reloadFeatureFlags",
"parameters": [],
"relativePath": "lib/src/posthog_flutter_platform_interface.dart",
"returnTypeName": "Future<void>",
"returnTypeName": "Future<PostHogFeatureFlagsLoaded>",
"type": "method",
"typeParameterNames": []
},
Expand Down Expand Up @@ -1855,7 +1855,7 @@
"name": "reloadFeatureFlags",
"parameters": [],
"relativePath": "lib/src/posthog_flutter_platform_interface.dart",
"returnTypeName": "Future<void>",
"returnTypeName": "Future<PostHogFeatureFlagsLoaded>",
"type": "method",
"typeParameterNames": []
},
Expand Down Expand Up @@ -2301,7 +2301,7 @@
"isRequired": false,
"name": "onFeatureFlags",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "void Function()?"
"typeName": "void Function(PostHogFeatureFlagsLoaded)?"
},
{
"isDeprecated": false,
Expand Down Expand Up @@ -2582,7 +2582,7 @@
"isWriteable": true,
"name": "onFeatureFlags",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "void Function()?"
"typeName": "void Function(PostHogFeatureFlagsLoaded)?"
},
{
"entryPoints": [],
Expand Down Expand Up @@ -4522,7 +4522,7 @@
"name": "reloadFeatureFlags",
"parameters": [],
"relativePath": "lib/src/posthog.dart",
"returnTypeName": "Future<void>",
"returnTypeName": "Future<PostHogFeatureFlagsLoaded>",
"type": "method",
"typeParameterNames": []
},
Expand Down Expand Up @@ -6026,6 +6026,89 @@
"Object"
],
"typeParameterNames": []
},
{
"entryPoints": [
"posthog_flutter.dart"
],
"executableDeclarations": [
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isStatic": false,
"name": "new",
"parameters": [
{
"isDeprecated": false,
"isExperimental": false,
"isNamed": true,
"isRequired": true,
"name": "variants",
"relativePath": "lib/src/feature_flags_loaded.dart",
"typeName": "Map<String, Object?>"
},
{
"isDeprecated": false,
"isExperimental": false,
"isNamed": true,
"isRequired": true,
"name": "errorsLoading",
"relativePath": "lib/src/feature_flags_loaded.dart",
"typeName": "bool"
}
],
"relativePath": "lib/src/feature_flags_loaded.dart",
"returnTypeName": "PostHogFeatureFlagsLoaded",
"type": "constructor",
"typeParameterNames": []
}
],
"fieldDeclarations": [
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isReadable": true,
"isStatic": false,
"isWriteable": false,
"name": "flags",
"relativePath": "lib/src/feature_flags_loaded.dart",
"typeName": "List<String>"
},
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isReadable": true,
"isStatic": false,
"isWriteable": false,
"name": "variants",
"relativePath": "lib/src/feature_flags_loaded.dart",
"typeName": "Map<String, Object>"
},
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isReadable": true,
"isStatic": false,
"isWriteable": false,
"name": "errorsLoading",
"relativePath": "lib/src/feature_flags_loaded.dart",
"typeName": "bool"
}
],
"isDeprecated": false,
"isExperimental": false,
"isRequired": false,
"isSealed": false,
"name": "PostHogFeatureFlagsLoaded",
"relativePath": "lib/src/feature_flags_loaded.dart",
"superTypeNames": [
"Object"
],
"typeParameterNames": []
}
],
"iosPlatformConstraints": null,
Expand Down
7 changes: 5 additions & 2 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ Future<void> main() async {
final config = PostHogConfig(
'phc_6lqCaCDCBEWdIGieihq5R2dZpPVbAUFISA75vFZow06',
);
config.onFeatureFlags = () {
debugPrint('[PostHog] Feature flags loaded!');
config.onFeatureFlags = (loaded) {
debugPrint(
'[PostHog] Feature flags loaded: ${loaded.variants}, '
'errorsLoading: ${loaded.errorsLoading}',
);
};

config.beforeSend = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import com.posthog.PostHogBeforeSend
import com.posthog.PostHogBootstrapConfig
import com.posthog.PostHogCompression
import com.posthog.PostHogConfig
import com.posthog.PostHogOnFeatureFlags
import com.posthog.PostHogFeatureFlagsCallback
import com.posthog.PostHogFeatureFlagsLoaded
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import com.posthog.android.internal.getApplicationInfo
Expand Down Expand Up @@ -814,9 +815,9 @@ class PosthogFlutterPlugin :
}

onFeatureFlags =
PostHogOnFeatureFlags {
PostHogFeatureFlagsCallback { loaded ->
Log.i("PostHogFlutter", "Android onFeatureFlags triggered. Notifying Dart.")
invokeFlutterMethod("onFeatureFlagsCallback", emptyMap<String, Any?>())
invokeFlutterMethod("onFeatureFlagsCallback", loaded.toFlutterMap())
}
}

Expand Down Expand Up @@ -1929,9 +1930,9 @@ class PosthogFlutterPlugin :
// Resolve the Dart Future only once flags have actually finished
// loading. The native callback fires on a background thread, so the
// result must be posted back to the main thread for Flutter.
PostHog.reloadFeatureFlags {
PostHog.reloadFeatureFlags { loaded ->
Handler(Looper.getMainLooper()).post {
result.success(null)
result.success(loaded.toFlutterMap())
}
}
} catch (e: Throwable) {
Expand Down Expand Up @@ -2232,6 +2233,8 @@ class PosthogFlutterPlugin :
}
}

private fun PostHogFeatureFlagsLoaded.toFlutterMap(): Map<String, Any> = mapOf("variants" to variants, "errorsLoading" to errorsLoading)

private fun invokeFlutterMethod(
method: String,
arguments: Any? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin {

override init() {
super.init()
featureFlagsSubscription = PostHogSDK.shared.onFeatureFlags { [weak self] _ in
self?.featureFlagsDidUpdate()
featureFlagsSubscription = PostHogSDK.shared.onFeatureFlags { [weak self] loaded in
self?.featureFlagsDidUpdate(loaded)
}
}

Expand All @@ -87,8 +87,12 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
registrar.addMethodCallDelegate(instance, channel: methodChannel)
}

func featureFlagsDidUpdate() {
invokeFlutterMethod("onFeatureFlagsCallback", arguments: [String: Any]())
func featureFlagsDidUpdate(_ loaded: PostHogFeatureFlagsLoaded) {
invokeFlutterMethod("onFeatureFlagsCallback", arguments: Self.featureFlagsLoadedMap(loaded))
}

private static func featureFlagsLoadedMap(_ loaded: PostHogFeatureFlagsLoaded) -> [String: Any] {
["variants": loaded.variants, "errorsLoading": loaded.errorsLoading]
}

private let dispatchQueue = DispatchQueue(label: "com.posthog.PosthogFlutterPlugin",
Expand Down Expand Up @@ -1621,9 +1625,9 @@ extension PosthogFlutterPlugin {
private func reloadFeatureFlags(_ result: @escaping FlutterResult) {
// Resolve the Dart Future only once flags have actually finished loading.
// The native callback fires on a background thread, so hop to main for Flutter.
PostHogSDK.shared.reloadFeatureFlags { _ in
PostHogSDK.shared.reloadFeatureFlags { loaded in
DispatchQueue.main.async {
result(nil)
result(Self.featureFlagsLoadedMap(loaded))
}
}
}
Expand Down
1 change: 1 addition & 0 deletions posthog_flutter/lib/posthog_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
library posthog_flutter;

export 'src/feature_flag_result.dart';
export 'src/feature_flags_loaded.dart';
export 'src/logs/posthog_log_record.dart';
export 'src/logs/posthog_log_severity.dart';
export 'src/logs/posthog_logger.dart' hide CaptureLog;
Expand Down
12 changes: 8 additions & 4 deletions posthog_flutter/lib/posthog_flutter_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:posthog_flutter/src/utils/channel_serialization.dart';
import 'package:posthog_flutter/src/utils/property_normalizer.dart';

import 'src/feature_flag_result.dart';
import 'src/feature_flags_loaded.dart';
import 'src/logs/posthog_log_severity.dart';
import 'src/posthog_config.dart';
import 'src/posthog_constants.dart';
Expand Down Expand Up @@ -129,10 +130,12 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
final dartCallback = config.onFeatureFlags!;

// JS SDK calls with: (flags: string[], variants: Record, context?: {errorsLoading})
// We ignore the JS parameters and just invoke the void callback
final jsCallback =
(JSArray jsFlags, JSObject jsFlagVariants, [JSObject? jsContext]) {
dartCallback();
dartCallback(featureFlagsLoadedFromMap({
'variants': jsFlagVariants.dartify(),
'errorsLoading': (jsContext?.dartify() as Map?)?['errorsLoading'],
}));
}.toJS;

ph.onFeatureFlags(jsCallback);
Expand Down Expand Up @@ -393,8 +396,9 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
}

@override
Future<void> reloadFeatureFlags() async {
return handleWebMethodCall(const MethodCall('reloadFeatureFlags'));
Future<PostHogFeatureFlagsLoaded> reloadFeatureFlags() async {
return featureFlagsLoadedFromMap(
await handleWebMethodCall(const MethodCall('reloadFeatureFlags')));
}

@override
Expand Down
7 changes: 7 additions & 0 deletions posthog_flutter/lib/src/core/posthog_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dart:math';
import 'package:meta/meta.dart';

import '../feature_flag_result.dart';
import '../feature_flags_loaded.dart';
import '../posthog_config.dart';
import 'feature_flag_utils.dart';
import 'feature_flags.dart';
Expand Down Expand Up @@ -923,6 +924,12 @@ abstract class PostHogCore extends PostHogCoreStateless {
await wrap(_flagsAsync);
}

/// The enabled flags currently served, and whether the latest load failed.
PostHogFeatureFlagsLoaded featureFlagsLoaded() => PostHogFeatureFlagsLoaded(
variants: _getFeatureFlags() ?? const {},
errorsLoading: _getStoredFlagDetails()?.requestError != null,
);

/// Registers a callback for when feature flags are loaded. A callback
/// registered right after the client is created also hears about the flags
/// it starts with, bootstrapped or cached. Returns an unsubscribe function.
Expand Down
53 changes: 53 additions & 0 deletions posthog_flutter/lib/src/feature_flags_loaded.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/// The feature flags passed to [PostHogConfig.onFeatureFlags] and returned by
/// [Posthog.reloadFeatureFlags].
///
/// Only enabled flags are included. When the latest load failed,
/// [errorsLoading] is `true` and the flags are the last known values, which
/// can be empty.
///
/// **Example:**
/// ```dart
/// final config = PostHogConfig('<project_token>')
/// ..onFeatureFlags = (loaded) {
/// if (loaded.errorsLoading) {
/// // The values are the last known (cached) flags.
/// }
/// if (loaded.variants['new-checkout'] == 'test') {
/// // Show the test variant.
/// }
/// };
/// ```
class PostHogFeatureFlagsLoaded {
/// The keys of the enabled feature flags.
List<String> get flags => List.unmodifiable(variants.keys);

/// The values of the enabled feature flags, keyed by flag key.
///
/// A value is `true` for an enabled boolean flag, or the variant [String]
/// for a multivariate flag. Disabled flags are not included.
final Map<String, Object> variants;

/// Whether the latest attempt to load feature flags failed.
///
/// When it did, [flags] and [variants] hold the last known values.
///
/// **Flutter web:** when [PostHogConfig.onFeatureFlags] is first set, it is
/// called right away with the current flags and `false`, even if
/// posthog-js's last load before that failed.
final bool errorsLoading;
Comment thread
turnipdabeets marked this conversation as resolved.

/// Creates the loaded feature flags from [variants], keeping only enabled
/// flags: `true`, or a non-empty variant string.
PostHogFeatureFlagsLoaded({
required Map<String, Object?> variants,
required this.errorsLoading,
}) : variants = Map.unmodifiable({
for (final MapEntry(:key, :value) in variants.entries)
if (value == true || (value is String && value.isNotEmpty))
key: value!,
});

@override
String toString() =>
'PostHogFeatureFlagsLoaded(variants: $variants, errorsLoading: $errorsLoading)';
}
Loading
Loading