[common] Wire onboarding recorder into the :auth WebView host (brokered), AB#3708195, Fixes AB#3708195 - #3204
Conversation
|
✅ Work item link check complete. Description contains link AB#3708195 to an Azure Boards work item. |
|
Follow-up noted from the #3201 round-2 review: Raising it here rather than on #3201 because the class is added by this PR, not that one.
This is not a regression and does not block the stack — flagging it because "process-static + holds a recorder reference" is the shape where an untested lifecycle bites later, and the registry is the one piece of this feature with no direct coverage. I'd like to add these before the stack merges. |
|
Fixed in 8d0a325. All three cases I listed are now pinned by The self-review also turned up two things beyond the missing tests: The map was unbounded. The "entries MUST be unregistered" contract was enforced only by convention, in a process that lives as long as the device is up — so any terminal path that missed Logging dropped the correlationId. Both the registry and Both behaviour changes are revert-tested rather than assumed:
178 tests green in this area (WebViewClient 93, bridge 41, recorder 18, registry 12, payload 10, correlation store 4). The full |
a0a5f62 to
e797c59
Compare
…ed), AB#3708195 The broker builds an onboarding telemetry recorder per request from the seed, but the WebView that renders the interactive auth / remediation pages lives in a separate AuthorizationActivity created by the OS from an Intent. A live recorder cannot ride an Intent, so the WebView-side onboarding hooks were inert for brokered flows: steps observed in the WebView (MDM enrollment, Company Portal launch, broker install), the last loaded domain, and the Auth UX log_telemetry error code never reached the blob the broker finalizes and returns. Both components run in the broker :auth process, so this adds an in-process handoff keyed by the request correlationId. OnboardingRecorderRegistry (new) - Rejects DiagnosticContext.UNSET_CORRELATION_ID as a key. Every thread whose request context was never set carries that sentinel, and the authorization Intent extra is populated by reading the request-context map directly rather than through getThreadCorrelationId(), so the raw sentinel can reach the registry. It is shared by definition: accepting it would let two unrelated requests resolve to the same recorder and merge one flow's blocking errors into the other's uploaded blob. Rejecting it makes the feature inert (and logs a warning) instead of silently mis-attributing telemetry, which for a component whose whole purpose is correct attribution is the only acceptable failure. - Bounded, access-ordered map (cap 16). Entries must be unregistered on terminal outcome, and because this is process-static state in a process that lives as long as the device is up, a missed unregister would otherwise leak a recorder permanently - the recorder object and its collected steps / blocking errors, not an Activity, since OnboardingTelemetryRecorder holds only the application context. On overflow it evicts the least-recently-used entry and logs a warning, bounding the leak while surfacing the underlying bug. - Stores the concrete OnboardingTelemetryRecorder because the WebView client's hooks (e.g. setLastLoadedDomain) use methods not on the common4j interface. WebViewAuthorizationFragment - Resolves the recorder in onCreateView and attaches it to the WebView client before initializeAuthUxJavaScriptApi(...). No-op when the request seeded no recorder, which is the MSAL-client path. - Reads the correlation id from its own state bundle in extractState, alongside every other request field, rather than from activity.getIntent(). This fragment supports being hosted by an activity it does not own, whose Intent would not carry the extra. The id is also added to onSaveInstanceState: the Intent survives activity recreation but the bundle did not carry it, so reading only from the bundle would otherwise have silently dropped the recorder after a config change AuthorizationActivity does not declare (e.g. uiMode). DiagnosticContext - UNSET_CORRELATION_ID is now public, and the internal duplicate string literal in getThreadCorrelationId() references it. Callers that use the correlation id as a KEY rather than for logging have to reject the sentinel explicitly, and a copied "UNSET" literal in the registry would silently stop matching if this ever changed - reintroducing cross-request contamination with no signal. Note this PR alone changes no behaviour: register/unregister have no caller in this repo (both live in the paired broker PR), so get() always returns null and the hooks stay inert. Merge order: #3197 -> #3201 -> #3204 -> broker PR. Tests: OnboardingRecorderRegistryTest (15) covers correlation-id keying, the UNSET sentinel (including the concrete two-request cross-wiring it prevents), removal / lifecycle, absent-key behaviour, null and empty inputs, idempotent unregister, and the leak bound. Revert-tested: removing the sentinel guard fails with "the sentinel must never become a key expected:<0> but was:<1>"; disabling the cap fails with "expected:<16> but was:<100>"; switching eviction to insertion order evicts a live in-use recorder. Verified end-to-end on device: built brokerHost against this change and drove a brokered MAM/Conditional-Access flow; the finalized blob carried the WebView- observed steps, last_loaded_domain, and blocking_errors ["530003"] from the Auth UX bridge. 186 tests green in this area: WebViewClient 98, bridge 41, recorder 18, registry 15, payload 10, correlation store 4.
e797c59 to
11a7e49
Compare
…ed), AB#3708195 The broker builds an onboarding telemetry recorder per request from the seed, but the WebView that renders the interactive auth / remediation pages lives in a separate AuthorizationActivity created by the OS from an Intent. A live recorder cannot ride an Intent, so the WebView-side onboarding hooks were inert for brokered flows: steps observed in the WebView (MDM enrollment, Company Portal launch, broker install), the last loaded domain, and the Auth UX log_telemetry error code never reached the blob the broker finalizes and returns. Both components run in the broker :auth process, so this adds an in-process handoff keyed by the request correlationId. OnboardingRecorderRegistry (new) - Rejects DiagnosticContext.UNSET_CORRELATION_ID as a key. Every thread whose request context was never set carries that sentinel, and the authorization Intent extra is populated by reading the request-context map directly rather than through getThreadCorrelationId(), so the raw sentinel can reach the registry. It is shared by definition: accepting it would let two unrelated requests resolve to the same recorder and merge one flow's blocking errors into the other's uploaded blob. Rejecting it makes the feature inert (and logs a warning) instead of silently mis-attributing telemetry, which for a component whose whole purpose is correct attribution is the only acceptable failure. - Bounded, access-ordered map (cap 16). Entries must be unregistered on terminal outcome, and because this is process-static state in a process that lives as long as the device is up, a missed unregister would otherwise leak a recorder permanently - the recorder object and its collected steps / blocking errors, not an Activity, since OnboardingTelemetryRecorder holds only the application context. On overflow it evicts the least-recently-used entry and logs a warning, bounding the leak while surfacing the underlying bug. - Stores the concrete OnboardingTelemetryRecorder because the WebView client's hooks (e.g. setLastLoadedDomain) use methods not on the common4j interface. WebViewAuthorizationFragment - Resolves the recorder in onCreateView and attaches it to the WebView client before initializeAuthUxJavaScriptApi(...). No-op when the request seeded no recorder, which is the MSAL-client path. - Reads the correlation id from its own state bundle in extractState, alongside every other request field, rather than from activity.getIntent(). This fragment supports being hosted by an activity it does not own, whose Intent would not carry the extra. The id is also added to onSaveInstanceState: the Intent survives activity recreation but the bundle did not carry it, so reading only from the bundle would otherwise have silently dropped the recorder after a config change AuthorizationActivity does not declare (e.g. uiMode). DiagnosticContext - UNSET_CORRELATION_ID is now public, and the internal duplicate string literal in getThreadCorrelationId() references it. Callers that use the correlation id as a KEY rather than for logging have to reject the sentinel explicitly, and a copied "UNSET" literal in the registry would silently stop matching if this ever changed - reintroducing cross-request contamination with no signal. Note this PR alone changes no behaviour: register/unregister have no caller in this repo (both live in the paired broker PR), so get() always returns null and the hooks stay inert. Merge order: #3197 -> #3201 -> #3204 -> broker PR. Tests: OnboardingRecorderRegistryTest (15) covers correlation-id keying, the UNSET sentinel (including the concrete two-request cross-wiring it prevents), removal / lifecycle, absent-key behaviour, null and empty inputs, idempotent unregister, and the leak bound. Revert-tested: removing the sentinel guard fails with "the sentinel must never become a key expected:<0> but was:<1>"; disabling the cap fails with "expected:<16> but was:<100>"; switching eviction to insertion order evicts a live in-use recorder. Verified end-to-end on device: built brokerHost against this change and drove a brokered MAM/Conditional-Access flow; the finalized blob carried the WebView- observed steps, last_loaded_domain, and blocking_errors ["530003"] from the Auth UX bridge. 186 tests green in this area: WebViewClient 98, bridge 41, recorder 18, registry 15, payload 10, correlation store 4.
There was a problem hiding this comment.
Pull request overview
Wires brokered onboarding telemetry into the :auth WebView through a correlation-ID registry.
Changes:
- Adds a bounded, thread-safe recorder registry.
- Restores and uses correlation IDs to attach recorders.
- Exposes the unset-correlation sentinel and adds registry tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
DiagnosticContext.java |
Exposes the unset sentinel. |
OnboardingRecorderRegistry.kt |
Implements recorder handoff and eviction. |
OnboardingRecorderRegistryTest.kt |
Tests registry behavior and bounds. |
WebViewAuthorizationFragment.java |
Resolves and attaches the recorder. |
changelog.txt |
Documents the changes. |
…ed), AB#3708195 The broker builds an onboarding telemetry recorder per request from the seed, but the WebView that renders the interactive auth / remediation pages lives in a separate AuthorizationActivity created by the OS from an Intent. A live recorder cannot ride an Intent, so the WebView-side onboarding hooks were inert for brokered flows: steps observed in the WebView (MDM enrollment, Company Portal launch, broker install), the last loaded domain, and the Auth UX log_telemetry error code never reached the blob the broker finalizes and returns. Both components run in the broker :auth process, so this adds an in-process handoff keyed by the request correlationId. OnboardingRecorderRegistry (new) - Rejects DiagnosticContext.UNSET_CORRELATION_ID as a key. Every thread whose request context was never set carries that sentinel, and the authorization Intent extra is populated by reading the request-context map directly rather than through getThreadCorrelationId(), so the raw sentinel can reach the registry. It is shared by definition: accepting it would let two unrelated requests resolve to the same recorder and merge one flow's blocking errors into the other's uploaded blob. Rejecting it makes the feature inert (and logs a warning) instead of silently mis-attributing telemetry. - Bounded, access-ordered map (cap 16). Entries must be unregistered on terminal outcome, and because this is process-static state in a process that lives as long as the device is up, a missed unregister would otherwise leak a recorder permanently - the recorder object and its collected steps / blocking errors, not an Activity, since OnboardingTelemetryRecorder holds only the application context. On overflow it evicts the least-recently-used entry and logs a warning, bounding the leak while surfacing the underlying bug. AuthorizationFragment - Captures the correlation id in extractState and round-trips it through onSaveInstanceState. The read was already a base-class responsibility, so the save belongs next to it: putting it in one subclass left the other two (BrowserAuthorizationFragment, CurrentTaskBrowserAuthorizationFragment) still blanking their diagnostic context after activity recreation, and invited the pair to drift apart again. All three subclasses already call super, so the parent implementation fixes all of them with no duplication. - This also repairs a pre-existing bug on that path: extractState previously fed setDiagnosticContextForNewThread(null) after a recreation, so every subsequent log line for the request lost its correlation id. WebViewAuthorizationFragment - Resolves the recorder in onCreateView from the inherited mCorrelationId and attaches it to the WebView client before initializeAuthUxJavaScriptApi(...). No-op when the request seeded no recorder, which is the MSAL-client path. DiagnosticContext - UNSET_CORRELATION_ID is now public, and the internal duplicate string literal in getThreadCorrelationId() references it. Callers that use the correlation id as a KEY rather than for logging have to reject the sentinel explicitly, and a copied "UNSET" literal in the registry would silently stop matching if this ever changed - reintroducing cross-request contamination with no signal. Logger - Notes that its own UNSET literal is a display placeholder, deliberately independent of the DiagnosticContext sentinel: it also stands in for a missing thread id and is never used as a key, so it must not be collapsed into the new constant. Note this PR alone changes no behaviour for the recorder handoff: register/unregister have no caller in this repo (both live in the paired broker PR). Merge order: #3197 -> #3201 -> #3204 -> broker PR. Tests - OnboardingRecorderRegistryTest (15): keying, the UNSET sentinel (including the concrete two-request cross-wiring it prevents), removal / lifecycle, absent-key behaviour, null and empty inputs, idempotent unregister, and the leak bound. - AuthorizationFragmentCorrelationIdTest (6, new): the fragment half of the handoff - extractState captures the id, the captured id resolves the registered recorder, it survives save/restore, and the no-recorder / missing-id / sentinel paths resolve to null rather than throwing. Without these, a regression in the correlation-id plumbing would leave every hook inert while the registry tests still passed. Revert-tested: removing the sentinel guard fails with "the sentinel must never become a key expected:<0> but was:<1>"; disabling the cap fails with "expected:<16> but was:<100>"; switching eviction to insertion order evicts a live in-use recorder; dropping the onSaveInstanceState round-trip fails with "the correlation id must round-trip through the saved bundle expected:<...> but was:<null>". Verified end-to-end on device: built brokerHost against this change and drove a brokered MAM/Conditional-Access flow; the finalized blob carried the WebView- observed steps, last_loaded_domain, and blocking_errors ["530003"] from the Auth UX bridge. 246 tests green across the onboarding / Auth UX / authorization-fragment suites.
11a7e49 to
e067771
Compare
…ed), AB#3708195 The broker builds an onboarding telemetry recorder per request from the seed, but the WebView that renders the interactive auth / remediation pages lives in a separate AuthorizationActivity created by the OS from an Intent. A live recorder cannot ride an Intent, so the WebView-side onboarding hooks were inert for brokered flows: steps observed in the WebView (MDM enrollment, Company Portal launch, broker install), the last loaded domain, and the Auth UX log_telemetry error code never reached the blob the broker finalizes and returns. Both components run in the broker :auth process, so this adds an in-process handoff keyed by the request correlationId. OnboardingRecorderRegistry (new) - Rejects DiagnosticContext.UNSET_CORRELATION_ID as a key. Every thread whose request context was never set carries that sentinel, and the authorization Intent extra is populated by reading the request-context map directly rather than through getThreadCorrelationId(), so the raw sentinel can reach the registry. It is shared by definition: accepting it would let two unrelated requests resolve to the same recorder and merge one flow's blocking errors into the other's uploaded blob. Rejecting it makes the feature inert (and logs a warning) instead of silently mis-attributing telemetry. - Bounded, access-ordered map (cap 16). Entries must be unregistered on terminal outcome, and because this is process-static state in a process that lives as long as the device is up, a missed unregister would otherwise leak a recorder permanently - the recorder object and its collected steps / blocking errors, not an Activity, since OnboardingTelemetryRecorder holds only the application context. On overflow it evicts the least-recently-used entry and logs a warning, bounding the leak while surfacing the underlying bug. AuthorizationFragment - Captures the correlation id in extractState and round-trips it through onSaveInstanceState. The read was already a base-class responsibility, so the save belongs next to it: putting it in one subclass left the other two (BrowserAuthorizationFragment, CurrentTaskBrowserAuthorizationFragment) still blanking their diagnostic context after activity recreation, and invited the pair to drift apart again. All three subclasses already call super, so the parent implementation fixes all of them with no duplication. - This also repairs a pre-existing bug on that path: extractState previously fed setDiagnosticContextForNewThread(null) after a recreation, so every subsequent log line for the request lost its correlation id. WebViewAuthorizationFragment - Resolves the recorder in onCreateView from the inherited mCorrelationId and attaches it to the WebView client before initializeAuthUxJavaScriptApi(...). No-op when the request seeded no recorder, which is the MSAL-client path. DiagnosticContext - UNSET_CORRELATION_ID is now public, and the internal duplicate string literal in getThreadCorrelationId() references it. Callers that use the correlation id as a KEY rather than for logging have to reject the sentinel explicitly, and a copied "UNSET" literal in the registry would silently stop matching if this ever changed - reintroducing cross-request contamination with no signal. Logger - Notes that its own UNSET literal is a display placeholder, deliberately independent of the DiagnosticContext sentinel: it also stands in for a missing thread id and is never used as a key, so it must not be collapsed into the new constant. Note this PR alone changes no behaviour for the recorder handoff: register/unregister have no caller in this repo (both live in the paired broker PR). Merge order: #3197 -> #3201 -> #3204 -> broker PR. Tests - OnboardingRecorderRegistryTest (15): keying, the UNSET sentinel (including the concrete two-request cross-wiring it prevents), removal / lifecycle, absent-key behaviour, null and empty inputs, idempotent unregister, and the leak bound. - AuthorizationFragmentCorrelationIdTest (6, new): the fragment half of the handoff - extractState captures the id, the captured id resolves the registered recorder, it survives save/restore, and the no-recorder / missing-id / sentinel paths resolve to null rather than throwing. Without these, a regression in the correlation-id plumbing would leave every hook inert while the registry tests still passed. Revert-tested: removing the sentinel guard fails with "the sentinel must never become a key expected:<0> but was:<1>"; disabling the cap fails with "expected:<16> but was:<100>"; switching eviction to insertion order evicts a live in-use recorder; dropping the onSaveInstanceState round-trip fails with "the correlation id must round-trip through the saved bundle expected:<...> but was:<null>". Verified end-to-end on device: built brokerHost against this change and drove a brokered MAM/Conditional-Access flow; the finalized blob carried the WebView- observed steps, last_loaded_domain, and blocking_errors ["530003"] from the Auth UX bridge. 246 tests green across the onboarding / Auth UX / authorization-fragment suites.
e067771 to
de36bf5
Compare
…ed), AB#3708195 The broker builds an onboarding telemetry recorder per request from the seed, but the WebView that renders the interactive auth / remediation pages lives in a separate AuthorizationActivity created by the OS from an Intent. A live recorder cannot ride an Intent, so the WebView-side onboarding hooks were inert for brokered flows: steps observed in the WebView (MDM enrollment, Company Portal launch, broker install), the last loaded domain, and the Auth UX log_telemetry error code never reached the blob the broker finalizes and returns. Both components run in the broker :auth process, so this adds an in-process handoff keyed by the request correlationId. OnboardingRecorderRegistry (new) - Rejects DiagnosticContext.UNSET_CORRELATION_ID as a key. Every thread whose request context was never set carries that sentinel, and the authorization Intent extra is populated by reading the request-context map directly rather than through getThreadCorrelationId(), so the raw sentinel can reach the registry. It is shared by definition: accepting it would let two unrelated requests resolve to the same recorder and merge one flow's blocking errors into the other's uploaded blob. Rejecting it makes the feature inert (and logs a warning) instead of silently mis-attributing telemetry. - Bounded, access-ordered map (cap 16). Entries must be unregistered on terminal outcome, and because this is process-static state in a process that lives as long as the device is up, a missed unregister would otherwise leak a recorder permanently - the recorder object and its collected steps / blocking errors, not an Activity, since OnboardingTelemetryRecorder holds only the application context. On overflow it evicts the least-recently-used entry and logs a warning, bounding the leak while surfacing the underlying bug. AuthorizationFragment - Captures the correlation id in extractState and round-trips it through onSaveInstanceState. The read was already a base-class responsibility, so the save belongs next to it: putting it in one subclass left the other two (BrowserAuthorizationFragment, CurrentTaskBrowserAuthorizationFragment) still blanking their diagnostic context after activity recreation, and invited the pair to drift apart again. All three subclasses already call super, so the parent implementation fixes all of them with no duplication. - This also repairs a pre-existing bug on that path: extractState previously fed setDiagnosticContextForNewThread(null) after a recreation, so every subsequent log line for the request lost its correlation id. WebViewAuthorizationFragment - Resolves the recorder in onCreateView from the inherited mCorrelationId and attaches it to the WebView client before initializeAuthUxJavaScriptApi(...). No-op when the request seeded no recorder, which is the MSAL-client path. DiagnosticContext - UNSET_CORRELATION_ID is now public, and the internal duplicate string literal in getThreadCorrelationId() references it. Callers that use the correlation id as a KEY rather than for logging have to reject the sentinel explicitly, and a copied "UNSET" literal in the registry would silently stop matching if this ever changed - reintroducing cross-request contamination with no signal. Logger - Notes that its own UNSET literal is a display placeholder, deliberately independent of the DiagnosticContext sentinel: it also stands in for a missing thread id and is never used as a key, so it must not be collapsed into the new constant. Note this PR alone changes no behaviour for the recorder handoff: register/unregister have no caller in this repo (both live in the paired broker PR). Merge order: #3197 -> #3201 -> #3204 -> broker PR. Tests - OnboardingRecorderRegistryTest (15): keying, the UNSET sentinel (including the concrete two-request cross-wiring it prevents), removal / lifecycle, absent-key behaviour, null and empty inputs, idempotent unregister, and the leak bound. - AuthorizationFragmentCorrelationIdTest (6, new): the fragment half of the handoff - extractState captures the id, the captured id resolves the registered recorder, it survives save/restore, and the no-recorder / missing-id / sentinel paths resolve to null rather than throwing. Without these, a regression in the correlation-id plumbing would leave every hook inert while the registry tests still passed. Revert-tested: removing the sentinel guard fails with "the sentinel must never become a key expected:<0> but was:<1>"; disabling the cap fails with "expected:<16> but was:<100>"; switching eviction to insertion order evicts a live in-use recorder; dropping the onSaveInstanceState round-trip fails with "the correlation id must round-trip through the saved bundle expected:<...> but was:<null>". Verified end-to-end on device: built brokerHost against this change and drove a brokered MAM/Conditional-Access flow; the finalized blob carried the WebView- observed steps, last_loaded_domain, and blocking_errors ["530003"] from the Auth UX bridge. 246 tests green across the onboarding / Auth UX / authorization-fragment suites.
de36bf5 to
a18f82e
Compare
…ed), AB#3708195 The broker builds an onboarding telemetry recorder per request from the seed, but the WebView that renders the interactive auth / remediation pages lives in a separate AuthorizationActivity created by the OS from an Intent. A live recorder cannot ride an Intent, so the WebView-side onboarding hooks were inert for brokered flows: steps observed in the WebView (MDM enrollment, Company Portal launch, broker install), the last loaded domain, and the Auth UX log_telemetry error code never reached the blob the broker finalizes and returns. Both components run in the broker :auth process, so this adds an in-process handoff keyed by the request correlationId. OnboardingRecorderRegistry (new) - Rejects DiagnosticContext.UNSET_CORRELATION_ID as a key. Every thread whose request context was never set carries that sentinel, and the authorization Intent extra is populated by reading the request-context map directly rather than through getThreadCorrelationId(), so the raw sentinel can reach the registry. It is shared by definition: accepting it would let two unrelated requests resolve to the same recorder and merge one flow's blocking errors into the other's uploaded blob. Rejecting it makes the feature inert (and logs a warning) instead of silently mis-attributing telemetry. - Bounded, access-ordered map (cap 16). Entries must be unregistered on terminal outcome, and because this is process-static state in a process that lives as long as the device is up, a missed unregister would otherwise leak a recorder permanently - the recorder object and its collected steps / blocking errors, not an Activity, since OnboardingTelemetryRecorder holds only the application context. On overflow it evicts the least-recently-used entry and logs a warning, bounding the leak while surfacing the underlying bug. AuthorizationFragment - Captures the correlation id in extractState and round-trips it through onSaveInstanceState. The read was already a base-class responsibility, so the save belongs next to it: putting it in one subclass left the other two (BrowserAuthorizationFragment, CurrentTaskBrowserAuthorizationFragment) still blanking their diagnostic context after activity recreation, and invited the pair to drift apart again. All three subclasses already call super, so the parent implementation fixes all of them with no duplication. - This also repairs a pre-existing bug on that path: extractState previously fed setDiagnosticContextForNewThread(null) after a recreation, so every subsequent log line for the request lost its correlation id. WebViewAuthorizationFragment - Resolves the recorder in onCreateView from the inherited mCorrelationId and attaches it to the WebView client before initializeAuthUxJavaScriptApi(...). No-op when the request seeded no recorder, which is the MSAL-client path. DiagnosticContext - UNSET_CORRELATION_ID is now public, and the internal duplicate string literal in getThreadCorrelationId() references it. Callers that use the correlation id as a KEY rather than for logging have to reject the sentinel explicitly, and a copied "UNSET" literal in the registry would silently stop matching if this ever changed - reintroducing cross-request contamination with no signal. Logger - Notes that its own UNSET literal is a display placeholder, deliberately independent of the DiagnosticContext sentinel: it also stands in for a missing thread id and is never used as a key, so it must not be collapsed into the new constant. Note this PR alone changes no behaviour for the recorder handoff: register/unregister have no caller in this repo (both live in the paired broker PR). Merge order: #3197 -> #3201 -> #3204 -> broker PR. Tests - OnboardingRecorderRegistryTest (15): keying, the UNSET sentinel (including the concrete two-request cross-wiring it prevents), removal / lifecycle, absent-key behaviour, null and empty inputs, idempotent unregister, and the leak bound. - AuthorizationFragmentCorrelationIdTest (6, new): the fragment half of the handoff - extractState captures the id, the captured id resolves the registered recorder, it survives save/restore, and the no-recorder / missing-id / sentinel paths resolve to null rather than throwing. Without these, a regression in the correlation-id plumbing would leave every hook inert while the registry tests still passed. Revert-tested: removing the sentinel guard fails with "the sentinel must never become a key expected:<0> but was:<1>"; disabling the cap fails with "expected:<16> but was:<100>"; switching eviction to insertion order evicts a live in-use recorder; dropping the onSaveInstanceState round-trip fails with "the correlation id must round-trip through the saved bundle expected:<...> but was:<null>". Verified end-to-end on device: built brokerHost against this change and drove a brokered MAM/Conditional-Access flow; the finalized blob carried the WebView- observed steps, last_loaded_domain, and blocking_errors ["530003"] from the Auth UX bridge. 246 tests green across the onboarding / Auth UX / authorization-fragment suites.
a18f82e to
9e4f318
Compare
…erge) into the recorder-wiring branch
|
Nit (non-blocking): In mCorrelationId = state.getString(DiagnosticContext.CORRELATION_ID); // may be null
setDiagnosticContextForNewThread(mCorrelationId);
I checked the full path and this is safe today: The only reason to tighten it is defense-in-depth: the null-tolerance is load-bearing on if (mCorrelationId != null) {
outState.putString(DiagnosticContext.CORRELATION_ID, mCorrelationId);
}Not blocking — purely a robustness nit. |
Review nit from Prvnkmr337: extractState can leave mCorrelationId null (an MSAL client with no diagnostic context), and onSaveInstanceState then stored that null under CORRELATION_ID, which setDiagnosticContextForNewThread later put() into a RequestContext with no null guard. Verified his analysis rather than assuming: RequestContext extends HashMap, so put(key, null) is legal, and the only reader -- DiagnosticContext .getThreadCorrelationId() -- guards null and substitutes a random UUID. So this is safe today and matches pre-existing behaviour. Taken anyway as defence-in-depth, because that safety is load-bearing on the map type: a future swap to ConcurrentHashMap/Hashtable would make put(key, null) throw. Guarding the write is the cheaper end -- an absent key is already handled identically to "never saved", so nothing downstream has to tolerate a null at all. Test: testNullCorrelationId_IsNotWrittenToTheSavedBundle asserts the key is absent and that the recreation path still reads back null without throwing. Revert-tested: removing the guard fails with "a null correlation id must not be written under the key". No changelog entry -- the existing #3204 round-trip entry already covers this method's behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Taken, in I verified your analysis rather than assuming it: Took it anyway for the reason you gave — the safety is load-bearing on the map type, and a future swap to Test asserts the key is absent and that the recreation path still reads back null without throwing; revert-tested. No changelog entry — the existing round-trip entry already covers this method. |
Three #3204 entries -> one, matching the repo's one-entry-per-PR convention (89 of 90 released entries are single-entry; median ~101 chars, these were 358 / 189 / 318). Kept the correlation-id round-trip fix in the line because it affects all three authorization fragments and every log line after a recreation, so it is worth finding from the changelog. The UNSET_CORRELATION_ID exposure exists to support this feature and is covered by the PR. Also de-duplicated the #3201 block: merging the consolidation up appended the new line instead of replacing the old three, which is the changelog merge hazard this stack has hit before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
One entry per PR across the stack, matching the repo convention (89 of 90 released entries are single-entry; median ~101 chars). The #3209 line drops from 562 to 224 characters and now leads with what a reader needs -- non-brokered flows get a telemetry-only bridge, gated on the onboarding seed -- leaving the rationale to the PR description. Also de-duplicated #3201 and #3204: merging each consolidation up the stack APPENDED the new line rather than replacing the old ones, so the block had to be rebuilt canonically. Worth remembering -- a changelog edit merged upward is not idempotent, and the count has to be re-censused after every merge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
|
||
| // Written under the registry lock by removeEldestEntry, read and cleared by register() | ||
| // immediately after the lock is released, so the warning is logged off-lock. | ||
| private var evictedKey: String? = null |
There was a problem hiding this comment.
nit: can we put @GuardedBy("recorders") on evictedKey? It's a plain var that's only safe because removeEldestEntry writes it and register reads/clears it under the same lock. The comment already spells that out, the annotation just makes it harder for a future edit to touch it off-lock without noticing.
There was a problem hiding this comment.
Done, in 01a2b2d25.
Worth flagging that the comment it points at was wrong, in a way that mattered for exactly this annotation. It said evictedKey is "read and cleared by register() immediately after the lock is released" — but the read-and-clear is evictedKey.also { evictedKey = null } inside the synchronized(recorders) block; only the resulting warning is logged off-lock. So the prose described a lock discipline that would have made @GuardedBy("recorders") a lie, and a future reader trusting it would have concluded the annotation was wrong and removed it rather than the access. Reworded so the two now agree.
Both accesses verified under the monitor: removeEldestEntry runs inside put(), which the caller already holds the lock for, and register()'s read/clear is in the same critical section.
Used androidx.annotation.GuardedBy — same artifact as the VisibleForTesting already imported here, so no new dependency, and it's the variant Android Lint's GuardedBy check understands.
…t/pbi-3708195-recorder-wiring # Conflicts: # common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationFragment.java # common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java
|
Synced the stack with
Verification that the resolution kept Also took Shahzaib (@shahzaibj)'s |
Summary
Implements the common (AndroidCommon) half of AB#3708195 — wiring the onboarding telemetry recorder into the interactive
:authWebView host so the brokered flow records WebView-observed onboarding steps and the Auth UXlog_telemetryerror code into the blob the broker finalizes and returns.Problem
The onboarding recorder is built per-request by the broker's
AccountChooser, but the interactive WebView runs in a separateAuthorizationActivitycreated by the OS from an Intent — a live recorder can't ride that Intent. As a resultAzureActiveDirectoryWebViewClient's recorder hooks (recordOnboardingStep,recordLastLoadedDomain, and thelog_telemetryrecordAuthUxServerErrorCodesink from #3201) were dead in production: nothing ever calledsetOnboardingTelemetryRecorder(...).Changes
OnboardingRecorderRegistry(new) — a process-static handoff keyed by request correlationId. The owner (AccountChooser) and the WebView host both run in the broker:authprocess, so an in-process registry bridges them. Stores the concreteOnboardingTelemetryRecorderbecause the WebView hooks (e.g.setLastLoadedDomain) use methods not on the common4j interface.DiagnosticContextseeds every thread's request context withUNSET, and the authorization Intent extra is populated by reading that map directly rather than throughgetThreadCorrelationId()— so the raw sentinel can reach the registry. It is shared by definition, so accepting it would let two unrelated requests resolve to the same recorder and merge one flow's blocking errors into the other's uploaded blob. All three accessors reject it: the feature goes inert (andregisterwarns) instead of silently mis-attributing telemetry.LinkedHashMap). Entries must be unregistered on terminal outcome, and since this is process-static state in a process that lives as long as the device is up, a missedunregisterwould leak the recorder and its collected steps/blocking errors permanently (not an Activity —OnboardingTelemetryRecorderholds only the application context). On overflow it evicts the least-recently-used entry and logs a warning, bounding the leak and surfacing the underlying bug. The cap is far above real concurrency (one interactive request at a time), so it should never evict in legitimate use; LRU is not load-bearing today (the host resolves once and holds the reference) but is the safer default if a future caller re-resolves.AuthorizationFragment— captures the correlation id inextractStateand round-trips it throughonSaveInstanceState. The read was already a base-class responsibility, so the save belongs next to it: previously the key was never saved at all, so a recreated fragment readnullback and blanked its diagnostic context, losing the join key on every subsequent log line for the request. Fixing it in the parent covers all three authorization fragments (WebView, Browser, CurrentTaskBrowser) — all already callsuper— and removes any chance of the save and read drifting apart.WebViewAuthorizationFragment— resolves the recorder inonCreateViewfrom the inheritedmCorrelationIdand callssetOnboardingTelemetryRecorder(...)right beforeinitializeAuthUxJavaScriptApi(...). No-op when the request seeded no recorder. Reading from the state bundle rather thanactivity.getIntent()also matters because this fragment supports being hosted by an activity it does not own, whose Intent would not carry the extra.DiagnosticContext(common4j) —UNSET_CORRELATION_IDis now public, and the internal duplicate"UNSET"literal ingetThreadCorrelationId()references it. Callers that use the correlation id as a key rather than for logging must reject the sentinel explicitly, and a copied literal in the registry would silently stop matching if this ever changed — reintroducing cross-request contamination with no signal.Logger(common4j) — notes that its ownUNSETliteral is a display placeholder, deliberately not the same thing: it also stands in for a missing thread id and is never used as a key, so it must not be collapsed into the new constant.Tests —
OnboardingRecorderRegistryTest(15): correlation-id keying (one request must never see another's recorder), the UNSET sentinel — including the concrete two-request cross-wiring it prevents — removal/lifecycle, absent-key behaviour (the no-seed MSAL path gets null rather than a throw), null/empty inputs, idempotent unregister, and the two leak-bound properties.AuthorizationFragmentCorrelationIdTest(6, new): the fragment half of the handoff —extractStatecaptures the id, the captured id resolves the registered recorder, it survives save/restore, and the no-recorder / missing-id / sentinel paths resolve to null rather than throwing. Without these, a regression in the correlation-id plumbing would leave every hook inert while the registry tests still passed.All four behaviours are revert-tested: removing the sentinel guard fails with
the sentinel must never become a key expected:<0> but was:<1>, disabling the cap fails withexpected:<16> but was:<100>, switching to insertion order evicts a live in-use recorder, and dropping theonSaveInstanceStateround-trip fails withthe correlation id must round-trip through the saved bundle expected:<…> but was:<null>.The paired broker PR registers the recorder in the registry (keyed by the same correlationId) and unregisters it on terminal outcome in a
finally. Load-bearing assumption to confirm on that side: the valueAccountChooserregisters under must be identical toDiagnosticContext.INSTANCE.requestContext[CORRELATION_ID]as read on the thread that builds the authorization Intent. The on-device E2E below shows they matched; the sentinel guard now converts a mismatch from silent cross-contamination into an inert feature plus a logged warning, but the two sides still have to agree for the handoff to work at all.Verified end-to-end on device
Built brokerHost against these changes and drove a brokered MAM/Conditional-Access flow (MSAL test app → Broker Host, MAMCA account) on an emulator. Logcat confirmed the full chain, and the finalized blob returned on the failure path:
{"schema_version":"1.0.0","session_correlation_id":"…","onboarding_mode":"brokered", "steps_list":[{"step_id":"AuthenticationStarted",…},{"step_id":"CABlockReceived",…}, {"step_id":"InterruptFlowStarted",…},{"step_id":"UserCanceled",…}], "blocking_errors":["530003"],"last_blocking_error":"530003", "last_loaded_domain":"login.microsoftonline.com","last_completed_step":"UserCanceled"}last_loaded_domainpopulated (a previously-dead WebViewClient hook, now active).530003present via thelog_telemetrybridge → sink (a debug-only bridge simulation stood in for the not-yet-built server page JS, AB#3696811).Notes
:auth-only (ProcessUtil.isRunningOnAuthService+ENABLE_JS_API_FOR_AUTHUX); non-brokered/iOS is AB#3688630.log_telemetrypost) is kept on a local build branch, not in this PR.Draft PR — opened for early review. AB#3708195.