Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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. ([#YOUR_PR_NUMBER_HERE])

## 5.0.0

- chore: support flutter web wasm builds ([#112](https://github.com/PostHog/posthog-flutter/pull/112))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package com.posthog.flutter

import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.posthog.PersonProfiles
import com.posthog.PostHog
import com.posthog.PostHogConfig
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import com.posthog.android.internal.getApplicationInfo
import com.posthog.PostHogOnFeatureFlags
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
Expand All @@ -27,6 +30,8 @@ class PosthogFlutterPlugin :

private lateinit var applicationContext: Context

private val mainHandler = Handler(Looper.getMainLooper())

private val snapshotSender = SnapshotSender()

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
Expand Down Expand Up @@ -257,12 +262,36 @@ class PosthogFlutterPlugin :
posthogConfig.getIfNotNull<Boolean>("sessionReplay") {
sessionReplay = it
}
posthogConfig.getIfNotNull<String>("dataMode") {
// Assuming DataMode is an enum or similar, handle appropriately
}

this.sessionReplayConfig.captureLogcat = false

sdkName = "posthog-flutter"
sdkVersion = postHogVersion

onFeatureFlags = PostHogOnFeatureFlags {
try {
Log.i("PostHogFlutter", "Android onFeatureFlags triggered. Notifying Dart.")
val arguments = mapOf(
"flags" to emptyList<String>(),
"flagVariants" to emptyMap<String, Any?>(),
"errorsLoading" to false
)
mainHandler.post { channel.invokeMethod("onFeatureFlagsCallback", arguments) }
} catch (e: Exception) {
Log.e("PostHogFlutter", "Error in onFeatureFlags signalling: ${e.message}", e)
val errorArguments = mapOf(
"flags" to emptyList<String>(),
"flagVariants" to emptyMap<String, Any?>(),
"errorsLoading" to true
)
mainHandler.post { channel.invokeMethod("onFeatureFlagsCallback", errorArguments) }
}
}
}

PostHogAndroid.setup(applicationContext, config)
}

Expand Down
37 changes: 34 additions & 3 deletions ios/Classes/PosthogFlutterPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,46 @@ import PostHog
#endif

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

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

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 = methodChannel

initPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addMethodCallDelegate(instance, channel: methodChannel)
}

@objc func featureFlagsDidUpdate() {
let flags: [String] = []
let flagVariants: [String: Any] = [:]

guard let channel = self.channel else {
print("PosthogFlutterPlugin: FlutterMethodChannel is nil in featureFlagsDidUpdate.")
return
}

channel.invokeMethod("onFeatureFlagsCallback", arguments: [
"flags": flags,
"flagVariants": flagVariants,
"errorsLoading": false
])
}

private let dispatchQueue = DispatchQueue(label: "com.posthog.PosthogFlutterPlugin",
Expand Down
47 changes: 47 additions & 0 deletions lib/src/posthog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,52 @@ class Posthog {

Future<String?> getSessionId() => _posthog.getSessionId();

/// Sets a callback to be invoked when feature flags are loaded from the PostHog server.
///
/// The behavior of this callback differs slightly between platforms:
///
/// **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>).
///
/// **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.
/// - `flagVariants`: Will be an empty map.
/// 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.
///
/// **Common Parameters:**
/// - `errorsLoading` (optional named parameter): A boolean indicating if an error occurred during the request to load the feature flags.
/// This is `true` if the request timed out or if there was an error. It will be `false` if the request was successful.
///
/// This is particularly useful on the first app load to ensure flags are available before you try to access them.
///
/// Example:
/// ```dart
/// Posthog().onFeatureFlags((flags, flagVariants, {errorsLoading}) {
/// if (errorsLoading == true) {
/// // Handle error, e.g. flags might be stale or unavailable
/// print('Error loading feature flags!');
/// return;
/// }
/// // On Web, you can iterate through flags and flagVariants directly.
/// // On Mobile, flags and flagVariants will be empty here.
/// // After this callback, you can safely query specific flags:
/// final isNewFeatureEnabled = await Posthog().isFeatureEnabled('new-feature');
/// if (isNewFeatureEnabled) {
/// // Implement logic for 'new-feature'
/// }
/// final variantValue = await Posthog().getFeatureFlag('multivariate-flag');
/// if (variantValue == 'test-variant') {
/// // Implement logic for 'test-variant' of 'multivariate-flag'
/// }
/// });
/// ```
void onFeatureFlags(OnFeatureFlagsCallback callback) =>
_posthog.onFeatureFlags(callback);

Posthog._internal();
}
52 changes: 52 additions & 0 deletions lib/src/posthog_flutter_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,58 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
/// The method channel used to interact with the native platform.
final _methodChannel = const MethodChannel('posthog_flutter');

OnFeatureFlagsCallback? _onFeatureFlagsCallback;
bool _methodCallHandlerInitialized = false;

void _ensureMethodCallHandlerInitialized() {
if (!_methodCallHandlerInitialized) {
_methodChannel.setMethodCallHandler(_handleMethodCall);
_methodCallHandlerInitialized = true;
}
}

Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'onFeatureFlagsCallback':
if (_onFeatureFlagsCallback != null) {
try {
final args = call.arguments as Map<dynamic, dynamic>;
// Ensure correct types from native
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>{};
final errorsLoading = args['errorsLoading'] as bool?;

_onFeatureFlagsCallback!(flags, flagVariants,
errorsLoading: errorsLoading);
} catch (e, s) {
printIfDebug('Error processing onFeatureFlagsCallback: $e\n$s');
// Invoke callback with empty/default values and errorsLoading: true
// to signal that an attempt was made but failed due to data issues.
if (_onFeatureFlagsCallback != null) {
_onFeatureFlagsCallback!([], <String, dynamic>{},
errorsLoading: true);
}
}
}
break;
default:
break;
}
}

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

@override
Future<void> setup(PostHogConfig config) async {
if (!isSupportedPlatform()) {
Expand Down
12 changes: 12 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 Expand Up @@ -124,5 +132,9 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface {
throw UnimplementedError('getSessionId() not implemented');
}

void onFeatureFlags(OnFeatureFlagsCallback callback) {
throw UnimplementedError('onFeatureFlags() has not been implemented.');
}

// TODO: missing capture with more parameters
}
Loading