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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/proud-owls-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog_flutter": minor
---

Add a `bootstrap` option to `PostHogConfig` for pre-seeding identity and feature flags before the first `/flags` response. Set `config.bootstrap = PostHogBootstrapConfig(...)` before `setup()` so early events carry a caller-controlled distinct ID and flag reads return your values during cold start. The values are forwarded to the native iOS and Android SDKs and mirror the `bootstrap` option in posthog-js. On Flutter web, configure `bootstrap` in your `posthog.init` call instead.
54 changes: 24 additions & 30 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,45 +41,39 @@ flutter build macos

## Testing with local native SDKs

The native SDK version floors in `posthog_flutter/darwin/posthog_flutter.podspec`,
`posthog_flutter/darwin/posthog_flutter/Package.swift`, and
`posthog_flutter/android/build.gradle` track the next expected native release. To
develop against an unreleased native branch, point the example app at your local
checkout with a gitignored override file — the committed build stays release-clean,
and a source override ignores the version floor.

### iOS

- Update `example/ios/Podfile` to override the `PostHog` pod with your local path:
- Create `example/ios/Podfile.local` (gitignored) pointing at your checkout:

```ruby
target 'Runner' do
use_frameworks!
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))

# add this
pod 'PostHog', :path => File.expand_path('~/posthog-ios')
end
pod 'PostHog', :path => File.expand_path('~/posthog-ios')
```

- Run `cd example/ios && pod install` to install the local pod
- Open iOS simulator
- Run the app with `flutter run`

### Android

In your local `posthog-android` repo:
- Run `cd example/ios && pod install`, then `flutter run`.

- Run `make dryRelease` to build and publish the package to Maven local
The example `Podfile` evaluates `Podfile.local` when present, so no committed file
changes.

In the `posthog-flutter` repo:
### Android

- Update `/android/build.gradle` to add `mavenLocal()` as a repository:
- Create `example/android/local.settings.gradle.kts` (gitignored) with a composite
build of your checkout:

```gradle
allprojects {
repositories {
mavenLocal() // add this
google()
mavenCentral()
}
}
```kotlin
// absolute path — Kotlin does not expand "~"
includeBuild("/absolute/path/to/posthog-android")
```

- Open Android simulator
- Run the app with `flutter run`
- Run `flutter run`.

Gradle substitutes `com.posthog:posthog(-android)` with the local source regardless
of the version floor, so no publishing or `mavenLocal()` is needed. As a fallback you
can still `make dryRelease` in `posthog-android` (add `-PandroidVersion=<floor>` so the
published version satisfies the floor) and add `mavenLocal()` to the repositories.
135 changes: 135 additions & 0 deletions api/posthog_flutter.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2125,6 +2125,17 @@
"relativePath": "lib/src/posthog_config.dart",
"typeName": "PostHogLogsConfig"
},
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isReadable": true,
"isStatic": false,
"isWriteable": true,
"name": "bootstrap",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "PostHogBootstrapConfig?"
},
{
"entryPoints": [],
"isDeprecated": false,
Expand Down Expand Up @@ -2765,6 +2776,130 @@
],
"typeParameterNames": []
},
{
"entryPoints": [
"posthog_flutter.dart"
],
"executableDeclarations": [
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isStatic": false,
"name": "toMap",
"parameters": [],
"relativePath": "lib/src/posthog_config.dart",
"returnTypeName": "Map<String, Object?>",
"type": "method",
"typeParameterNames": []
},
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isStatic": false,
"name": "new",
"parameters": [
{
"isDeprecated": false,
"isExperimental": false,
"isNamed": true,
"isRequired": false,
"name": "distinctId",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "String?"
},
{
"isDeprecated": false,
"isExperimental": false,
"isNamed": true,
"isRequired": false,
"name": "isIdentifiedId",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "bool"
},
{
"isDeprecated": false,
"isExperimental": false,
"isNamed": true,
"isRequired": false,
"name": "featureFlags",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "Map<String, Object>?"
},
{
"isDeprecated": false,
"isExperimental": false,
"isNamed": true,
"isRequired": false,
"name": "featureFlagPayloads",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "Map<String, Object?>?"
}
],
"relativePath": "lib/src/posthog_config.dart",
"returnTypeName": "PostHogBootstrapConfig",
"type": "constructor",
"typeParameterNames": []
}
],
"fieldDeclarations": [
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isReadable": true,
"isStatic": false,
"isWriteable": false,
"name": "distinctId",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "String?"
},
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isReadable": true,
"isStatic": false,
"isWriteable": false,
"name": "isIdentifiedId",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "bool"
},
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isReadable": true,
"isStatic": false,
"isWriteable": false,
"name": "featureFlags",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "Map<String, Object>?"
},
{
"entryPoints": [],
"isDeprecated": false,
"isExperimental": false,
"isReadable": true,
"isStatic": false,
"isWriteable": false,
"name": "featureFlagPayloads",
"relativePath": "lib/src/posthog_config.dart",
"typeName": "Map<String, Object?>?"
}
],
"isDeprecated": false,
"isExperimental": false,
"isRequired": false,
"isSealed": false,
"name": "PostHogBootstrapConfig",
"relativePath": "lib/src/posthog_config.dart",
"superTypeNames": [
"Object"
],
"typeParameterNames": []
},
{
"entryPoints": [
"posthog_flutter.dart"
Expand Down
3 changes: 3 additions & 0 deletions example/android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ gradle-wrapper.jar
/local.properties
GeneratedPluginRegistrant.java

# Local development override for an unreleased posthog-android (see CONTRIBUTING.md).
local.settings.gradle.kts

# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
Expand Down
6 changes: 6 additions & 0 deletions example/android/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ plugins {
}

include(":app")

// Local development against an unreleased posthog-android. Inert unless a
// gitignored local.settings.gradle.kts exists; see CONTRIBUTING.md. The name must
// end in .settings.gradle.kts so Kotlin DSL compiles it against Settings.
val localSettings = file("local.settings.gradle.kts")
if (localSettings.exists()) apply(from = localSettings)
3 changes: 3 additions & 0 deletions example/ios/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*

# Local development override for an unreleased PostHog iOS SDK (see CONTRIBUTING.md).
Podfile.local

# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
Expand Down
6 changes: 6 additions & 0 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ target 'Runner' do
use_modular_headers!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))

# Local development against an unreleased PostHog iOS SDK. Inert unless a
# gitignored Podfile.local exists; see CONTRIBUTING.md.
podfile_local = File.expand_path('Podfile.local', __dir__)
instance_eval(File.read(podfile_local), podfile_local) if File.exist?(podfile_local)

target 'RunnerTests' do
inherit! :search_paths
end
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions posthog_flutter/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ android {
dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.mockito:mockito-core:5.0.0'
// + Version 3.54.1 and the versions up to 4.0.0, not including 4.0.0 and higher
implementation 'com.posthog:posthog-android:[3.54.1,4.0.0]'
implementation 'com.posthog:posthog-android:[3.55.0,4.0.0)'
}

testOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.view.ViewGroup
import androidx.annotation.RequiresApi
import com.posthog.PersonProfiles
import com.posthog.PostHog
import com.posthog.PostHogBootstrapConfig
import com.posthog.PostHogConfig
import com.posthog.PostHogOnFeatureFlags
import com.posthog.android.PostHogAndroid
Expand Down Expand Up @@ -488,6 +489,11 @@ class PosthogFlutterPlugin :
}
}

// Bootstrap precedence and flag layering live in the native SDK; forward values only.
posthogConfig.getIfNotNull<Map<String, Any>>("bootstrap") {
this.bootstrap = bootstrapConfigFromMap(it)
}

sdkName = "posthog-flutter"
sdkVersion = postHogVersion

Expand Down Expand Up @@ -810,7 +816,8 @@ class PosthogFlutterPlugin :
// different platform view (e.g. a masked map) that merely overlaps, so
// compositing it would leak masked content. Slack absorbs rounding.
val tolerance = 8
if (destX < -tolerance || destY < -tolerance ||
if (destX < -tolerance ||
destY < -tolerance ||
destX + svLogW > destBitmap.width + tolerance ||
destY + svLogH > destBitmap.height + tolerance
) {
Expand Down Expand Up @@ -1423,3 +1430,12 @@ class PosthogFlutterPlugin :
flutterSurveysDelegate?.handleSurveyAction(type, args, result)
}
}

@Suppress("UNCHECKED_CAST")
internal fun bootstrapConfigFromMap(bootstrap: Map<String, Any>): PostHogBootstrapConfig =
PostHogBootstrapConfig(
distinctId = bootstrap["distinctId"] as? String,
isIdentifiedId = bootstrap["isIdentifiedId"] as? Boolean ?: false,
featureFlags = bootstrap["featureFlags"] as? Map<String, Any>,
featureFlagPayloads = bootstrap["featureFlagPayloads"] as? Map<String, Any?>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import android.app.Activity
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.mockito.Mockito
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue

/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
Expand Down Expand Up @@ -189,4 +191,49 @@ internal class PosthogFlutterPluginTest {
Mockito.isNull(),
)
}

@Test
fun bootstrapConfigFromMap_fullMap_decodesAllFields() {
val config =
bootstrapConfigFromMap(
mapOf(
"distinctId" to "user-123",
"isIdentifiedId" to true,
"featureFlags" to mapOf("beta-ui" to "variant-a", "legacy" to true),
"featureFlagPayloads" to mapOf("beta-ui" to mapOf("color" to "blue")),
),
)

assertEquals("user-123", config.distinctId)
assertTrue(config.isIdentifiedId)
assertEquals(mapOf("beta-ui" to "variant-a", "legacy" to true), config.featureFlags)
assertEquals(mapOf("beta-ui" to mapOf("color" to "blue")), config.featureFlagPayloads)
}

@Test
fun bootstrapConfigFromMap_emptyMap_usesDefaults() {
val config = bootstrapConfigFromMap(emptyMap())

assertNull(config.distinctId)
assertFalse(config.isIdentifiedId)
assertNull(config.featureFlags)
assertNull(config.featureFlagPayloads)
}

@Test
fun bootstrapConfigFromMap_wrongTypes_fallBackToDefaults() {
val config =
bootstrapConfigFromMap(
mapOf(
"distinctId" to 42,
"isIdentifiedId" to "yes",
"featureFlags" to listOf("beta-ui"),
),
)

assertNull(config.distinctId)
assertFalse(config.isIdentifiedId)
assertNull(config.featureFlags)
assertNull(config.featureFlagPayloads)
}
}
Loading
Loading