Skip to content

RUM-17009: Preserve application attribution without an active RUM session - #3970

Open
kikoveiga wants to merge 1 commit into
developfrom
kikoveiga/RUM-17009/preserve-rum-application-id
Open

kikoveiga wants to merge 1 commit into
developfrom
kikoveiga/RUM-17009/preserve-rum-application-id

Conversation

@kikoveiga

@kikoveiga kikoveiga commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Keep application identity available independently of the RUM session lifecycle. While RUM is enabled, the monitor always produces a RumContext with a required applicationId; sessionId and session metadata are nullable. Having no session means an application-only context, not a missing RUM context.

  • Publish only application_id when no session exists, clearing stale session, view and action correlation. Failed event processing also falls back to application-only context.
  • Give telemetry the configured application ID directly and omit absent sessions. Normalize legacy zero-UUID session values when reading shared context; session-dependent events and metrics require a real session ID.
  • Keep native logs and WebView logs attributed to the application without retaining conflicting session IDs. WebView RUM/replay events require session context; replay uploads reject records with missing or invalid application/session/view IDs.
  • Deliver context snapshots to feature receivers so later updates cannot mutate their previously received context.

Motivation

RUM-17009: Android PR #3304 cleared the entire RUM context to prevent zero-UUID correlation in logs and traces. That also removed application attribution when sessions ended. This PR preserves that correlation safeguard while retaining application identity.

The telemetry approach follows iOS PR #3095: application identity comes from configuration, independently of session state. Android additionally refactors its internal context to represent an optional session; the iOS PR leaves its shared context model unchanged.

Additional Notes

No public API changes. NULL_UUID remains for compatibility and required schema fallbacks, but no longer represents an absent session in the internal RUM context.

Local validation: the context refactor passed 13,260 unit tests (one assumption-based skip), all 74 instrumented integration tests, formatting, Detekt and generated-file checks. The subsequent WebView correlation fix passed all 194 WebView tests, formatting and Detekt; its regression test was verified to fail before the fix.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@datadog-prod-us1-6

datadog-prod-us1-6 Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Tests

✅ All CI checks and tests passed. Datadog automation helped this PR pass.

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog retried 1 test - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
• Patch Coverage: 80.23%
• Overall Coverage: 71.76% (-0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c29b60b | Docs | View more details | Give us feedback!

@kikoveiga
kikoveiga force-pushed the kikoveiga/RUM-17009/preserve-rum-application-id branch 7 times, most recently from 7166051 to 83930b2 Compare October 14, 2026 13:24
@kikoveiga

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T14:05:53.441871Z c29b60b Draft marked ready
🔒 Security Review ✅ Completed 2026-10-14T14:06:19.817913Z c29b60b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 83930b2f31

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83930b2f31

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kikoveiga
kikoveiga force-pushed the kikoveiga/RUM-17009/preserve-rum-application-id branch from 83930b2 to c29b60b Compare October 14, 2026 13:45
@kikoveiga
kikoveiga marked this pull request as ready for review October 14, 2026 14:01
@kikoveiga
kikoveiga requested review from a team as code owners October 14, 2026 14:01

@datadog-prod-us1-6 datadog-prod-us1-6 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.

Bits Code Review: FAIL

Resources that finish before the first RUM session skip completion bookkeeping, leaving pending counters positive and preventing stopped view/session scopes from completing. Both successful-resource and resource-error paths need the dropped-event notification.

Open Bits AI session

🤖 Bits Code Review · Commit c29b60b

val rumContext = getRumContext()
val sessionId = rumContext.sessionId
if (sessionId == null) {
sent = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Balance pending counters when dropping sessionless resources

When a startup resource completes before the first RUM session, this return removes its scope without calling eventDropped(), leaving pendingResourceCount positive. A subsequent stopSession() cannot complete the stopped view/session scopes, retaining them until renewal. The equivalent guard in sendError() leaves pendingErrorCount positive after resource-to-error conversion. Previously, the no-op write invoked the dropped-event callback and balanced these counters. Both sessionless paths must preserve that notification and cleanup.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

Comment on lines +252 to +253
@Suppress("UnsafeThirdPartyFunctionCall") // currentContext is non-null
val contextSnapshot = HashMap(currentContext)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is it just for an additional guard? onContextUpdate argument for the context is of Map type, so it is not mutable unless receiver tries to cast to the mutable map explicitly (unlikely, I think).

as a price to pay - extra object allocation and iteration to copy.

}

@ParameterizedTest
@CsvSource("false,false", "true,false", "true,true")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure we need parametrized tests with boolean inputs. Maybe just a single test with forged values?

If we need to extend the coverage, we can use RepeatedTest

Comment on lines -15 to +23
val applicationId: String = NULL_UUID,
val sessionId: String = NULL_UUID,
val applicationId: String,
val sessionId: String? = null,
val isSessionActive: Boolean = false,
val viewId: String? = null,
val viewName: String? = null,
val viewUrl: String? = null,
val actionId: String? = null,
val sessionState: RumSessionScope.State = RumSessionScope.State.NOT_TRACKED,
val sessionStartReason: RumSessionScope.StartReason = RumSessionScope.StartReason.USER_APP_LAUNCH,
val sessionState: RumSessionScope.State? = null,
val sessionStartReason: RumSessionScope.StartReason? = null,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what is the benefit of dropping NULL_UUID and RumSessionScope.State.NOT_TRACKED here?

maybe we can extract RumContext into dd-sdk-android-internal, so that all modules can access it directly and use properties/methods specific to this class instead of relying on the generic types (e.g. strings, nulls)?

Comment on lines +242 to +246
val sessionId = rumContext.sessionId
if (sessionId == null) {
sent = true
return
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it seems this change is not related and should be (if valid) in a standalone PR?

val rulePsr = resourceAttributes.remove(RumAttributes.RULE_PSR) as? Number

val rumContext = getRumContext()
val sessionId = rumContext.sessionId

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here

Comment on lines +384 to +387
it["application_id"]?.let { value -> combinedAttributes[LogAttributes.RUM_APPLICATION_ID] = value }
it["session_id"]?.let { value -> combinedAttributes[LogAttributes.RUM_SESSION_ID] = value }
it["view_id"]?.let { value -> combinedAttributes[LogAttributes.RUM_VIEW_ID] = value }
it["action_id"]?.let { value -> combinedAttributes[LogAttributes.RUM_ACTION_ID] = value }

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.

Maybe also worth to extract to the constants?

if (it.syntheticsTestId != null) {
logSynthetics("_dd.application.id", it.applicationId)
logSynthetics("_dd.session.id", it.sessionId)
it.sessionId?.let { id -> logSynthetics("_dd.session.id", id) }

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.

Don't we have something broken after this change? looks like synthetics attributes should always be bound to the session?

@Suppress("ReturnCount")
fun fromFeatureContext(featureContext: Map<String, Any?>): RumContext? {
val applicationId = featureContext[APPLICATION_ID] as? String ?: return null
val sessionId = (featureContext[SESSION_ID] as? String)?.takeUnless { it.isBlank() || it == NULL_UUID }

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.

nit: putting every ?: on a new line makes it bit more readable:

 val sessionId = (featureContext[SESSION_ID] as? String)
        ?.takeUnless { it.isBlank() || it == NULL_UUID }
        ?: return RumContext(applicationId = applicationId)

it.clear()
rumContext?.toMap()?.let(it::putAll)
}
publishRumContext(rumContext.toMap())

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.

this might be too much for GC.
handleEvent executed for the every event, and this change gonna create new Map object every time.
Previous version was submitting runnable to the executor and only creates map there. Not sure if it's really affects the performance, but worth to check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants