feat: wire LifecycleCoordinator + openURL public API [3/4]#32
Conversation
| class DeviceContextProvider(private val context: Context) { | ||
| class DeviceContextProvider( | ||
| private val context: Context, | ||
| private val appContext: AppContext = AppContext.fromContext(context) |
There was a problem hiding this comment.
now reads the shared AppContext
78e1343 to
f30267a
Compare
End-to-end integration of the lifecycle subsystem. After this slice the feature is fully functional behind `InitOptions.trackLifecycleEvents` — default stays false so existing customers are not affected on upgrade. Coordinator seam - LifecycleCoordinator wraps LifecycleEventTracker. MetaRouterAnalytics- Client no longer references the tracker directly; future session / attribution work has a clear place to land alongside lifecycle events. - Constructed only when trackLifecycleEvents=true. When the feature is off, lifecycleCoordinator is null and every dispatch site is a null-safe no-op. AppContext caching - AnalyticsClient reads AppContext.fromContext() exactly once at init and injects the same snapshot into both DeviceContextProvider and LifecycleEventTracker. The per-event enrichment path no longer hits PackageManager. Public API - handleDeepLink renamed to openURL across AnalyticsInterface, AnalyticsProxy, PendingCall, and the proxied call replay. Name matches Segment's iOS SDK and avoids the over-promise of 'handle' (the SDK does not route or parse the URL — it buffers it for the next Application Opened). - openURL on the client logs Logger.warn and no-ops when the feature is disabled. Silent no-op was bad DX; misconfiguration is now diagnosable from logcat. Opt-in default - InitOptions.trackLifecycleEvents flips from true to false. KDoc updated. InitOptionsTest renamed to assert the new default. Lifecycle ordering - onBackground emits Application Backgrounded BEFORE flush / flushToDisk so the event lands in the same drain (unchanged behavior; comment kept next to the code). Refs: sc-38235
Slice 3 follow-ups from code review. - onForeground ordering: dispatcher.resume() now runs BEFORE coordinator.onForeground(). Mirrors iOS so the resumed dispatcher picks up the just-emitted Application Opened in its next tick rather than waiting for the following flush cycle. Functionally benign on Android (track() enqueues regardless of dispatcher state) but the cross-platform parity contract was an explicit constraint. - Coordinator gate: trackLifecycleEvents is now the sole on/off signal. Previously a test seam injection of injectedLifecycleCoordinator could install a coordinator while the flag was false. Production paths were fine via MetaRouter.kt, but the off-state should be structurally enforced regardless of the seam. - AnalyticsInterface.openURL KDoc: drop the misleading Intent.EXTRA_REFERRER + getStringExtra suggestion. Per Android docs EXTRA_REFERRER is a Uri (returns null via getStringExtra). Point hosts at Activity.referrer?.host, which is the canonical API for the calling-app host. - DeviceContextProvider: KDoc on the appContext constructor default noting it exists for test ergonomics only. Production code must pass the explicit cached snapshot. Refs: sc-38235
382bcd4 to
4e7a426
Compare
| // every consumer (DeviceContextProvider for per-event enrichment, the | ||
| // lifecycle tracker for install/update detection). Bundle/PackageInfo is | ||
| // OS-loaded and immutable post-process-start, so caching is safe. | ||
| val appContext = injectedAppContext ?: AppContext.fromContext(context) |
There was a problem hiding this comment.
allows for DI for testing
| * @param uri The deep-link URI that opened the app | ||
| * @param sourceApplication Optional referring application identifier | ||
| */ | ||
| fun openURL(uri: Uri, sourceApplication: String? = null) |
There was a problem hiding this comment.
nit/readme: Deep-link vs. cold-launch timing: if the host calls openURL from MainActivity.onCreate while the SDK is still initializing, the call is queued in the proxy and replays after bind by which time the cold-launch Opened has already fired. The deep link then attaches to the next Opened (or never, if the app is killed first). Small real-world nuance the README can call out.
There was a problem hiding this comment.
Confirmed — onReady() (which emits the cold-launch Opened) runs inside client init, before proxy.bind replays queued calls, so an openURL issued mid-init attaches to the next Opened at best. Added a "Cold-launch timing" paragraph to the openURL KDoc in 8e3a3cd; the README slice (sc-38236) will carry the integrator-facing version.
| delay(100) | ||
| val info = client.getDebugInfo() | ||
| assertEquals(0, info["queueLength"]) |
There was a problem hiding this comment.
nit: awaitCondition above seems better than static delay here. Is this designed like this for a reason?
Review feedback on PR #32: - Document that an openURL call made while the SDK is still initializing replays after the cold-launch Application Opened has fired, so the deep link attaches to the next Opened (or is lost with the process). README slice (sc-38236) will restate for integrators. - Replace the static delay(100) in the lifecycle-disabled test with a sentinel event + awaitCondition. The channel is single-consumer FIFO, so asserting queueLength == 1 after the sentinel lands proves no lifecycle event was emitted during init. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shortcut: sc-38235
Parent: sc-36799
iOS reference: sc-38230
Slice 3 of 4 — first slice in the stack where the feature actually emits events end-to-end.
Summary
End-to-end integration of the lifecycle subsystem behind
InitOptions.trackLifecycleEvents(defaultfalse— opt-in). After this slice the feature is fully functional; existing customers are not affected on upgrade.Coordinator seam
LifecycleCoordinatorwrapsLifecycleEventTracker.MetaRouterAnalyticsClientno longer references the tracker directly — future session / attribution work has a clean place to land alongside lifecycle.trackLifecycleEvents == true. When disabled,lifecycleCoordinator == nulland every dispatch site is a null-safe no-op.Public API
openURLacrossAnalyticsInterface,AnalyticsProxy,PendingCall, and the proxied call replay. Name matches Segment's iOS SDK and avoids the "handle" over-promise (the SDK doesn't route or parse the URL — it buffers it for the nextApplication Opened).openURLon the client logsLogger.warnand no-ops when the feature is disabled. Silent no-op was bad DX; misconfig is now diagnosable from logcat.AppContextcachingAppContext.fromContext()exactly once at init and injects the same snapshot into bothDeviceContextProviderandLifecycleEventTracker. Per-event enrichment no longer hitsPackageManager.Opt-in default
InitOptions.trackLifecycleEventsflipstrue→false. KDoc updated.InitOptionsTestrenamed to assert the new default.Lifecycle ordering
onBackgroundemitsApplication Backgroundedbeforeflush/flushToDiskso the event lands in the same drain (unchanged behavior; comment kept next to the code).Stack
LifecycleEventTrackeralgorithm + testsopenURLpublic API