diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5e398b..3431240 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
## Next
- chore: change host to new address ([#106](https://github.com/PostHog/posthog-flutter/pull/106))
+- chore: allow manual initialization of the SDK ([#117](https://github.com/PostHog/posthog-flutter/pull/117))
## 4.5.0
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ca99bbd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+.PHONY: formatKotlin formatSwift formatDart
+
+# brew install ktlint
+# TODO: add ktlint steps in CI
+formatKotlin:
+ ktlint --format
+
+# brew install swiftlint
+# TODO: add swiftlint steps in CI
+formatSwift:
+ swiftformat ios/Classes --swiftversion 5.3
+ swiftlint ios/Classes --fix
+
+formatDart:
+ dart format .
+ dart analyze .
diff --git a/README.md b/README.md
index c4e71c4..434951c 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ To use this plugin, add `posthog_flutter` as a [dependency in your pubspec.yaml
| Method | Android | iOS/macOS | Web |
| ------------------------- | ------- | --------- | --- |
+| `setup` | X | X | |
| `identify` | X | X | X |
| `capture` | X | X | X |
| `screen` | X | X | X |
@@ -29,6 +30,7 @@ To use this plugin, add `posthog_flutter` as a [dependency in your pubspec.yaml
| `getFeatureFlag` | X | X | X |
| `getFeatureFlagPayload` | X | X | X |
| `group` | X | X | X |
+| `close` | X | X | |
### Example
@@ -80,9 +82,9 @@ Remember that the application lifecycle events won't have any special context se
### Android
-#### AndroidManifest.xml
+Automatically:
-```xml
+```xml file=AndroidManifest.xml
@@ -97,11 +99,40 @@ Remember that the application lifecycle events won't have any special context se
```
+Or manually, disable the auto init:
+
+```xml file=AndroidManifest.xml
+
+
+
+ [...]
+
+
+
+
+```
+
+And setup the SDK manually:
+
+```dart
+Future main() async {
+ // init WidgetsFlutterBinding if not yet
+ WidgetsFlutterBinding.ensureInitialized();
+ final config = PostHogConfig('YOUR_API_KEY_GOES_HERE');
+ config.debug = true;
+ config.captureApplicationLifecycleEvents = true;
+ // or EU Host: 'https://eu.i.posthog.com'
+ config.host = 'https://us.i.posthog.com';
+ await Posthog().setup(config);
+ runApp(MyApp());
+}
+```
+
### iOS/macOS
-#### Info.plist
+Automatically:
-```xml
+```xml file=Info.plist
@@ -121,9 +152,40 @@ Remember that the application lifecycle events won't have any special context se
```
+Or manually, disable the auto init:
+
+```xml file=Info.plist
+
+
+
+
+ [...]
+ com.posthog.posthog.AUTO_INIT
+
+ [...]
+
+
+```
+
+And setup the SDK manually:
+
+```dart
+Future main() async {
+ // init WidgetsFlutterBinding if not yet
+ WidgetsFlutterBinding.ensureInitialized();
+ final config = PostHogConfig('YOUR_API_KEY_GOES_HERE');
+ config.debug = true;
+ config.captureApplicationLifecycleEvents = true;
+ // or EU Host: 'https://eu.i.posthog.com'
+ config.host = 'https://us.i.posthog.com';
+ await Posthog().setup(config);
+ runApp(MyApp());
+}
+```
+
### Web
-```html
+```html file=index.html
diff --git a/android/src/main/kotlin/com/posthog/posthog_flutter/PosthogFlutterPlugin.kt b/android/src/main/kotlin/com/posthog/posthog_flutter/PosthogFlutterPlugin.kt
index b978ed9..7587015 100644
--- a/android/src/main/kotlin/com/posthog/posthog_flutter/PosthogFlutterPlugin.kt
+++ b/android/src/main/kotlin/com/posthog/posthog_flutter/PosthogFlutterPlugin.kt
@@ -1,13 +1,25 @@
package com.posthog.posthog_flutter
import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
+import android.os.Build
+import android.os.Bundle
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.internal.replay.*
+import com.posthog.internal.replay.RRFullSnapshotEvent
+import com.posthog.internal.replay.RRIncrementalMutationData
+import com.posthog.internal.replay.RRIncrementalSnapshotEvent
+import com.posthog.internal.replay.RRMutatedNode
+import com.posthog.internal.replay.RRStyle
+import com.posthog.internal.replay.RRWireframe
+import com.posthog.internal.replay.capture
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@@ -16,27 +28,87 @@ import io.flutter.plugin.common.MethodChannel.Result
import java.io.ByteArrayOutputStream
/** PosthogFlutterPlugin */
-class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
- /// The MethodChannel that will the communication between Flutter and native Android
- ///
- /// This local reference serves to register the plugin with the Flutter Engine and unregister it
- /// when the Flutter Engine is detached from the Activity
+class PosthogFlutterPlugin :
+ FlutterPlugin,
+ MethodCallHandler {
+ // / The MethodChannel that will be the communication between Flutter and native Android
+ // /
+ // / This local reference serves to register the plugin with the Flutter Engine and unregister it
+ // / when the Flutter Engine is detached from the Activity
private lateinit var channel: MethodChannel
- private lateinit var context: Context
+ private lateinit var applicationContext: Context
+
+ private val snapshotSender = SnapshotSender()
+
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "posthog_flutter")
- context = flutterPluginBinding.applicationContext
+ this.applicationContext = flutterPluginBinding.applicationContext
+ initPlugin()
channel.setMethodCallHandler(this)
}
- override fun onMethodCall(call: MethodCall, result: Result) {
+ // TODO: expose on the android SDK instead
+ @Throws(PackageManager.NameNotFoundException::class)
+ private fun getApplicationInfo(context: Context): ApplicationInfo =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ context
+ .packageManager
+ .getApplicationInfo(
+ context.packageName,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()),
+ )
+ } else {
+ context
+ .packageManager
+ .getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
+ }
- when (call.method) {
+ private fun initPlugin() {
+ try {
+ val ai = getApplicationInfo(applicationContext)
+ val bundle = ai.metaData ?: Bundle()
+ val autoInit = bundle.getBoolean("com.posthog.posthog.AUTO_INIT", true)
+
+ if (!autoInit) {
+ Log.i("PostHog", "com.posthog.posthog.AUTO_INIT is disabled!")
+ return
+ }
+
+ val apiKey = bundle.getString("com.posthog.posthog.API_KEY")
+ if (apiKey.isNullOrEmpty()) {
+ Log.e("PostHog", "com.posthog.posthog.API_KEY is missing!")
+ return
+ }
+
+ val host = bundle.getString("com.posthog.posthog.POSTHOG_HOST", PostHogConfig.DEFAULT_HOST)
+ val captureApplicationLifecycleEvents = bundle.getBoolean("com.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS", false)
+ val debug = bundle.getBoolean("com.posthog.posthog.DEBUG", false)
+
+ val posthogConfig = mutableMapOf()
+ posthogConfig["apiKey"] = apiKey
+ posthogConfig["host"] = host
+ posthogConfig["captureApplicationLifecycleEvents"] = captureApplicationLifecycleEvents
+ posthogConfig["debug"] = debug
+
+ setupPostHog(posthogConfig)
+ } catch (e: Throwable) {
+ Log.e("PostHog", "initPlugin error: $e")
+ }
+ }
+
+ override fun onMethodCall(
+ call: MethodCall,
+ result: Result,
+ ) {
+ when (call.method) {
+ "setup" -> {
+ setup(call, result)
+ }
"identify" -> {
identify(call, result)
}
@@ -92,175 +164,143 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
"register" -> {
register(call, result)
}
-
"unregister" -> {
unregister(call, result)
}
-
"debug" -> {
debug(call, result)
}
-
"flush" -> {
flush(result)
}
-
- "initNativeSdk" -> {
- val configMap = call.arguments as? Map
- if (configMap != null) {
- initPlugin(configMap)
- result.success(null)
- } else {
- result.error("INVALID_ARGUMENT", "Config map is null or invalid", null)
- }
+ "close" -> {
+ close(result)
}
"sendFullSnapshot" -> {
- sendFullSnapshot(call, result)
+ handleSendFullSnapshot(call, result)
}
"sendIncrementalSnapshot" -> {
- sendIncrementalSnapshot(call, result)
+ handleSendIncrementalSnapshot(call, result)
}
else -> {
result.notImplemented()
}
}
+ }
+
+ private fun setup(
+ call: MethodCall,
+ result: Result,
+ ) {
+ try {
+ val args = call.arguments() as Map? ?: mapOf()
+ if (args.isEmpty()) {
+ result.error("PosthogFlutterException", "Arguments is null or empty", null)
+ return
+ }
+
+ setupPostHog(args)
+ result.success(null)
+ } catch (e: Throwable) {
+ result.error("PosthogFlutterException", e.localizedMessage, null)
+ }
}
- private fun initPlugin(configMap: Map) {
- val apiKey = configMap["apiKey"] as? String ?: ""
- val options = configMap["options"] as? Map ?: emptyMap()
-
- val captureNativeAppLifecycleEvents =
- options["captureNativeAppLifecycleEvents"] as? Boolean ?: false
- val enableSessionReplay = options["enableSessionReplay"] as? Boolean ?: false
- val sessionReplayConfigMap =
- options["sessionReplayConfig"] as? Map ?: emptyMap()
-
- val maskAllTextInputs = sessionReplayConfigMap["maskAllTextInputs"] as? Boolean ?: true
- val maskAllImages = sessionReplayConfigMap["maskAllImages"] as? Boolean ?: true
- val captureLog = sessionReplayConfigMap["captureLog"] as? Boolean ?: true
- val debouncerDelayMs =
- (sessionReplayConfigMap["androidDebouncerDelayMs"] as? Int ?: 500).toLong()
-
- val config = PostHogAndroidConfig(apiKey).apply {
- debug = false
- captureDeepLinks = false
- captureApplicationLifecycleEvents = captureNativeAppLifecycleEvents
- captureScreenViews = false
- sessionReplay = enableSessionReplay
- sessionReplayConfig.screenshot = true
- sessionReplayConfig.captureLogcat = captureLog
- sessionReplayConfig.debouncerDelayMs = debouncerDelayMs
- sessionReplayConfig.maskAllImages = maskAllImages
- sessionReplayConfig.maskAllTextInputs = maskAllTextInputs
+ private fun setupPostHog(posthogConfig: Map) {
+ val apiKey = posthogConfig["apiKey"] as String?
+ if (apiKey.isNullOrEmpty()) {
+ Log.e("PostHog", "apiKey is missing!")
+ return
}
- PostHogAndroid.setup(context, config)
+ val host = posthogConfig["host"] as String? ?: PostHogConfig.DEFAULT_HOST
+
+ val config =
+ PostHogAndroidConfig(apiKey, host).apply {
+ captureScreenViews = false
+ captureDeepLinks = false
+ posthogConfig.getIfNotNull("captureApplicationLifecycleEvents") {
+ captureApplicationLifecycleEvents = it
+ }
+ posthogConfig.getIfNotNull("debug") {
+ debug = it
+ }
+ posthogConfig.getIfNotNull("flushAt") {
+ flushAt = it
+ }
+ posthogConfig.getIfNotNull("maxQueueSize") {
+ maxQueueSize = it
+ }
+ posthogConfig.getIfNotNull("maxBatchSize") {
+ maxBatchSize = it
+ }
+ posthogConfig.getIfNotNull("flushInterval") {
+ flushIntervalSeconds = it
+ }
+ posthogConfig.getIfNotNull("sendFeatureFlagEvents") {
+ sendFeatureFlagEvent = it
+ }
+ posthogConfig.getIfNotNull("preloadFeatureFlags") {
+ preloadFeatureFlags = it
+ }
+ posthogConfig.getIfNotNull("optOut") {
+ optOut = it
+ }
+ posthogConfig.getIfNotNull("personProfiles") {
+ when (it) {
+ "never" -> personProfiles = PersonProfiles.NEVER
+ "always" -> personProfiles = PersonProfiles.ALWAYS
+ "identifiedOnly" -> personProfiles = PersonProfiles.IDENTIFIED_ONLY
+ }
+ }
+ posthogConfig.getIfNotNull("enableSessionReplay") {
+ sessionReplay = it
+ }
+
+ posthogConfig.getIfNotNull