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>("sessionReplayConfig") { sessionReplayConfig -> + sessionReplayConfig.getIfNotNull("androidDebouncerDelayMs") { + this.sessionReplayConfig.debouncerDelayMs = it + } + } + + sdkName = "posthog-flutter" + sdkVersion = postHogVersion + + } + PostHogAndroid.setup(applicationContext, config) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) } - /* - * TEMPORARY FUNCTION FOR TESTING PURPOSES - * This function sends a screenshot to PostHog. - * It should be removed or refactored in the other version. - */ - private fun sendFullSnapshot(call: MethodCall, result: MethodChannel.Result) { + private fun handleSendFullSnapshot(call: MethodCall, result: Result) { val imageBytes = call.argument("imageBytes") val id = call.argument("id") ?: 1 if (imageBytes != null) { - val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) - - val base64String = bitmapToBase64(bitmap) - - val wireframe = RRWireframe( - id = id, - x = 0, - y = 0, - width = bitmap.width, - height = bitmap.height, - type = "screenshot", - base64 = base64String, - style = RRStyle() - ) - - val snapshotEvent = RRFullSnapshotEvent( - listOf(wireframe), - initialOffsetTop = 0, - initialOffsetLeft = 0, - timestamp = System.currentTimeMillis() - ) - - Log.d("Snapshot", "Sending Full Snapshot") - listOf(snapshotEvent).capture() + snapshotSender.sendFullSnapshot(imageBytes, id) result.success(null) } else { result.error("INVALID_ARGUMENT", "Image bytes are null", null) } } - /* - * TEMPORARY FUNCTION FOR TESTING PURPOSES - * This function sends a screenshot to PostHog. - * It should be removed or refactored in the other version. - */ - private fun sendIncrementalSnapshot(call: MethodCall, result: MethodChannel.Result) { + private fun handleSendIncrementalSnapshot(call: MethodCall, result: Result) { val imageBytes = call.argument("imageBytes") val id = call.argument("id") ?: 1 if (imageBytes != null) { - val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) - - val base64String = bitmapToBase64(bitmap) - - val wireframe = RRWireframe( - id = id, - x = 0, - y = 0, - width = bitmap.width, - height = bitmap.height, - type = "screenshot", - base64 = base64String, - style = RRStyle() - ) - - val mutatedNode = RRMutatedNode(wireframe, parentId = null) - val updatedNodes = listOf(mutatedNode) - - val incrementalMutationData = RRIncrementalMutationData( - adds = null, - removes = null, - updates = updatedNodes - ) - - val incrementalSnapshotEvent = RRIncrementalSnapshotEvent( - mutationData = incrementalMutationData, - timestamp = System.currentTimeMillis() - ) - - Log.d("Snapshot", "Sending Incremental Snapshot") - listOf(incrementalSnapshotEvent).capture() + snapshotSender.sendIncrementalSnapshot(imageBytes, id) result.success(null) } else { result.error("INVALID_ARGUMENT", "Image bytes are null", null) } } - private fun bitmapToBase64(bitmap: Bitmap): String? { - ByteArrayOutputStream().use { byteArrayOutputStream -> - bitmap.compress( - Bitmap.CompressFormat.JPEG, - 30, - byteArrayOutputStream - ) - val byteArray = byteArrayOutputStream.toByteArray() - return android.util.Base64.encodeToString(byteArray, android.util.Base64.NO_WRAP) - } - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - - private fun getFeatureFlag(call: MethodCall, result: Result) { + private fun getFeatureFlag( + call: MethodCall, + result: Result, + ) { try { val featureFlagKey: String = call.argument("key")!! val flag = PostHog.getFeatureFlag(featureFlagKey) @@ -270,7 +310,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun getFeatureFlagPayload(call: MethodCall, result: Result) { + private fun getFeatureFlagPayload( + call: MethodCall, + result: Result, + ) { try { val featureFlagKey: String = call.argument("key")!! val flag = PostHog.getFeatureFlagPayload(featureFlagKey) @@ -280,7 +323,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun identify(call: MethodCall, result: Result) { + private fun identify( + call: MethodCall, + result: Result, + ) { try { val userId: String = call.argument("userId")!! val userProperties: Map? = call.argument("userProperties") @@ -292,7 +338,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun capture(call: MethodCall, result: Result) { + private fun capture( + call: MethodCall, + result: Result, + ) { try { val eventName: String = call.argument("eventName")!! val properties: Map? = call.argument("properties") @@ -303,7 +352,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun screen(call: MethodCall, result: Result) { + private fun screen( + call: MethodCall, + result: Result, + ) { try { val screenName: String = call.argument("screenName")!! val properties: Map? = call.argument("properties") @@ -314,7 +366,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun alias(call: MethodCall, result: Result) { + private fun alias( + call: MethodCall, + result: Result, + ) { try { val alias: String = call.argument("alias")!! PostHog.alias(alias) @@ -351,7 +406,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun debug(call: MethodCall, result: Result) { + private fun debug( + call: MethodCall, + result: Result, + ) { try { val debug: Boolean = call.argument("debug")!! PostHog.debug(debug) @@ -370,7 +428,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun isFeatureEnabled(call: MethodCall, result: Result) { + private fun isFeatureEnabled( + call: MethodCall, + result: Result, + ) { try { val key: String = call.argument("key")!! val isEnabled = PostHog.isFeatureEnabled(key) @@ -389,7 +450,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun group(call: MethodCall, result: Result) { + private fun group( + call: MethodCall, + result: Result, + ) { try { val groupType: String = call.argument("groupType")!! val groupKey: String = call.argument("groupKey")!! @@ -401,7 +465,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun register(call: MethodCall, result: Result) { + private fun register( + call: MethodCall, + result: Result, + ) { try { val key: String = call.argument("key")!! val value: Any = call.argument("value")!! @@ -412,7 +479,10 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } } - private fun unregister(call: MethodCall, result: Result) { + private fun unregister( + call: MethodCall, + result: Result, + ) { try { val key: String = call.argument("key")!! PostHog.unregister(key) @@ -430,4 +500,24 @@ class PosthogFlutterPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { result.error("PosthogFlutterException", e.localizedMessage, null) } } + + private fun close(result: Result) { + try { + PostHog.close() + result.success(null) + } catch (e: Throwable) { + result.error("PosthogFlutterException", e.localizedMessage, null) + } + } + + // Call the `completion` closure if cast to map value with `key` and type `T` is successful. + @Suppress("UNCHECKED_CAST") + private fun Map.getIfNotNull( + key: String, + callback: (T) -> Unit, + ) { + (get(key) as? T)?.let { + callback(it) + } + } } diff --git a/android/src/main/kotlin/com/posthog/posthog_flutter/SnapshotSender.kt b/android/src/main/kotlin/com/posthog/posthog_flutter/SnapshotSender.kt new file mode 100644 index 0000000..88b7efb --- /dev/null +++ b/android/src/main/kotlin/com/posthog/posthog_flutter/SnapshotSender.kt @@ -0,0 +1,86 @@ +package com.posthog.posthog_flutter + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import com.posthog.internal.replay.* +import java.io.ByteArrayOutputStream + +/* +* TEMPORARY CLASS FOR TESTING PURPOSES +* This function sends a screenshot to PostHog. +* It should be removed or refactored in the other version. +*/ +class SnapshotSender { + + fun sendFullSnapshot(imageBytes: ByteArray, id: Int = 1) { + val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + val base64String = bitmapToBase64(bitmap) + + val wireframe = RRWireframe( + id = id, + x = 0, + y = 0, + width = bitmap.width, + height = bitmap.height, + type = "screenshot", + base64 = base64String, + style = RRStyle() + ) + + val snapshotEvent = RRFullSnapshotEvent( + listOf(wireframe), + initialOffsetTop = 0, + initialOffsetLeft = 0, + timestamp = System.currentTimeMillis() + ) + + Log.d("Snapshot", "Sending Full Snapshot") + listOf(snapshotEvent).capture() + } + + fun sendIncrementalSnapshot(imageBytes: ByteArray, id: Int = 1) { + val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + val base64String = bitmapToBase64(bitmap) + + val wireframe = RRWireframe( + id = id, + x = 0, + y = 0, + width = bitmap.width, + height = bitmap.height, + type = "screenshot", + base64 = base64String, + style = RRStyle() + ) + + val mutatedNode = RRMutatedNode(wireframe, parentId = null) + val updatedNodes = listOf(mutatedNode) + + val incrementalMutationData = RRIncrementalMutationData( + adds = null, + removes = null, + updates = updatedNodes + ) + + val incrementalSnapshotEvent = RRIncrementalSnapshotEvent( + mutationData = incrementalMutationData, + timestamp = System.currentTimeMillis() + ) + + Log.d("Snapshot", "Sending Incremental Snapshot") + listOf(incrementalSnapshotEvent).capture() + } + + private fun bitmapToBase64(bitmap: Bitmap): String? { + ByteArrayOutputStream().use { byteArrayOutputStream -> + bitmap.compress( + Bitmap.CompressFormat.JPEG, + 30, + byteArrayOutputStream + ) + val byteArray = byteArrayOutputStream.toByteArray() + return android.util.Base64.encodeToString(byteArray, android.util.Base64.NO_WRAP) + } + } +} diff --git a/android/src/test/kotlin/com/posthog/posthog_flutter/PosthogFlutterPluginTest.kt b/android/src/test/kotlin/com/posthog/posthog_flutter/PosthogFlutterPluginTest.kt index b9a5d12..8d8da6a 100644 --- a/android/src/test/kotlin/com/posthog/posthog_flutter/PosthogFlutterPluginTest.kt +++ b/android/src/test/kotlin/com/posthog/posthog_flutter/PosthogFlutterPluginTest.kt @@ -2,8 +2,8 @@ package com.posthog.posthog_flutter import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import kotlin.test.Test import org.mockito.Mockito +import kotlin.test.Test /* * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. @@ -14,12 +14,11 @@ import org.mockito.Mockito */ internal class PosthogFlutterPluginTest { - @Test fun onMethodCall_identify_returnsExpectedValue() { val plugin = PosthogFlutterPlugin() - var arguments = mapOf("userId" to "abc"); + var arguments = mapOf("userId" to "abc") val call = MethodCall("identify", arguments) val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) @@ -32,7 +31,7 @@ internal class PosthogFlutterPluginTest { fun onMethodCall_alias_returnsExpectedValue() { val plugin = PosthogFlutterPlugin() - var arguments = mapOf("alias" to "abc"); + var arguments = mapOf("alias" to "abc") val call = MethodCall("alias", arguments) val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) diff --git a/example/android/app/src/main/kotlin/com/example/posthog_flutter_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/posthog_flutter_example/MainActivity.kt index efdc6a1..1529f5a 100644 --- a/example/android/app/src/main/kotlin/com/example/posthog_flutter_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/example/posthog_flutter_example/MainActivity.kt @@ -2,5 +2,4 @@ package com.example.posthog_flutter_example import io.flutter.embedding.android.FlutterActivity -class MainActivity: FlutterActivity() { -} +class MainActivity : FlutterActivity() diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 97940ec..99dd680 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -44,7 +44,7 @@ com.posthog.posthog.POSTHOG_HOST https://us.i.posthog.com com.posthog.posthog.API_KEY - _6SG-F7I1vCuZ-HdJL3VZQqjBlaSb1_20hDPwqMNnGI + phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS com.posthog.posthog.DEBUG @@ -53,5 +53,7 @@ UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/main.dart b/example/lib/main.dart index 7945fa5..4e091d3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,21 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:posthog_flutter/posthog_flutter.dart'; -void main() { - WidgetsFlutterBinding.ensureInitialized(); +import 'package:posthog_flutter/posthog_flutter.dart'; - Posthog().init( - 'API-KEY', - options: PostHogOptions( - captureNativeAppLifecycleEvents: true, - enableSessionReplay: true, - sessionReplayConfig: PostHogSessionReplayConfig( - maskAllTextInputs: true, - maskAllImages: true, - androidDebouncerDelay: const Duration(milliseconds: 200), - ), - ), - ); +Future main() async { + // // init WidgetsFlutterBinding if not yet + /* + WidgetsFlutterBinding.ensureInitialized(); + final config = + PostHogConfig('phc_l9TgCltyBi2JjR5OnCO8tjNeuEhgbvYTuyG7cHgQuRu'); + config.debug = true; + config.captureApplicationLifecycleEvents = true; + config.host = 'https://us.i.posthog.com'; + config.enableSessionReplay = true; + config.postHogSessionReplayConfig.maskAllTextInputs = true; + config.postHogSessionReplayConfig.maskAllImages = true; + config.postHogSessionReplayConfig.androidDebouncerDelay = const Duration(milliseconds: 200); + await Posthog().setup(config); + */ runApp(const MyApp()); } @@ -356,4 +357,4 @@ class ThirdRoute extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist index dc2d6f0..55201d4 100644 --- a/example/macos/Runner/Info.plist +++ b/example/macos/Runner/Info.plist @@ -32,7 +32,7 @@ com.posthog.posthog.POSTHOG_HOST https://us.i.posthog.com com.posthog.posthog.API_KEY - _6SG-F7I1vCuZ-HdJL3VZQqjBlaSb1_20hDPwqMNnGI + phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS com.posthog.posthog.DEBUG diff --git a/ios/Classes/PostHogFlutterVersion.swift b/ios/Classes/PostHogFlutterVersion.swift index 5dcdd41..dbab251 100644 --- a/ios/Classes/PostHogFlutterVersion.swift +++ b/ios/Classes/PostHogFlutterVersion.swift @@ -8,4 +8,4 @@ import Foundation // This property is internal only -internal let postHogFlutterVersion = "4.5.0" +let postHogFlutterVersion = "4.5.0" diff --git a/ios/Classes/PosthogFlutterPlugin.swift b/ios/Classes/PosthogFlutterPlugin.swift index 6fbfdb1..2f88607 100644 --- a/ios/Classes/PosthogFlutterPlugin.swift +++ b/ios/Classes/PosthogFlutterPlugin.swift @@ -1,55 +1,123 @@ import PostHog #if os(iOS) -import Flutter -import UIKit + import Flutter + import UIKit #elseif os(macOS) -import FlutterMacOS -import AppKit + import AppKit + import FlutterMacOS #endif public class PosthogFlutterPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { -#if os(iOS) - let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger()) -#elseif os(macOS) - let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger) -#endif + #if os(iOS) + let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger()) + #elseif os(macOS) + let channel = FlutterMethodChannel(name: "posthog_flutter", binaryMessenger: registrar.messenger) + #endif let instance = PosthogFlutterPlugin() initPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public static func initPlugin() { - // Initialise PostHog + let autoInit = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.AUTO_INIT") as? Bool ?? true + if !autoInit { + print("[PostHog] com.posthog.posthog.AUTO_INIT is disabled!") + return + } + let apiKey = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.API_KEY") as? String ?? "" + let host = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.POSTHOG_HOST") as? String ?? PostHogConfig.defaultHost + let captureApplicationLifecycleEvents = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS") as? Bool ?? false + let debug = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.DEBUG") as? Bool ?? false + + setupPostHog([ + "apiKey": apiKey, + "host": host, + "captureApplicationLifecycleEvents": captureApplicationLifecycleEvents, + "debug": debug + ]) + } + + private static func setupPostHog(_ posthogConfig: [String: Any]) { + let apiKey = posthogConfig["apiKey"] as? String ?? "" if apiKey.isEmpty { - print("[PostHog] com.posthog.posthog.API_KEY is missing!") + print("[PostHog] apiKey is missing!") return } - let host = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.POSTHOG_HOST") as? String ?? PostHogConfig.defaultHost - let postHogCaptureLifecyleEvents = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS") as? Bool ?? false - let postHogDebug = Bundle.main.object(forInfoDictionaryKey: "com.posthog.posthog.DEBUG") as? Bool ?? false + let host = posthogConfig["host"] as? String ?? PostHogConfig.defaultHost let config = PostHogConfig( apiKey: apiKey, host: host ) - config.captureApplicationLifecycleEvents = postHogCaptureLifecyleEvents - config.debug = postHogDebug config.captureScreenViews = false - + + if let captureApplicationLifecycleEvents = posthogConfig["captureApplicationLifecycleEvents"] as? Bool { + config.captureApplicationLifecycleEvents = captureApplicationLifecycleEvents + } + if let debug = posthogConfig["debug"] as? Bool { + config.debug = debug + } + if let flushAt = posthogConfig["flushAt"] as? Int { + config.flushAt = flushAt + } + if let maxQueueSize = posthogConfig["maxQueueSize"] as? Int { + config.maxQueueSize = maxQueueSize + } + if let maxBatchSize = posthogConfig["maxBatchSize"] as? Int { + config.maxBatchSize = maxBatchSize + } + if let flushInterval = posthogConfig["flushInterval"] as? Int { + config.flushIntervalSeconds = Double(flushInterval) + } + if let sendFeatureFlagEvents = posthogConfig["sendFeatureFlagEvents"] as? Bool { + config.sendFeatureFlagEvent = sendFeatureFlagEvents + } + if let preloadFeatureFlags = posthogConfig["preloadFeatureFlags"] as? Bool { + config.preloadFeatureFlags = preloadFeatureFlags + } + if let optOut = posthogConfig["optOut"] as? Bool { + config.optOut = optOut + } + if let personProfiles = posthogConfig["personProfiles"] as? String { + switch personProfiles { + case "never": + config.personProfiles = .never + case "always": + config.personProfiles = .always + case "identifiedOnly": + config.personProfiles = .identifiedOnly + default: + break + } + } + if let dataMode = posthogConfig["dataMode"] as? String { + switch dataMode { + case "wifi": + config.dataMode = .wifi + case "cellular": + config.dataMode = .cellular + case "any": + config.dataMode = .any + default: + break + } + } + // Update SDK name and version - postHogSdkName = "posthog-flutter" - postHogVersion = postHogFlutterVersion - + postHogSdkName = "posthog-flutter" + postHogVersion = postHogFlutterVersion + PostHogSDK.shared.setup(config) - // } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { + case "setup": + setup(call, result: result) case "getFeatureFlag": getFeatureFlag(call, result: result) case "isFeatureEnabled": @@ -84,18 +152,31 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { unregister(call, result: result) case "flush": flush(result) + case "close": + close(result) default: result(FlutterMethodNotImplemented) } } + private func setup( + _ call: FlutterMethodCall, + result: @escaping FlutterResult + ) { + if let args = call.arguments as? [String: Any] { + PosthogFlutterPlugin.setupPostHog(args) + result(nil) + } else { + _badArgumentError(result) + } + } + private func getFeatureFlag( _ call: FlutterMethodCall, result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let featureFlagKey = args["key"] as? String - { + let featureFlagKey = args["key"] as? String { let value = PostHogSDK.shared.getFeatureFlag(featureFlagKey) result(value) } else { @@ -108,8 +189,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let featureFlagKey = args["key"] as? String - { + let featureFlagKey = args["key"] as? String { let value = PostHogSDK.shared.isFeatureEnabled(featureFlagKey) result(value) } else { @@ -122,8 +202,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let featureFlagKey = args["key"] as? String - { + let featureFlagKey = args["key"] as? String { let value = PostHogSDK.shared.getFeatureFlagPayload(featureFlagKey) result(value) } else { @@ -136,8 +215,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let userId = args["userId"] as? String - { + let userId = args["userId"] as? String { let userProperties = args["userProperties"] as? [String: Any] let userPropertiesSetOnce = args["userPropertiesSetOnce"] as? [String: Any] @@ -157,8 +235,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let eventName = args["eventName"] as? String - { + let eventName = args["eventName"] as? String { let properties = args["properties"] as? [String: Any] PostHogSDK.shared.capture( eventName, @@ -175,8 +252,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let screenName = args["screenName"] as? String - { + let screenName = args["screenName"] as? String { let properties = args["properties"] as? [String: Any] PostHogSDK.shared.screen( screenName, @@ -193,8 +269,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let alias = args["alias"] as? String - { + let alias = args["alias"] as? String { PostHogSDK.shared.alias(alias) result(nil) } else { @@ -227,8 +302,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let debug = args["debug"] as? Bool - { + let debug = args["debug"] as? Bool { PostHogSDK.shared.debug(debug) result(nil) } else { @@ -248,8 +322,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { ) { if let args = call.arguments as? [String: Any], let groupType = args["groupType"] as? String, - let groupKey = args["groupKey"] as? String - { + let groupKey = args["groupKey"] as? String { let groupProperties = args["groupProperties"] as? [String: Any] PostHogSDK.shared.group(type: groupType, key: groupKey, groupProperties: groupProperties) result(nil) @@ -264,8 +337,7 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { ) { if let args = call.arguments as? [String: Any], let key = args["key"] as? String, - let value = args["value"] - { + let value = args["value"] { PostHogSDK.shared.register([key: value]) result(nil) } else { @@ -278,20 +350,24 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { result: @escaping FlutterResult ) { if let args = call.arguments as? [String: Any], - let key = args["key"] as? String - { + let key = args["key"] as? String { PostHogSDK.shared.unregister(key) result(nil) } else { _badArgumentError(result) } } - + private func flush(_ result: @escaping FlutterResult) { PostHogSDK.shared.flush() result(nil) } + private func close(_ result: @escaping FlutterResult) { + PostHogSDK.shared.close() + result(nil) + } + // Return bad Arguments error private func _badArgumentError(_ result: @escaping FlutterResult) { result(FlutterError( diff --git a/lib/posthog_flutter.dart b/lib/posthog_flutter.dart index 9a48350..ba1980d 100644 --- a/lib/posthog_flutter.dart +++ b/lib/posthog_flutter.dart @@ -1,6 +1,6 @@ library posthog_flutter; export 'src/posthog.dart'; +export 'src/posthog_config.dart'; export 'src/posthog_observer.dart'; -export 'src/posthog_options.dart'; export 'src/replay/posthog_screenshot_widget.dart'; diff --git a/lib/posthog_flutter_web.dart b/lib/posthog_flutter_web.dart index 93c75dc..83cccbe 100644 --- a/lib/posthog_flutter_web.dart +++ b/lib/posthog_flutter_web.dart @@ -24,7 +24,6 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { channel.setMethodCallHandler(instance.handleMethodCall); } - Future handleMethodCall(MethodCall call) async { - return handleWebMethodCall(call, context); - } + Future handleMethodCall(MethodCall call) => + handleWebMethodCall(call, context); } diff --git a/lib/src/android_initializer.dart b/lib/src/android_initializer.dart deleted file mode 100644 index 7b7ebc0..0000000 --- a/lib/src/android_initializer.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:posthog_flutter/src/posthog_options.dart'; - -import 'platform_initializer.dart'; - -class AndroidInitializer implements PlatformInitializer { - static const MethodChannel _channel = MethodChannel('posthog_flutter'); - - @override - Future init(String apiKey, PostHogOptions options) async { - - if (options.enableSessionReplay){ - setDefaultDebouncerDelay(options); - } - - final Map configMap = { - 'apiKey': apiKey, - 'options': options.toMap(), - }; - - try { - await _channel.invokeMethod('initNativeSdk', configMap); - } on PlatformException catch (e) { - print('Failed to initialize PostHog on Android: ${e.message}'); - } - } - - void setDefaultDebouncerDelay(PostHogOptions options){ - options.sessionReplayConfig?.androidDebouncerDelay ??= const Duration(milliseconds: 200); - } -} diff --git a/lib/src/ios_initializer.dart b/lib/src/ios_initializer.dart deleted file mode 100644 index 3a6223b..0000000 --- a/lib/src/ios_initializer.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:posthog_flutter/src/posthog_options.dart'; - -import 'platform_initializer.dart'; - -class IOSInitializer implements PlatformInitializer { - @override - Future init(String apiKey, PostHogOptions options) { - // TODO: implement init - throw UnimplementedError(); - } -} diff --git a/lib/src/platform_initializer.dart b/lib/src/platform_initializer.dart deleted file mode 100644 index f34e30e..0000000 --- a/lib/src/platform_initializer.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:posthog_flutter/src/posthog_options.dart'; - -abstract class PlatformInitializer { - Future init(String apiKey, PostHogOptions options); -} diff --git a/lib/src/platform_initializer_factory.dart b/lib/src/platform_initializer_factory.dart deleted file mode 100644 index c6f6319..0000000 --- a/lib/src/platform_initializer_factory.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:io' show Platform; - -import 'android_initializer.dart'; -import 'ios_initializer.dart'; -import 'platform_initializer.dart'; - -class PlatformInitializerFactory { - static PlatformInitializer getInitializer() { - if (Platform.isAndroid) { - return AndroidInitializer(); - } else if (Platform.isIOS) { - return IOSInitializer(); - } else { - throw UnsupportedError('Platform not supported'); - } - } -} diff --git a/lib/src/posthog.dart b/lib/src/posthog.dart index 2c07ba0..305f6ef 100644 --- a/lib/src/posthog.dart +++ b/lib/src/posthog.dart @@ -1,63 +1,45 @@ -import 'package:posthog_flutter/src/platform_initializer_factory.dart'; -import 'package:posthog_flutter/src/posthog_config.dart'; -import 'package:posthog_flutter/src/posthog_options.dart'; - +import 'posthog_config.dart'; import 'posthog_flutter_platform_interface.dart'; class Posthog { static PosthogFlutterPlatformInterface get _posthog => PosthogFlutterPlatformInterface.instance; - static final Posthog _instance = Posthog._internal(); + static final _instance = Posthog._internal(); + + PostHogConfig? _config; factory Posthog() { return _instance; } - Posthog._internal(); - - bool _initialized = false; - late String _apiKey; - late PostHogOptions _options; String? _currentScreen; - /// Initialization method - void init(String apiKey, {PostHogOptions? options}) { - if (_initialized) { - throw Exception('Posthog is already initialized'); - } - - _apiKey = apiKey; - _options = options ?? PostHogOptions(); - _initialized = true; - - PostHogConfig().options = options!; - - final initializer = PlatformInitializerFactory.getInitializer(); - initializer.init(_apiKey, _options); + /// 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 + Future setup(PostHogConfig config) { + _config = config; // Store the config + return _posthog.setup(config); } + PostHogConfig? get config => _config; + Future identify({ required String userId, Map? userProperties, Map? userPropertiesSetOnce, - }) { - if (!_initialized) { - throw Exception('Posthog is not initialized'); - } - return _posthog.identify( - userId: userId, - userProperties: userProperties, - userPropertiesSetOnce: userPropertiesSetOnce); - } + }) => + _posthog.identify( + userId: userId, + userProperties: userProperties, + userPropertiesSetOnce: userPropertiesSetOnce); Future capture({ required String eventName, Map? properties, }) { - if (!_initialized) { - throw Exception('Posthog is not initialized'); - } final propertiesCopy = properties == null ? null : {...properties}; final currentScreen = _currentScreen; @@ -75,92 +57,58 @@ class Posthog { Future screen({ required String screenName, Map? properties, - }) { - if (!_initialized) { - throw Exception('Posthog is not initialized'); - } - _currentScreen = screenName; - - return _posthog.screen( - screenName: screenName, - properties: properties, - ); - } + }) => + _posthog.screen( + screenName: screenName, + properties: properties, + ); Future alias({ required String alias, - }) { - if (!_initialized) { - throw Exception('Posthog is not initialized'); - } - return _posthog.alias( - alias: alias, - ); - } + }) => + _posthog.alias( + alias: alias, + ); - Future getDistinctId() { - if (!_initialized) { - throw Exception('Posthog is not initialized'); - } - return _posthog.getDistinctId(); - } + Future getDistinctId() => _posthog.getDistinctId(); - Future reset() { - return _posthog.reset(); - } + Future reset() => _posthog.reset(); - Future disable() { - return _posthog.disable(); - } + Future disable() => _posthog.disable(); - Future enable() { - return _posthog.enable(); - } + Future enable() => _posthog.enable(); - Future debug(bool enabled) { - return _posthog.debug(enabled); - } + Future debug(bool enabled) => _posthog.debug(enabled); - Future register(String key, Object value) { - return _posthog.register(key, value); - } + Future register(String key, Object value) => + _posthog.register(key, value); - Future unregister(String key) { - return _posthog.unregister(key); - } + Future unregister(String key) => _posthog.unregister(key); - Future isFeatureEnabled(String key) { - return _posthog.isFeatureEnabled(key); - } + Future isFeatureEnabled(String key) => _posthog.isFeatureEnabled(key); - Future reloadFeatureFlags() { - return _posthog.reloadFeatureFlags(); - } + Future reloadFeatureFlags() => _posthog.reloadFeatureFlags(); Future group({ required String groupType, required String groupKey, Map? groupProperties, - }) { - return _posthog.group( - groupType: groupType, - groupKey: groupKey, - groupProperties: groupProperties, - ); - } + }) => + _posthog.group( + groupType: groupType, + groupKey: groupKey, + groupProperties: groupProperties, + ); - Future getFeatureFlag(String key) { - return _posthog.getFeatureFlag(key: key); - } + Future getFeatureFlag(String key) => + _posthog.getFeatureFlag(key: key); - Future getFeatureFlagPayload(String key) { - return _posthog.getFeatureFlagPayload(key: key); - } + Future getFeatureFlagPayload(String key) => + _posthog.getFeatureFlagPayload(key: key); - Future flush() async { - if (!_initialized) { - throw Exception('Posthog is not initialized'); - } - return _posthog.flush(); - } + Future flush() => _posthog.flush(); + + Future close() => _posthog.close(); + + Posthog._internal(); } diff --git a/lib/src/posthog_config.dart b/lib/src/posthog_config.dart index ca3229e..7fe5d0f 100644 --- a/lib/src/posthog_config.dart +++ b/lib/src/posthog_config.dart @@ -1,13 +1,95 @@ -import 'package:posthog_flutter/src/posthog_options.dart'; +enum PostHogPersonProfiles { never, always, identifiedOnly } + +enum PostHogDataMode { wifi, cellular, any } class PostHogConfig { - static final PostHogConfig _instance = PostHogConfig._internal(); + final String apiKey; + var host = 'https://us.i.posthog.com'; + var flushAt = 20; + var maxQueueSize = 1000; + var maxBatchSize = 50; + var flushInterval = const Duration(seconds: 30); + var sendFeatureFlagEvents = true; + var preloadFeatureFlags = true; + var captureApplicationLifecycleEvents = false; + var debug = false; + var optOut = false; + var personProfiles = PostHogPersonProfiles.identifiedOnly; + var enableSessionReplay = false; + + var postHogSessionReplayConfig = PostHogSessionReplayConfig(); + + /// iOS only + var dataMode = PostHogDataMode.any; - factory PostHogConfig() { - return _instance; + // TODO: missing getAnonymousId, propertiesSanitizer, sessionReplay, captureDeepLinks + // onFeatureFlags, integrations + + PostHogConfig(this.apiKey); + + Map toMap() { + return { + 'apiKey': apiKey, + 'host': host, + 'flushAt': flushAt, + 'maxQueueSize': maxQueueSize, + 'maxBatchSize': maxBatchSize, + 'flushInterval': flushInterval.inSeconds, + 'sendFeatureFlagEvents': sendFeatureFlagEvents, + 'preloadFeatureFlags': preloadFeatureFlags, + 'captureApplicationLifecycleEvents': captureApplicationLifecycleEvents, + 'debug': debug, + 'optOut': optOut, + 'personProfiles': personProfiles.name, + 'enableSessionReplay':enableSessionReplay, + 'dataMode': dataMode.name, + 'sessionReplayConfig': postHogSessionReplayConfig.toMap(), + }; } +} - PostHogConfig._internal(); +class PostHogSessionReplayConfig { + /// Enable masking of all text input fields + /// Experimental support + /// Default: true + var maskAllTextInputs = true; - late PostHogOptions options; + /// Enable masking of all images to a placeholder + /// Experimental support + /// Default: true + var maskAllImages = true; + + /// Enable capturing of logcat as console events + /// Android only + /// Experimental support + /// Default: true + var captureLog = true; + + /// Debouncer delay used to reduce the number of snapshots captured and reduce performance impact + /// This is used for capturing the view as a screenshot + /// The lower the number, the more snapshots will be captured but higher the performance impact + /// Defaults to 1s on iOS + var iOSDebouncerDelay = const Duration(milliseconds: 200); + + /// Debouncer delay used to reduce the number of snapshots captured and reduce performance impact + /// This is used for capturing the view as a screenshot + /// The lower the number, the more snapshots will be captured but higher the performance impact + /// Defaults to 0.3s on Android + var androidDebouncerDelay = const Duration(milliseconds: 200); + + /// Enable capturing network telemetry + /// iOS only + /// Experimental support + /// Default: true + var captureNetworkTelemetry = true; + + Map toMap() { + return { + 'maskAllImages': maskAllImages, + 'captureLog': captureLog, + 'iOSDebouncerDelayMs': iOSDebouncerDelay.inMilliseconds, + 'androidDebouncerDelayMs': androidDebouncerDelay.inMilliseconds, + 'captureNetworkTelemetry': captureNetworkTelemetry, + }; + } } diff --git a/lib/src/posthog_flutter_io.dart b/lib/src/posthog_flutter_io.dart index ab342cc..13cc29f 100644 --- a/lib/src/posthog_flutter_io.dart +++ b/lib/src/posthog_flutter_io.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'posthog_config.dart'; import 'posthog_flutter_platform_interface.dart'; /// An implementation of [PosthogFlutterPlatformInterface] that uses method channels. @@ -8,6 +9,15 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { /// The method channel used to interact with the native platform. final _methodChannel = const MethodChannel('posthog_flutter'); + @override + Future setup(PostHogConfig config) async { + try { + await _methodChannel.invokeMethod('setup', config.toMap()); + } on PlatformException catch (exception) { + _printIfDebug('Exeption on setup: $exception'); + } + } + @override Future identify({ required String userId, @@ -211,6 +221,15 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { } } + @override + Future close() async { + try { + return await _methodChannel.invokeMethod('close'); + } on PlatformException catch (exception) { + _printIfDebug('Exeption on close: $exception'); + } + } + void _printIfDebug(String message) { if (kDebugMode) { print(message); diff --git a/lib/src/posthog_flutter_platform_interface.dart b/lib/src/posthog_flutter_platform_interface.dart index 6797cd1..7678282 100644 --- a/lib/src/posthog_flutter_platform_interface.dart +++ b/lib/src/posthog_flutter_platform_interface.dart @@ -1,5 +1,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'posthog_config.dart'; import 'posthog_flutter_io.dart'; abstract class PosthogFlutterPlatformInterface extends PlatformInterface { @@ -23,6 +24,10 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { _instance = instance; } + Future setup(PostHogConfig config) { + throw UnimplementedError('setup() has not been implemented.'); + } + Future identify( {required String userId, Map? userProperties, @@ -111,5 +116,9 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { throw UnimplementedError('flush() has not been implemented.'); } - // TODO: missing capture with more parameters, close + Future close() { + throw UnimplementedError('close() has not been implemented.'); + } + + // TODO: missing capture with more parameters } diff --git a/lib/src/posthog_flutter_web_handler.dart b/lib/src/posthog_flutter_web_handler.dart index 70843bd..a4fcd59 100644 --- a/lib/src/posthog_flutter_web_handler.dart +++ b/lib/src/posthog_flutter_web_handler.dart @@ -5,6 +5,10 @@ import 'package:flutter/services.dart'; Future handleWebMethodCall(MethodCall call, JsObject context) async { final analytics = JsObject.fromBrowserObject(context['posthog']); switch (call.method) { + case 'setup': + // not supported on Web + // analytics.callMethod('setup'); + break; case 'identify': final userProperties = call.arguments['userProperties'] ?? {}; final userPropertiesSetOnce = @@ -94,6 +98,10 @@ Future handleWebMethodCall(MethodCall call, JsObject context) async { // not supported on Web // analytics.callMethod('flush'); break; + case 'close': + // not supported on Web + // analytics.callMethod('close'); + break; default: throw PlatformException( code: 'Unimplemented', diff --git a/lib/src/posthog_options.dart b/lib/src/posthog_options.dart deleted file mode 100644 index 8f71f36..0000000 --- a/lib/src/posthog_options.dart +++ /dev/null @@ -1,96 +0,0 @@ -enum Persistence { - memory, - file, -} - -class PostHogSessionReplayConfig { - /// Enable masking of all text input fields - /// Experimental support - /// Default: true - final bool maskAllTextInputs; - - /// Enable masking of all images to a placeholder - /// Experimental support - /// Default: true - final bool maskAllImages; - - /// Enable capturing of logcat as console events - /// Android only - /// Experimental support - /// Default: true - final bool captureLog; - - /// Debouncer delay used to reduce the number of snapshots captured and reduce performance impact - /// This is used for capturing the view as a screenshot - /// The lower the number, the more snapshots will be captured but higher the performance impact - /// Defaults to 1s on iOS - final Duration? iOSDebouncerDelay; - - /// Debouncer delay used to reduce the number of snapshots captured and reduce performance impact - /// This is used for capturing the view as a screenshot - /// The lower the number, the more snapshots will be captured but higher the performance impact - /// Defaults to 0.5s on Android - Duration? androidDebouncerDelay; - - /// Enable capturing network telemetry - /// iOS only - /// Experimental support - /// Default: true - final bool captureNetworkTelemetry; - - PostHogSessionReplayConfig({ - this.maskAllTextInputs = true, - this.maskAllImages = true, - this.captureLog = true, - this.iOSDebouncerDelay, - this.androidDebouncerDelay, - this.captureNetworkTelemetry = true, - }); - - Map toMap() { - return { - 'maskAllTextInputs': maskAllTextInputs, - 'maskAllImages': maskAllImages, - 'captureLog': captureLog, - 'iOSDebouncerDelayMs': iOSDebouncerDelay?.inMilliseconds, - 'androidDebouncerDelayMs': androidDebouncerDelay?.inMilliseconds, - 'captureNetworkTelemetry': captureNetworkTelemetry, - }; - } -} - -class PostHogOptions { - /// Allows you to provide the storage type. By default 'file'. - /// 'file' will try to load the best available storage, the provided 'customStorage', 'customAsyncStorage' or in-memory storage. - final Persistence persistence; - - /// Captures native app lifecycle events such as Application Installed, Application Updated, Application Opened, Application Became Active, and Application Backgrounded. - /// By default is false. - /// If you're already using the 'captureLifecycleEvents' options with 'withReactNativeNavigation' or 'PostHogProvider', you should not set this to true, otherwise you may see duplicated events. - final bool captureNativeAppLifecycleEvents; - - /// Enable Recording of Session Replays for Android and iOS - /// Requires 'Record user sessions' to be enabled in the PostHog Project Settings - /// Experimental support - /// Defaults to false - final bool enableSessionReplay; - - /// Configuration for session replay - final PostHogSessionReplayConfig? sessionReplayConfig; - - PostHogOptions({ - this.persistence = Persistence.file, - this.captureNativeAppLifecycleEvents = false, - this.enableSessionReplay = false, - this.sessionReplayConfig, - }); - - Map toMap() { - return { - 'persistence': persistence.toString().split('.').last, - 'captureNativeAppLifecycleEvents': captureNativeAppLifecycleEvents, - 'enableSessionReplay': enableSessionReplay, - 'sessionReplayConfig': sessionReplayConfig?.toMap(), - }; - } -} diff --git a/lib/src/replay/element_parsers/element_data.dart b/lib/src/replay/element_parsers/element_data.dart index 8b662bd..542ef8b 100644 --- a/lib/src/replay/element_parsers/element_data.dart +++ b/lib/src/replay/element_parsers/element_data.dart @@ -24,20 +24,21 @@ class ElementData { rects.add(rect); } - for (var child in children!) { - if (child.children == null) { - rects.add(child.rect); - continue; - } - if (child.children!.length > 1) { - for (var grandChild in child.children!) { - rects.add(grandChild.rect); + if (children != null) { + for (var child in children!) { + if (child.children == null) { + rects.add(child.rect); + continue; + } + if (child.children!.length > 1) { + for (var grandChild in child.children!) { + rects.add(grandChild.rect); + } + } else { + rects.add(child.rect); } - } else { - rects.add(child.rect); } } - return rects; } } diff --git a/lib/src/replay/element_parsers/element_parsers_const.dart b/lib/src/replay/element_parsers/element_parsers_const.dart index a434eb1..a4397d0 100644 --- a/lib/src/replay/element_parsers/element_parsers_const.dart +++ b/lib/src/replay/element_parsers/element_parsers_const.dart @@ -1,7 +1,7 @@ import 'package:flutter/rendering.dart'; +import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/replay/element_parsers/element_parser.dart'; import 'package:posthog_flutter/src/replay/element_parsers/element_parser_factory.dart'; -import 'package:posthog_flutter/src/posthog_options.dart'; class ElementParsersConst { final ElementParserFactory _factory; diff --git a/lib/src/replay/mask/image_mask_painter.dart b/lib/src/replay/mask/image_mask_painter.dart index c7a8991..2f1fab0 100644 --- a/lib/src/replay/mask/image_mask_painter.dart +++ b/lib/src/replay/mask/image_mask_painter.dart @@ -2,7 +2,6 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; class ImageMaskPainter { - /// Draw the image and apply the black mask over the rects. Future drawMaskedImage(ui.Image image, List rects, double pixelRatio) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); diff --git a/lib/src/replay/mask/posthog_mask_controller.dart b/lib/src/replay/mask/posthog_mask_controller.dart index 5ac5f69..5391830 100644 --- a/lib/src/replay/mask/posthog_mask_controller.dart +++ b/lib/src/replay/mask/posthog_mask_controller.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/posthog_config.dart'; -import 'package:posthog_flutter/src/posthog_options.dart'; import 'package:posthog_flutter/src/replay/element_parsers/element_data.dart'; import 'package:posthog_flutter/src/replay/element_parsers/element_parser.dart'; import 'package:posthog_flutter/src/replay/element_parsers/element_parser_factory.dart'; @@ -29,7 +29,7 @@ class PostHogMaskController { } static final PostHogMaskController instance = - PostHogMaskController._privateConstructor(PostHogConfig().options.sessionReplayConfig!); + PostHogMaskController._privateConstructor(Posthog().config!.postHogSessionReplayConfig); Future?> getCurrentScreenRects() async { final BuildContext? context = containerKey.currentContext; diff --git a/lib/src/replay/posthog_screenshot_widget.dart b/lib/src/replay/posthog_screenshot_widget.dart index 8d4c3a0..16a6748 100644 --- a/lib/src/replay/posthog_screenshot_widget.dart +++ b/lib/src/replay/posthog_screenshot_widget.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:typed_data'; import 'dart:ui' as ui; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:posthog_flutter/src/posthog_config.dart'; +import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/replay/mask/posthog_mask_controller.dart'; import 'change_detector.dart'; @@ -32,9 +32,11 @@ class _PostHogScreenshotWidgetState extends State { @override void initState() { + final options = Posthog().config; + super.initState(); - if (!PostHogConfig().options.enableSessionReplay) { + if (options!.enableSessionReplay == false) { return; } @@ -71,7 +73,6 @@ class _PostHogScreenshotWidgetState extends State { image.dispose(); if (!_sentFullSnapshot) { - await _nativeCommunicator.sendFullSnapshot(pngBytes, id: _wireframeId); _lastImageBytes = pngBytes; _sentFullSnapshot = true; @@ -86,8 +87,9 @@ class _PostHogScreenshotWidgetState extends State { } Duration _getDebounceDuration() { - final options = PostHogConfig().options; - final sessionReplayConfig = options.sessionReplayConfig; + final options = Posthog().config; + + final sessionReplayConfig = options?.postHogSessionReplayConfig; if (Theme.of(context).platform == TargetPlatform.android) { return sessionReplayConfig?.androidDebouncerDelay ?? const Duration(milliseconds: 200); diff --git a/lib/src/replay/screenshot/screenshot_capturer.dart b/lib/src/replay/screenshot/screenshot_capturer.dart index 5dad8e1..b33c366 100644 --- a/lib/src/replay/screenshot/screenshot_capturer.dart +++ b/lib/src/replay/screenshot/screenshot_capturer.dart @@ -1,12 +1,12 @@ import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/rendering.dart'; -import 'package:posthog_flutter/src/posthog_config.dart'; +import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/replay/mask/image_mask_painter.dart'; import 'package:posthog_flutter/src/replay/mask/posthog_mask_controller.dart'; class ScreenshotCapturer { - final PostHogConfig config = PostHogConfig(); + final config = Posthog().config; final ImageMaskPainter _imageMaskPainter = ImageMaskPainter(); ScreenshotCapturer(); @@ -44,9 +44,9 @@ class ScreenshotCapturer { final ui.Image image = await renderObject.toImage(pixelRatio: pixelRatio); - final replayConfig = config.options.sessionReplayConfig; + final replayConfig = config!.postHogSessionReplayConfig; - if (replayConfig!.maskAllTextInputs || replayConfig.maskAllImages) { + if (replayConfig.maskAllTextInputs || replayConfig.maskAllImages) { final screenElementsRects = await PostHogMaskController.instance.getCurrentScreenRects(); if (screenElementsRects == null) { diff --git a/test/posthog_observer_test.dart b/test/posthog_observer_test.dart index c51dd03..00d2e47 100644 --- a/test/posthog_observer_test.dart +++ b/test/posthog_observer_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/posthog_flutter_io.dart'; import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart'; import 'package:posthog_flutter/src/posthog_observer.dart';