Skip to content

feat(config): prefer autocapture over defaultTracking for mobile#305

Merged
chungdaniel merged 8 commits into
mainfrom
feat/autocapture-mobile-config
Jun 9, 2026
Merged

feat(config): prefer autocapture over defaultTracking for mobile#305
chungdaniel merged 8 commits into
mainfrom
feat/autocapture-mobile-config

Conversation

@chungdaniel

@chungdaniel chungdaniel commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates the Flutter SDK so Configuration.autocapture is the canonical autocapture configuration on mobile, matching the native iOS and Android SDKs which deprecated defaultTracking some time ago (Amplitude-Swift Configuration.swift, Amplitude-Kotlin Configuration.kt). The Flutter SDK was the only one still routing mobile config through defaultTracking.

  • AutocaptureOptions now exposes appLifecycles and deepLinks (mobile). AutocaptureEnabled sets them too so the "enable everything" sentinel keeps its semantic.
  • Configuration constructor's autocapture parameter is now nullable. When omitted, an AutocaptureOptions is derived from the (deprecated) defaultTracking field, folding both mobile fields (sessions, appLifecycles, deepLinks) and web fields that exist on both classes (attribution, pageViews). This is the load-bearing piece: it preserves end-to-end behavior for existing defaultTracking callers without requiring any code change on their side.
  • Native plugins (Android AmplitudeFlutterPlugin.kt, iOS SwiftAmplitudeFlutterPlugin.swift) now read only the autocapture map and translate it directly to the native AutocaptureOption set / AutocaptureOptions option set. No merge or fallback logic in native — by the time the map reaches native, Dart has already resolved it.
  • DefaultTrackingOptions class, Configuration.defaultTracking field, and the constructor parameter are all @Deprecated with a migration message pointing to autocapture.
  • iOS deepLinks is intentionally not translated — iOS handles deep links outside the native autocapture option set, matching pre-existing behavior on Amplitude-Swift.

Why this approach

A native-side merge (each plugin reads both autocapture and defaultTracking and resolves) was the first attempt but duplicates the resolution logic across two platforms and leaves Configuration.autocapture showing something different from what platforms actually use. Dart-side derivation keeps a single source of truth, makes the native plugins dumb translators, and matches the didSet pattern Amplitude-Swift already uses.

It also sets up planned Flutter frustration tracking cleanly: that flag will live on AutocaptureOptions but native plugins will explicitly skip it during translation (native frustration tracking only sees FlutterView and would emit useless events), and that "skip" lives in one place per platform with no other config to disentangle.

Backward compatibility

Existing callers using defaultTracking: DefaultTrackingOptions(...) see no behavior change. The derivation in the constructor folds their values into the autocapture map the native plugin now reads. They'll see one new deprecation warning from flutter analyze and a migration message pointing them at autocapture.

Caller Before After
Configuration(defaultTracking: DTO(appLifecycles: true)) Mobile reads defaultTracking{SESSIONS, APP_LIFECYCLES} Mobile reads derived autocapturesame {SESSIONS, APP_LIFECYCLES}
Configuration(autocapture: AutocaptureOptions(appLifecycles: true)) Mobile ignored autocapture; got {SESSIONS} from defaultTracking default Mobile reads autocapture → {SESSIONS, APP_LIFECYCLES} (now respected)
Configuration(apiKey: 'k') (neither) {SESSIONS} from defaultTracking defaults Derived autocapture has sessions: truesame {SESSIONS}
Both passed Mobile silently used only defaultTracking autocapture wins; defaultTracking ignored. New behavior, documented.

Test plan

  • flutter test — all unit tests green (45 tests including 6 new derivation tests)
  • flutter analyze — clean (only a pre-existing untracked-file issue in scratch)
  • iOS sim manual verification — existing defaultTracking config still emits [Amplitude] Application Started / Session events
  • iOS sim manual verification — new autocapture config emits same events
  • iOS sim manual verification — AutocaptureEnabled() enables mobile autocapture
  • iOS sim manual verification — AutocaptureDisabled() produces zero [Amplitude] events
  • Android emulator manual verification — all four scenarios above
  • Android emulator — deep link via adb shell am start ... fires [Amplitude] Deep Link Opened when enabled
  • Both passed (autocapture + defaultTracking) — autocapture wins (confirms derivation contract)
  • Dashboard sanity — events from each scenario arrive correctly and look identical between iOS/Android

Notes for reviewers

  • The single // ignore_for_file: deprecated_member_use_from_same_package at the top of lib/configuration.dart, test/configuration_test.dart, and test/default_tracking_test.dart is intentional — those files exist to maintain the deprecation bridge and verifying it works. Scattered per-line ignores would be noisier without adding signal.
  • example/pubspec.lock is removed in a separate chore: commit — it was tracked despite example/pubspec.lock already being in .gitignore:21.
  • Kotlin/Swift native tests for autocapture translation weren't added: the existing AmplitudeFlutterPluginTest.kt doesn't run via CI (flutter test only covers Dart), and the standalone ./gradlew test build can't resolve io.flutter.*. Worth revisiting when Android tests start running through Flutter tooling.

🤖 Generated with Claude Code


Note

Medium Risk
Changes which autocapture flags reach native SDKs and introduces explicit autocapture vs defaultTracking precedence when both are set; behavior for legacy defaultTracking-only callers is intended to stay the same via Dart derivation.

Overview
Makes Configuration.autocapture the single path for mobile default/autocapture behavior, aligned with native iOS/Android SDKs. AutocaptureOptions / AutocaptureEnabled now include appLifecycles and deepLinks in their serialized maps.

Dart resolves effective autocapture in Configuration._resolveAutocapture: explicit autocapture wins; otherwise values are derived from deprecated defaultTracking (sessions, mobile flags, and web attribution / pageViews). defaultTracking is @Deprecated but still emitted in toMap() for compatibility.

Android and iOS plugins stop reading defaultTracking and map only the autocapture argument (false → disabled; map → native option sets). iOS still does not map deepLinks from the map (unchanged platform behavior).

The example app initializes with AutocaptureEnabled() instead of DefaultTrackingOptions.all(). Unit tests cover derivation and mobile autocapture serialization.

Reviewed by Cursor Bugbot for commit 87674ff. Bugbot is set up for automated code reviews on this repo. Configure here.

Add appLifecycles and deepLinks to AutocaptureOptions and AutocaptureEnabled
so mobile autocapture can be configured via the canonical autocapture API
(matching Amplitude-Swift and Amplitude-Kotlin, which have already deprecated
defaultTracking in favor of autocapture).

This commit only widens the Dart surface — the AutocaptureOptions field is
serialized via Configuration.toMap() but native plugins don't yet read it.
That wiring follows in subsequent commits.

- AutocaptureOptions.appLifecycles: bool (default false)
- AutocaptureOptions.deepLinks: bool (default false, Android-only on the
  iOS side since iOS doesn't expose .deepLinks as an AutocaptureOption)
- AutocaptureEnabled also sets both true so the 'enable everything' sentinel
  keeps that semantic
- Class doc updated; sessions re-labeled 'Cross-platform'
- Tests for defaults, toMap, and AutocaptureEnabled
…itly set

Make Configuration's autocapture constructor parameter nullable. When null,
derive an AutocaptureOptions from the (deprecated) defaultTracking. The
field itself stays non-null, so callers continue to read a real Autocapture
value.

This lets us treat 'autocapture' as the single source of truth on the Dart
side. Native plugins read only the resolved autocapture map; they don't
need to know about defaultTracking. defaultTracking can be safely deprecated
without breaking existing callers — their values flow into the autocapture
map automatically.

Mirrors Amplitude-Swift's pattern (defaultTracking.didSet writes through to
autocapture).

Behavior:
- Explicit autocapture wins over defaultTracking (documented).
- AutocaptureDisabled/Enabled sentinels are preserved (not overridden).
- Derived AutocaptureOptions folds: sessions, appLifecycles, deepLinks
  (mobile) and attribution, pageViews (web). formInteractions and
  fileDownloads stay on the deprecated defaultTracking path — they have
  no AutocaptureOptions equivalent yet.
Replace the defaultTracking reading block with a direct autocapture map
translation. By the time the map reaches native, the Dart Configuration
constructor (see commit 8cb5931) has already resolved the effective
autocapture from defaultTracking, so no merge or fallback logic is needed
here.

frustrationInteractions is intentionally NOT translated to a native
AutocaptureOption — it is a Dart-only flag. Native FRUSTRATION_INTERACTIONS
only sees FlutterView and would emit useless rage events for Flutter apps.

screenViews is similarly not translated — screen view tracking is
implemented in Flutter, not the native SDK.

Manual verification: build the example app with
  autocapture: AutocaptureOptions(appLifecycles: true, deepLinks: true)
and confirm [Amplitude] Application Started events fire on Android. For
backward-compat, building with only defaultTracking should produce the
same behavior (Task A2 derivation does the legwork).
Replace the defaultTracking reading block with a direct autocapture map
translation. By the time the map reaches native, the Dart Configuration
constructor (commit 8cb5931) has already resolved the effective autocapture
from defaultTracking, so no merge or fallback logic is needed here.

iOS's native AutocaptureOptions option set does NOT include .deepLinks
(iOS handles deep links outside the autocapture set), so the deepLinks
key in the autocapture map is silently ignored on iOS. This matches the
pre-existing iOS behavior — defaultTracking on Amplitude-Swift also has
no deepLinks field.

frustrationInteractions is intentionally NOT translated — Dart-only flag.
screenViews is similarly not translated — implemented in Flutter.

Manual verification: build the example app on iOS with
  autocapture: AutocaptureOptions(appLifecycles: true)
and confirm [Amplitude] Application Started events fire. For backward
compat, building with only defaultTracking should produce the same
behavior (Task A2 derivation does the legwork).
…eOptions

Mark the DefaultTrackingOptions class, Configuration.defaultTracking field,
and Configuration's defaultTracking constructor parameter @deprecated with a
migration message pointing to autocapture / AutocaptureOptions. This matches
the native SDKs (Amplitude-Swift Configuration.swift:70-76 and
Amplitude-Kotlin Configuration.kt:171), which have already done the same.

Existing callers passing defaultTracking continue to work unchanged — the
Configuration constructor's autocapture resolver (commit 8cb5931) folds
their values into the derived AutocaptureOptions, so behavior is preserved.
The deprecation warning is the migration nudge.

Files that intentionally maintain the deprecation bridge get a single
'ignore_for_file: deprecated_member_use_from_same_package' header rather
than scattered per-line ignores — they exist to support the deprecated
API, and noisy warnings inside them add no signal.

Switched the example app to autocapture: AutocaptureEnabled() so the
example uses the new canonical API.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: AutocaptureDisabled silently falls back to platform defaults
    • Native Android and Darwin parsing now treats a boolean false autocapture value as an explicit empty autocapture option set instead of falling back to defaults.

Create PR

Or push these changes by commenting:

@cursor push 3ffc9b6406
Preview (3ffc9b6406)
diff --git a/android/src/main/kotlin/com/amplitude/amplitude_flutter/AmplitudeFlutterPlugin.kt b/android/src/main/kotlin/com/amplitude/amplitude_flutter/AmplitudeFlutterPlugin.kt
--- a/android/src/main/kotlin/com/amplitude/amplitude_flutter/AmplitudeFlutterPlugin.kt
+++ b/android/src/main/kotlin/com/amplitude/amplitude_flutter/AmplitudeFlutterPlugin.kt
@@ -237,15 +237,18 @@
         call.argument<String>("serverUrl")?.let { builder.serverUrl = it }
         call.argument<Int>("minTimeBetweenSessionsMillis")
             ?.let { builder.minTimeBetweenSessionsMillis = it.toLong() }
-        call.argument<Map<String, Any>>("autocapture")?.let { map ->
-            // The Dart Configuration constructor already resolved the effective
-            // autocapture map (deriving it from defaultTracking when not set
-            // explicitly), so we just translate the map to a native
-            // AutocaptureOption set.
-            builder.autocapture = buildSet {
-                if (map["sessions"] == true) add(AutocaptureOption.SESSIONS)
-                if (map["appLifecycles"] == true) add(AutocaptureOption.APP_LIFECYCLES)
-                if (map["deepLinks"] == true) add(AutocaptureOption.DEEP_LINKS)
+        when (val autocapture = call.argument<Any>("autocapture")) {
+            false -> builder.autocapture = emptySet()
+            is Map<*, *> -> {
+                // The Dart Configuration constructor already resolved the effective
+                // autocapture map (deriving it from defaultTracking when not set
+                // explicitly), so we just translate the map to a native
+                // AutocaptureOption set.
+                builder.autocapture = buildSet {
+                    if (autocapture["sessions"] == true) add(AutocaptureOption.SESSIONS)
+                    if (autocapture["appLifecycles"] == true) add(AutocaptureOption.APP_LIFECYCLES)
+                    if (autocapture["deepLinks"] == true) add(AutocaptureOption.DEEP_LINKS)
+                }
             }
         }
         call.argument<Map<String, Any>>("trackingOptions")?.let { map ->

diff --git a/darwin/amplitude_flutter/Sources/amplitude_flutter/SwiftAmplitudeFlutterPlugin.swift b/darwin/amplitude_flutter/Sources/amplitude_flutter/SwiftAmplitudeFlutterPlugin.swift
--- a/darwin/amplitude_flutter/Sources/amplitude_flutter/SwiftAmplitudeFlutterPlugin.swift
+++ b/darwin/amplitude_flutter/Sources/amplitude_flutter/SwiftAmplitudeFlutterPlugin.swift
@@ -192,10 +192,13 @@
         let migrateLegacyData = args["migrateLegacyData"] as? Bool ?? true
 
         // The Dart Configuration constructor already resolved the effective
-        // autocapture map (deriving it from defaultTracking when not set
-        // explicitly), so we just translate the map to a native
-        // AutocaptureOptions option set.
+        // autocapture value (deriving it from defaultTracking when not set
+        // explicitly), so we just translate it to a native AutocaptureOptions
+        // option set.
         let autocaptureOptions: AutocaptureOptions = {
+            if (args["autocapture"] as? Bool) == false {
+                return []
+            }
             guard let map = args["autocapture"] as? [String: Any] else {
                 return Configuration.Defaults.autocaptureOptions
             }

You can send follow-ups to the cloud agent here.

AutocaptureDisabled() serializes to `false`, but both the Android and
iOS plugins only matched on Map and dropped `false` into the "no
autocapture key" branch, which fell back to native SDK defaults instead
of disabling autocapture.

Match on `false` explicitly and translate it to an empty option set so
the three cases (disabled, options map, absent) line up across Dart,
iOS, and Android.
@chungdaniel

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0607b56. Configure here.

Comment thread lib/configuration.dart Outdated
Comment thread example/pubspec.lock Outdated

@aliaksandr-kazarez aliaksandr-kazarez left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM;

…nullable

Address PR review nit: dt no longer needs to be derived from a nullable.
Move the defaultTracking defaulting to the call site so the helper takes a
non-null DefaultTrackingOptions and the inner null-fallback (and the local
'dt') go away. The const DefaultTrackingOptions() default appears in the
initializer twice but it's const-canonicalized — zero cost.

No behavior change.
@chungdaniel chungdaniel merged commit ff33eab into main Jun 9, 2026
6 checks passed
@chungdaniel chungdaniel deleted the feat/autocapture-mobile-config branch June 9, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants