Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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 `onFeatureFlags` callback to `Posthog()` to listen for feature flag load events. On Web, this callback provides all flags and variants. On mobile (Android/iOS), it serves as a signal that flags have been loaded by the native SDK; the `flags` and `flagVariants` parameters will be empty in the callback, and developers should use `Posthog.getFeatureFlag()` or `Posthog.isFeatureEnabled()` to retrieve specific flag values. This allows developers to ensure flags are loaded before checking them, especially on the first app run. ([#224](https://github.com/PostHog/posthog-flutter/pull/224))

## 5.9.0

- feat: add autocapture exceptions ([#214](https://github.com/PostHog/posthog-flutter/pull/214))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.util.Log
import com.posthog.PersonProfiles
import com.posthog.PostHog
import com.posthog.PostHogConfig
import com.posthog.PostHogOnFeatureFlags
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import com.posthog.android.internal.getApplicationInfo
Expand Down Expand Up @@ -305,7 +306,15 @@ class PosthogFlutterPlugin :

sdkName = "posthog-flutter"
sdkVersion = postHogVersion

onFeatureFlags =
PostHogOnFeatureFlags {
Log.i("PostHogFlutter", "Android onFeatureFlags triggered. Notifying Dart.")
// Send empty map, Dart side handles defaults
invokeFlutterMethod("onFeatureFlagsCallback", emptyMap<String, Any?>())
}
}

PostHogAndroid.setup(applicationContext, config)
}

Expand Down
31 changes: 25 additions & 6 deletions ios/Classes/PosthogFlutterPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,48 @@ import PostHog

public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
private static var instance: PosthogFlutterPlugin?
private var channel: FlutterMethodChannel?

public static func getInstance() -> PosthogFlutterPlugin? {
instance
}

override init() {
super.init()
NotificationCenter.default.addObserver(
self,
selector: #selector(featureFlagsDidUpdate),
name: PostHogSDK.didReceiveFeatureFlags,
object: nil
)
}

deinit {
NotificationCenter.default.removeObserver(self)
}

public static func register(with registrar: FlutterPluginRegistrar) {
let methodChannel: FlutterMethodChannel
#if os(iOS)
let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger())
methodChannel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger())
#elseif os(macOS)
let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger)
methodChannel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger)
#endif
let instance = PosthogFlutterPlugin()
instance.channel = channel
instance.channel = methodChannel
PosthogFlutterPlugin.instance = instance
initPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addMethodCallDelegate(instance, channel: methodChannel)
}

@objc func featureFlagsDidUpdate() {
// Send empty map, Dart side handles defaults
invokeFlutterMethod("onFeatureFlagsCallback", arguments: [String: Any]())
}

private let dispatchQueue = DispatchQueue(label: "com.posthog.PosthogFlutterPlugin",
target: .global(qos: .utility))

private var channel: FlutterMethodChannel?

public static func initPlugin() {
let autoInit = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.AUTO_INIT") as? Bool ?? true
if !autoInit {
Expand Down
52 changes: 50 additions & 2 deletions lib/posthog_flutter_web.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// In order to *not* need this ignore, consider extracting the "web" version
// of your plugin as a separate package, instead of inlining it in the same
// package as the core of your plugin.
import 'dart:js_interop';

import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

import 'src/posthog_config.dart';
import 'src/posthog_flutter_platform_interface.dart';
import 'src/posthog_flutter_web_handler.dart';

Expand All @@ -20,8 +23,53 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
);
final PosthogFlutterWeb instance = PosthogFlutterWeb();
channel.setMethodCallHandler(instance.handleMethodCall);
PosthogFlutterPlatformInterface.instance = instance;
}

Future<dynamic> handleMethodCall(MethodCall call) async {
// The 'setup' call is now handled by the setup method override.
// Other method calls are delegated to handleWebMethodCall.
if (call.method == 'setup') {
// This case should ideally not be hit if Posthog().setup directly calls the overridden setup.
// However, to be safe, we can log or ignore.
// For now, let's assume direct call to overridden setup handles it.
return null;
}
return handleWebMethodCall(call);
}

Future<dynamic> handleMethodCall(MethodCall call) =>
handleWebMethodCall(call);
@override
Future<void> setup(PostHogConfig config) async {
// It's assumed posthog-js is initialized by the user in their HTML.
// This setup primarily hooks into the existing posthog-js instance.

// If apiKey and host are in config, and posthog.init is to be handled by plugin:
// This is an example if we wanted the plugin to also call posthog.init()
// final jsOptions = <String, dynamic>{
// 'api_host': config.host,
// // Add other relevant options from PostHogConfig if needed for JS init
// }.jsify();
// posthog?.callMethod('init'.toJS, config.apiKey.toJS, jsOptions);

if (config.onFeatureFlags != null && posthog != null) {
final dartCallback = config.onFeatureFlags!;

final jsCallback = (JSArray jsFlags, JSObject jsFlagVariants) {
final List<String> flags = jsFlags.toDart.whereType<String>().toList();

Map<String, dynamic> flagVariants = {};
final dartVariantsMap =
jsFlagVariants.dartify() as Map<dynamic, dynamic>?;
if (dartVariantsMap != null) {
flagVariants = dartVariantsMap
.map((key, value) => MapEntry(key.toString(), value));
}

// When posthog-js onFeatureFlags fires, it implies successful loading.
dartCallback(flags, flagVariants, errorsLoading: false);
}.toJS;

posthog!.onFeatureFlags(jsCallback);
}
}
}
46 changes: 42 additions & 4 deletions lib/src/posthog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,48 @@ class Posthog {

String? _currentScreen;

/// Android and iOS only
/// Only used for the manual setup
/// Requires disabling the automatic init on Android and iOS:
/// com.posthog.posthog.AUTO_INIT: false
/// Initializes the PostHog SDK.
///
/// This method sets up the connection to your PostHog instance and prepares the SDK for tracking events and feature flags.
///
/// - [config]: The [PostHogConfig] object containing your API key, host, and other settings.
/// To listen for feature flag load events, provide an `onFeatureFlags` callback in the [PostHogConfig].
///
/// **Behavior of `onFeatureFlags` callback (when provided in `PostHogConfig`):**
///
/// **Web:**
/// The callback will receive:
/// - `flags`: A list of active feature flag keys (`List<String>`).
/// - `flagVariants`: A map of feature flag keys to their variant values (`Map<String, dynamic>`).
/// - `errorsLoading`: Will be `false` as the callback firing implies success.
///
/// **Mobile (Android/iOS):**
/// The callback serves primarily as a notification that the native PostHog SDK
/// has finished loading feature flags. In this case:
/// - `flags`: Will be an empty list (`List<String>`).
/// - `flagVariants`: Will be an empty map (`Map<String, dynamic>`).
/// - `errorsLoading`: Will be `null` if the native call was successful but contained no error info, or `true` if an error occurred during Dart-side processing of the callback.
/// After this callback is invoked, you can reliably use `Posthog().getFeatureFlag('your-flag-key')`
/// or `Posthog().isFeatureEnabled('your-flag-key')` to get the values of specific flags.
///
/// **Example with `onFeatureFlags` in `PostHogConfig`:**
/// ```dart
/// final config = PostHogConfig(
/// apiKey: 'YOUR_API_KEY',
/// host: 'YOUR_POSTHOG_HOST',
/// onFeatureFlags: (flags, flagVariants, {errorsLoading}) {
/// if (errorsLoading == true) {
/// print('Error loading feature flags!');
/// return;
/// }
/// // ... process flags ...
/// },
/// );
/// await Posthog().setup(config);
/// ```
///
/// For Android and iOS, if you are performing a manual setup,
/// ensure `com.posthog.posthog.AUTO_INIT: false` is set in your native configuration.
Future<void> setup(PostHogConfig config) {
_config = config; // Store the config

Expand Down
14 changes: 11 additions & 3 deletions lib/src/posthog_config.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'posthog_flutter_platform_interface.dart';

enum PostHogPersonProfiles { never, always, identifiedOnly }

enum PostHogDataMode { wifi, cellular, any }
Expand Down Expand Up @@ -44,10 +46,16 @@ class PostHogConfig {
/// Configuration for error tracking and exception capture
final errorTrackingConfig = PostHogErrorTrackingConfig();

// TODO: missing getAnonymousId, propertiesSanitizer, captureDeepLinks
// onFeatureFlags, integrations
/// Callback to be invoked when feature flags are loaded.
/// See [Posthog.setup] for more details on behavior per platform.
OnFeatureFlagsCallback? onFeatureFlags;

// TODO: missing getAnonymousId, propertiesSanitizer, captureDeepLinks integrations

PostHogConfig(this.apiKey);
PostHogConfig(
this.apiKey, {
this.onFeatureFlags,
});

Map<String, dynamic> toMap() {
return {
Expand Down
35 changes: 35 additions & 0 deletions lib/src/posthog_flutter_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
/// The method channel used to interact with the native platform.
final _methodChannel = const MethodChannel('posthog_flutter');

OnFeatureFlagsCallback? _onFeatureFlagsCallback;

/// Stored configuration for accessing inAppIncludes and other settings
PostHogConfig? _config;

Expand All @@ -38,13 +40,44 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
case 'hideSurveys':
await cleanupSurveys();
return null;
case 'onFeatureFlagsCallback':
if (_onFeatureFlagsCallback != null) {
try {
final args = call.arguments as Map<dynamic, dynamic>;
// Ensure correct types from native
// For mobile, args will be an empty map. Callback expects optional params.
final flags =
(args['flags'] as List<dynamic>?)?.cast<String>() ?? [];
final flagVariants =
(args['flagVariants'] as Map<dynamic, dynamic>?)
?.map((k, v) => MapEntry(k.toString(), v)) ??
<String, dynamic>{};
// For mobile, errorsLoading is not explicitly sent, so it will be null here.
final errorsLoading = args['errorsLoading'] as bool?;

_onFeatureFlagsCallback!(flags, flagVariants,
errorsLoading: errorsLoading);
} catch (e, s) {
printIfDebug('Error processing onFeatureFlagsCallback: $e\n$s');
_onFeatureFlagsCallback!([], <String, dynamic>{},
errorsLoading: true);
}
}
break;
default:
printIfDebug(
'[PostHog] ${call.method} not implemented in PosthogFlutterPlatformInterface');
return null;
}
}

void onFeatureFlags(OnFeatureFlagsCallback callback) {
if (!isSupportedPlatform()) {
return;
}
_onFeatureFlagsCallback = callback;
}

@override
Future<void> showSurvey(Map<String, dynamic> survey) async {
if (!isSupportedPlatform()) {
Expand Down Expand Up @@ -128,6 +161,8 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
return;
}

_onFeatureFlagsCallback = config.onFeatureFlags;

try {
await _methodChannel.invokeMethod('setup', config.toMap());
} on PlatformException catch (exception) {
Expand Down
8 changes: 8 additions & 0 deletions lib/src/posthog_flutter_platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'posthog_config.dart';
import 'posthog_flutter_io.dart';

/// Defines the callback signature for when feature flags are loaded.
/// [flags] is a list of active feature flag keys.
/// [flagVariants] is a map of feature flag keys to their variant values (String or bool).
/// [errorsLoading] is true if there was an error loading flags, otherwise false or null.
typedef OnFeatureFlagsCallback = void Function(
List<String> flags, Map<String, dynamic> flagVariants,
{bool? errorsLoading});

abstract class PosthogFlutterPlatformInterface extends PlatformInterface {
/// Constructs a PosthogFlutterPlatform.
PosthogFlutterPlatformInterface() : super(token: _token);
Expand Down
1 change: 1 addition & 0 deletions lib/src/posthog_flutter_web_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ extension PostHogExtension on PostHog {
external void unregister(JSAny key);
// ignore: non_constant_identifier_names
external JSAny? get_session_id();
external void onFeatureFlags(JSFunction callback);
}

// Accessing PostHog from the window object
Expand Down
Loading
Loading