Skip to content

feat(rum): report app launch (TTID) on Android - #12

Merged
Fiona2016 merged 4 commits into
feat/refresh-rate-on-publishfrom
feat/rum-app-launch-on-publish
Jul 28, 2026
Merged

feat(rum): report app launch (TTID) on Android#12
Fiona2016 merged 4 commits into
feat/refresh-rate-on-publishfrom
feat/rum-app-launch-on-publish

Conversation

@Fiona2016

@Fiona2016 Fiona2016 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

What

Flutter never reported an app launch on Android. RumAppStartupDetector registers its Activity lifecycle callbacks when the RUM feature initializes, and Flutter initializes the SDK from Dart main() — by then the first Activity has already been created, so the detector never sees the launch.

The launch is now reported from the plugin, on the launch frame.

How

  • The plugin asks on every Android launch and the native SDK decides. Whether the native detector saw the launch is not something a host can tell: initializing the native SDK before Flutter attaches does not mean it initialized early enough to observe the first Activity, so an early version of this PR — which only asked when Flutter owned the initialization — lost the launch entirely on hosts that initialize late. notifyAppLaunchIfAbsent (feat(rum): let cross-platform hosts report an app launch the native detector misses fc-sdk-android#23) arbitrates instead: the detector wins whenever it fired, since it always fires from the first Activity's pre-create, before any frame a host could render. It also de-duplicates several engines in one process, so the plugin keeps no latch of its own.
  • DatadogSdkPlugin is now ActivityAware and stamps the UI creation time from onAttachedToActivity. Engine attach is deliberately not used: a pre-warmed or headless engine attaches with no UI, which would place the launch start far too early. An engine that never gets an Activity reports no UI creation time, and the native SDK falls back to timing from process start. The stamp uses AtomicLong.compareAndSet so two engines attaching concurrently cannot race.
  • Dart passes how long ago the launch frame finished rasterizing, so neither the callback scheduling nor the method channel round trip is counted as launch time. Measured at ~61ms on a release build. Timeline.now shares its clock with FrameTiming, which is what makes the subtraction meaningful; an implausible reading falls back to 0.
  • Nothing is reported when the first frame is already on screen when RUM is enabled. addTimingsCallback only delivers later frames, so the next one is not the launch frame — and a static page may never render another at all. Reporting nothing beats reporting something wrong.
  • Registration tolerates a missing binding, since enable() is reachable from plain Dart contexts (unit tests, background isolates) where touching SchedulerBinding.instance throws rather than returning null.

iOS measures app launch natively and is unchanged (no-op).

Also fixed here

  • attachToExisting reports a refresh rate again. It passed a null vital frequency, so the callback was never registered even with reportFlutterPerformance on — a regression against the behaviour before build and refresh-rate timings were split apart. DatadogAttachConfiguration now carries vitalUpdateFrequency, defaulting to VitalsFrequency.average.
  • One timings callback instead of two. Performance and refresh-rate reporting stay independently configurable but now share a single SchedulerBinding subscription and send one platform message per frame batch.

Depends on

Testing

  • Dart suite: 260 tests pass.
  • Plugin Kotlin suite: 74 tests pass (run against a locally published 0.5.0; this caught a leftover reference to the removed plugin latch that no Dart test or flutter analyze could reach).
  • flutter analyze lib clean.
  • Verified end to end against a local RUM intake and the dev Doris cluster: startup_type=cold_start, app_launch_metric=ttid, durations 0.6s–2.1s, landing in t_vitals. Cross-checked against the system's own Fully drawn figure for the same launches.

Known and intentional: a hot start (process alive, Activity alive, returning from background) reports nothing — tracked separately.

🤖 Generated with Claude Code

Fiona2016 and others added 4 commits July 27, 2026 04:10
The native RumAppStartupDetector registers its Activity lifecycle callbacks when
the RUM feature initializes. Flutter initializes the SDK from Dart main(), by
which time the first Activity has already been created, so the detector never
sees a launch and none is ever reported.

Report it from the plugin instead, on the launch frame:

- DatadogSdkPlugin is now ActivityAware and stamps the UI creation time from
  onAttachedToActivity. Engine attach is not used: a pre-warmed or headless
  engine attaches with no UI, which would place the launch start far too early.
  An engine that never gets an Activity reports no UI creation time, and the SDK
  falls back to timing from process start.
- Dart subscribes a one-shot timings callback and passes how long ago the launch
  frame finished rasterizing, so neither the callback scheduling nor the method
  channel round trip is counted as launch time. Measured at ~61ms on a release
  build, which is no longer charged to the launch.
- If the first frame is already on screen when RUM is enabled, no launch is
  reported at all: the next frame is not the launch frame, and a static page may
  never render one.
- Registration tolerates a missing binding, since enable() is reachable from
  plain Dart contexts where touching SchedulerBinding.instance throws.

Requires cloud.flashcat:dd-sdk-android-rum 0.5.0 for the notifyAppLaunch entry
point.

Verified end to end against a local RUM intake: startup_type=cold_start,
app_launch_metric=ttid, durations 0.6s-2.1s, landing in Doris.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…resh rate on attach

Three problems with the first cut, all of them cases where a launch or a vital
silently went missing.

Gating the app-launch request on Flutter-owned initialization lost the launch
entirely on hosts that attach to a natively initialized SDK. Initializing the
native SDK before Flutter attaches does not mean it initialized early enough for
the native detector to observe the first Activity, so on a host that initializes
late neither side reported anything. The request is now unconditional on Android
and the native SDK arbitrates through notifyAppLaunchIfAbsent, which is the only
side that knows whether its own detector saw the launch. The plugin's own
de-duplication is gone with it - one latch, in one place.

attachToExisting also stopped reporting a refresh rate: it passed a null vital
frequency, so the callback was never registered even with reportFlutterPerformance
on. That was a regression against the behaviour before build and refresh-rate
timings were split apart. DatadogAttachConfiguration now carries
vitalUpdateFrequency, defaulting to average.

Finally, performance and refresh-rate reporting each registered their own timings
callback and each sent its own platform message. They stay independently
configurable but now share one subscription and one message per frame batch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… exist

VitalsFrequency has frequent, average and rare - there is no never - so the
dartdoc link was broken and the advice unfollowable. Assigning null is what
actually disables the collection, matching how DatadogRumConfiguration documents
the same field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Fiona2016
Fiona2016 marked this pull request as ready for review July 28, 2026 12:40
@Fiona2016
Fiona2016 merged commit 60dfb07 into feat/refresh-rate-on-publish Jul 28, 2026
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.

1 participant