fix: RUM Session inflation on prewarmed iOS apps - #3088
fix: RUM Session inflation on prewarmed iOS apps#3088marco-saia-datadog wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
More details
The prewarmed-session state changes preserve the existing command flow: the initial session remains available until the first user event, while expired sessions renew with the correct inactivity precondition and stop the deferred initialization path. Apple-platform execution was unavailable in this sandbox, so confidence is based on the diff, call graph, and successful Swift parsing.
🤖 Datadog Autotest · Commit 7659ec1 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
Nice catch on the root cause 👌 . One general comment on test coverage: the new unit tests exercise RUMApplicationScope in isolation with hand-built DatadogContext/command sequences, which makes it hard to tell whether the exact scenario being fixed is reachable through the real app lifecycle.
Datadog/IntegrationUnitTests/RUM/RUMSessionTimeOutTests.swift already has testGivenBackgroundSession_whenItTimesOut_andNextEventIsTrackedInForeground, which covers a closely related prewarm + timeout + foreground-transition flow using the AppRunner framework from #2299. Could we add a case in that neighborhood that reproduces the specific inflation scenario this PR fixes (SDK init while backgrounded, silent period past the session timeout, then a real event)? That would give us an integration-level regression test alongside the unit tests, and confirm the fix addresses something that actually happens end-to-end rather than only a state reachable by calling RUMApplicationScope directly.
What and why?
Fixes RUM session inflation on prewarmed iOS apps.
PR #2299 (RUM-8372, first shipped in dd-sdk-ios 3.0.0) restricted the
ApplicationLaunchview guard so it can only ever pass onRUMSDKInitCommand, whereas before that PR the same guard could pass on any later command once the app left the background.We have received complaints of orphaned sessions in React Native SDK since the introduction of iOS v3. We believe that the cause is the application launch guard now permanently fails for any launch where the SDK initializes while the app is still backgrounded, and the code marks the app "activated" (
applicationActive = true) before even checking whether the guard passed, the SDK gives up retrying and the session sits with zero views until it hits the 15-minute inactivity timeout and closes. This produces an empty near-zero-duration orphan session whenever no other RUM event arrives in that window.How?
In
RUMApplicationScope.swift:1. Move
applicationActive = trueafter the check instartApplicationLaunchView.private func startApplicationLaunchView(on command: RUMCommand, context: DatadogContext, writer: Writer) { - applicationActive = true let isUserLaunch = context.launchInfo.launchReason == .userLaunch let isPrewarmed = context.launchInfo.launchReason == .prewarming let isBackgroundLaunch = context.launchInfo.launchReason == .backgroundLaunch let isStartedInForeground = command is RUMSDKInitCommand && context.applicationStateHistory.currentState != .background guard isUserLaunch || (isPrewarmed && isStartedInForeground) || (isBackgroundLaunch && isStartedInForeground) else { return } + applicationActive = truePreviously,
applicationActivewas set to true before the guard that decides if the view should start, now it's set only when the guard passes.When the SDK initializes while the app is still in the background (the prewarming case that causes the bug), the guard fails and
applicationActivenow staysfalse, so the SDK keeps retrying on each new command. This means the firststartView(or similar) command still finds Session A alive and keeps using it, instead of Session A expiring with no views.2. Stop the retry once a session actually gets renewed
Since
applicationActivecan now stayfalsefor longer, we need to make sure the retry stops once it's no longer needed. We setapplicationActive = trueat the start ofrefresh()andstartNewSession(), which are the two places that create a new session after the previous one expired or was stopped.3. Fix a side effect: don't recreate the initial session by mistake
There's an older piece of code that creates a session if none exists yet and
applicationActiveisfalse(meant for a case where, in theory, no session was ever created). With change 1,applicationActivecan now stayfalsewell into the app's life, and this old code could accidentally trigger again after a session already expired (e.g. when a session times out on a background/foreground lifecycle event). This would cause two problems: the new session woiuld get the wrong "start reason" (appears as a fresh prewarm launch instead of a session that timed out), and it would trigger a telemetry log meant to catch a bug.That old code path now only runs if no session has ever been created (
didCreateInitialSessionCount == 0). If a session already existed and expired, the correct path (startNewSession()) takes over instead, with the right start reason and no false error log.Validation
We are unable to verify that this fix resolves the prewarming-scenario issues in a sample app, since iOS provides no reliable way to force a genuine prewarming condition on demand. We only rely on unit tests passing for this PR.
Review checklist
make api-surfacewhen adding new APIs