Release Monitoring Report
Produce a version-over-version release-health report covering the Android Broker and/or
the Authenticator app in one self-contained HTML file at
$env:USERPROFILE\android-release-reports\release-report-broker-<bv>-auth-<av>-<YYYY-MM-DD>.html
(home folder, outside the workspace — reports never get committed). Omitted apps drop out
of the filename, so broker-only or authenticator-only runs are first-class.
The report answers two questions per app: what changed this release (KPIs + tables vs the
baseline) and why (error-code movers / per-scenario deltas). It ends in a clear verdict
per app: SAFE to proceed / WATCH / HOLD.
The output mirrors the canonical template at
assets/templates/report-template.html — a real filled
example kept as the structural + visual reference. The Step 1 bootstrap script copies it into
~/android-release-reports/… and you edit it in place (versions, dates, KPI values, table
rows, verdict prose). Do not redesign the layout.
Before writing any KQL, read the three reference files:
assets/docs/kusto-cheatsheet.md — how to run queries
(auth, run-kql.ps1 invocation, output JSON shape, token substitution, the end-to-end loop,
version-resolution recipes, hard gotchas).
assets/queries/README.md — the query catalog: what each .kql
computes, the token convention, and the Authenticator scenario→MV→column map.
assets/docs/investigation-patterns.md — the diagnostic
methodology: the count-vs-rate / version-attribution / code-frozen-control / rollout-cohort /
benign-vs-real / dimensional-decomposition / drill-to-sub-code / release-tag-diff / MV-introspection /
crash-version-attribution (P10) patterns, plus the decision flow. A KPI delta — or a crash that looks
new — is a question, not a verdict; run these before any table row becomes "regression" prose or a HOLD.
For the Authenticator crash/stability layer, also read:
assets/docs/crash-sources.md — App Center crash pull (auth/token,
errorGroups endpoint, the nextLink quirk), the share-vs-rate trap, the Kusto rate
denominator, secret handling, and the deferred Play Console (Phase 2). Authenticator only — the
Broker is not a store app and has no crash section.
Inputs to confirm
Ask only for what's missing; infer the rest.
- Which app(s) — Broker version rolling out, Authenticator version rolling out, or both.
At least one is required.
- Baseline per app — the previous release version to compare against, OR "auto-resolve the
previous version" (pick the next-highest-volume recent version), OR "all-versions baseline".
- Window — default last 30 days (Authenticator MVs) / 14 days (Broker). The new
version is usually young, so a longer window mostly grows the baseline cohort.
- Report date — defaults to today (used in the filename + the "Generated" banner).
- Authenticator crash layer (optional) — if an Authenticator version is given, add the
crash/stability section. It needs an App Center read-only token (
~/.android-release-reports/appcenter.token,
$APPCENTER_API_TOKEN, or --token-file). Step 3b step 0 auto-runs
preflight-appcenter.ps1 -Check and, if the token is
missing/expired, posts an unmissable action block for the engineer to run -Setup once, then
blocks on an auto-poll and won't finalize the report until a token validates or the engineer
explicitly skips (never a silent skip). The token is a
secret — never echo it or write it into the report.
If the user gives versions but not the baseline, run the resolution queries first and propose
<FIRST> (rolling out) / <SECOND> (previous) from volume, then continue.
Data sources (summary — full detail in the cheatsheet)
| App |
Cluster |
Database |
Version dim |
Time col |
| Broker |
https://idsharedeus2.kusto.windows.net |
ad-accounts-android-otel |
broker_version (16.1.0) |
EventInfo_Time |
| Authenticator |
https://idsharedeus2.eastus2.kusto.windows.net |
d496be22d62a46b0a3cf67ea2e736fd8 |
AppVersion (6.2606.3817) |
EventDate (MVs) |
run-kql.ps1 defaults to Broker; pass -Cluster/-Database for Authenticator. Requires
az login (Android Auth Client SDK security group).
Bundled assets
| File |
Purpose |
templates/report-template.html |
Canonical layout — a real filled example. Edit in place; do not restyle. The CSS in <head> is canonical. |
docs/kusto-cheatsheet.md |
Operational runbook: run-kql usage, JSON shape, token substitution, end-to-end loop, gotchas. |
docs/investigation-patterns.md |
Diagnostic methodology — count-vs-rate, version-attribution-vs-substitution, code-frozen control, rollout-cohort, benign-vs-real classification, dimensional decomposition, drill-to-sub-code, release-tag diff, MV introspection, crash version-attribution (P10 — is a crash new/release-caused?), plus the Authenticator drill ladder (outcomes MV → *_Errors_MV_V1 → raw passkeyoperations) and a decision flow. Apply before calling any delta a regression. |
docs/crash-sources.md |
Authenticator crash layer: App Center auth/endpoint, the three gotchas (errorGroupId-is-version-scoped, share-vs-rate, firstOccurrence-is-rollout-date), the newcrashes/signature new-crash detection flow, Kusto rate denominator, secret handling, Play Console Phase 2. |
queries/README.md |
Query catalog + Authenticator scenario→MV→column map. |
queries/*.kql |
6 Broker + 6 Authenticator scenario templates + authenticator-crash-denominator.kql (crash-rate denominator), all validated live. Substitute <TOKENS> before running. Includes broker-errors-by-host-app-span.kql — the per-span_name request-rate drill-down that complements the host-app device-share movers. |
scripts/run-kql.ps1 |
Direct-REST Kusto helper. -Query/-Out mandatory; -Cluster/-Database for Authenticator. |
scripts/find-suspect-prs.ps1 |
Release PR correlation. Auth-code (eSTS) attribution: defaults to broker/+common/ over a broker tag range (-Range v16.1.0..v16.2.0; broker uses its own tags, common maps via the broker submodule pointer). Crash attribution: -Repos authenticator over the app tag range (-Range 6.2606.3817..6.2606.4029) — resolves the app's own tags and parses ADO Merged PR NNNNNNNN: → pullrequest URLs. Three search streams: -S pickaxe + -DiffGrep (git log -G over diff text) + --grep (subject). For a crash, set -Symbol to the exception/API token from the stack (e.g. EntryPoints.get), not the crashing class, and always pass -DiffGrep — the crashing class is the victim, the culprit is a caller whose subject rarely names the subsystem. Prints PR ids + URLs for attribution cards. |
scripts/preflight-appcenter.ps1 |
App Center token gate for the crash layer. -Check (non-interactive, the skill auto-runs it) resolves the token (--token-file → $APPCENTER_API_TOKEN → ~/.android-release-reports/appcenter.token) and validates it with one GET /apps/{owner}/{app}, emitting STATUS: ok|missing|invalid|no-access|network + exit 0/2/3/4/5 (+ -Json). Add -Wait <sec> -IntervalSec <sec> to block-poll until a valid token appears (auto-resume, no handshake). -Setup (interactive — the engineer runs it once in a real terminal) opens the token page, reads the token via Read-Host -AsSecureString (never echoed), validates, then saves with a user-only ACL. Never prints the token. |
scripts/fetch-appcenter-crashes.js |
Pull Authenticator crash clusters from App Center → run-kql array-form JSON. groups (one version) + diff (two versions, signature-joined, per-1k rate when given Kusto denominators) + enrich (top signatures' daily trend + instance-sampled OS-major/device-model concentration) + newcrashes (genuinely-new java-frame signatures via anti-join against a union of priors, native/hex frames split out as new-native?) + signature (cross-version presence of one signature + trend — "is crash X version-specific?"). Captures exceptionMessage/appCodeFrame/firstOccurrence, drops hidden/Ignored groups, and --page-cap 0 exhausts paging for an accurate total. |
scripts/bootstrap-report.ps1 |
Copy the template to a version-named file, create _data/<slug>-<date>/, stamp the Generated date, prune old _data, detect unfilled-stub vs real-report collisions. |
scripts/compare-versions.js |
Delta + classification engine over run-kql JSON. rows mode (version-per-row metrics) and movers mode (paired error-share rows). Thresholds + volume guard. |
scripts/validate-report.ps1 |
Pre-publish validator: stale tokens, mojibake, raw-count leaks, version-string presence, both app sections, verdict callouts. |
Workflow
Step 1 — Bootstrap
$S = ".github/skills/release-monitoring-report/assets/scripts"
$out = & "$S\bootstrap-report.ps1" -BrokerVersion 16.1.0 -AuthVersion 6.2606.3817
Omit either -…Version for a single-app run. The script prints the report path and creates the
_data/<slug>-<date>/ folder ($DATA) for raw query payloads. Re-running on an unfilled stub
is silent; a populated report halts unless you pass -Force.
Step 2 — Resolve versions
If the baseline wasn't given, run broker-adoption.kql and the cheap Authenticator resolver
(cheatsheet § "Version resolution recipes") to pick <FIRST>/<SECOND> by volume. For an
"all-versions baseline", drop the version filter / list every version in <VERSIONS>.
Step 3 — Pull queries (both apps)
For each needed .kql: read it, substitute tokens (cheatsheet § "Filling tokens"), run via
run-kql.ps1 -Out $DATA\<name>.json. Run independent queries in parallel (PowerShell jobs).
Minimum useful set:
- Broker:
broker-adoption, broker-reliability-by-version, broker-error-rate-by-version,
broker-top-errors-by-version (the "why"), broker-latency-by-version.
- Broker via Authenticator (when an Authenticator version is given): also run
broker-by-host-app and broker-top-errors-by-host-app with <PACKAGE>=com.azure.authenticator
and <FIRST>/<SECOND> = the Authenticator app versions (these MVs key the host's broker on
AppInfo_Version, which for that package is the app version). This isolates whether the broker
regresses because of this app rollout, separate from the fleet-wide broker_version view.
- Authenticator:
auth-version-resolve (or cheap fallback), then per top scenario:
auth-scenario-success-rate (Registration/Auth) or auth-pn-checkforauth-completion (PN+CFA),
always alongside auth-scenario-initiates for the volume guard. Use auth-stats for adoption.
Step 3b — Authenticator crash layer (optional)
If an Authenticator version is given, read
assets/docs/crash-sources.md first, then gate on the token:
- Preflight the App Center token (do this first — don't silently skip). Auto-run:
pwsh -NoProfile -File "$S\preflight-appcenter.ps1" -Check -Json
Branch on the STATUS: line / exit code:
ok (exit 0) → proceed to step 1 (fully automatic, no user action).
missing (2) / invalid (3) → the token is absent or expired. Don't write the
placeholder yet, and DO NOT just tell the engineer and move on — set up a seamless capture +
auto-resume. Split this across the run so the engineer's paste window overlaps the Kusto work:
prompt EARLY (step a — the moment -Check first returns not-ok, even before the main queries),
then block LATE (step b — the foreground poll, at the crash layer, which is the report's last
section). Both are hard requirements; skipping either is a bug:
a. Surface an UNMISSABLE action block. Post the setup ask as its own dedicated, visually
prominent message — a ⚠️ ACTION REQUIRED header, one line stating "the report's
crash/stability section is blocked until you do this," and the exact command. Do not bury
it mid-paragraph, trail it with "meanwhile I'll continue," or imply the report will finish
without it — that both makes engineers miss it and tempts you to drop the gate in step b.
Open the best capture surface for the host and launch -Setup there:
- Desktop app (Terminal canvas available) → open a Terminal canvas and run
pwsh -File "<ABSOLUTE>\preflight-appcenter.ps1" -Setup in it, so the engineer pastes
without leaving the app.
- VS Code Copilot Chat → tell the engineer to run that same command in the integrated
terminal (they already have one open).
- Copilot CLI / anything else → surface the command for the engineer's own terminal.
In every case relay the ABSOLUTE script path that preflight -Check just printed (its
message uses $PSCommandPath). Never a repo-relative path like
.github\…\preflight-appcenter.ps1 — the engineer's terminal is usually at their home
directory, where a relative path fails with "not recognized as the name of a script file".
b. Run the blocking poll in the FOREGROUND as a single step and wait on its result — do NOT
background it and "check later." This is the enforcement mechanism, and the most important rule
in this branch. Observed failure: when the poll is launched detached (or merely described as
running "in parallel"), the agent posts the message, moves on, and the poll never actually runs —
so pasting the token resumes nothing and the crash layer silently never happens. The crash layer is
the last section of the report, so by the time you reach this branch every other Kusto query is
already done and there is nothing left to parallelize. Run it as an ordinary blocking command and
treat its return as a hard gate:
pwsh -NoProfile -File "$S\preflight-appcenter.ps1" -Check -Json -Wait 180 -IntervalSec 10
Wait for this call to return before writing the crash/stability section or declaring the report
complete — the blocking call is the wait. It returns ok the instant the engineer finishes
-Setup (they never say "done"). On timeout (missing/invalid after 180s), stop and explicitly
ask: keep waiting (re-run the poll) or skip crashes this run. Never silently skip, and never
finalize past a pending or timed-out poll without an explicit skip.
no-access (4) → their token is valid but not scoped to authapp-t7qc; tell them to generate a
read-only token in the right org, then -Setup.
network (5) → App Center unreachable; note it in the report and skip the crash layer this run.
- Run
authenticator-crash-denominator.kql (auth cluster) to get active devices for <FIRST>/<SECOND>.
- Pull + pair crashes (signature-joined; pass the two device counts so it computes the per-1k rate).
Use
--page-cap 0 for a verdict so the crash total isn't undercounted (there's no aggregate-total
endpoint — the total is only the pages fetched, and a busy version has 1,300+ groups):
node "$S\fetch-appcenter-crashes.js" diff --owner authapp-t7qc `
--app Microsoft-Authenticator-Android-Prod-App-Center `
--version 6.2606.3817 --base 6.2605.3042 --days 14 --page-cap 0 `
--devices-new <DEV_FIRST> --devices-base <DEV_SECOND> --out "$DATA\crash-diff.json"
The diff now also carries exceptionMessage, appCodeFrame (class.method:line), firstOccurrence,
appBuild, state, and drops team-muted hidden/Ignored groups by default — fill these into
the crash cards. diff status=new is only a CANDIDATE, not a verdict — it means "absent from the
single --base," and firstOccurrence is the version's rollout date, not the signature's
app-history first-seen (a years-old crash shows a first-seen inside your window). Confirm any "new"
with step 4 below.
3. Enrich the top movers — the list view can't show a crash's trend or which OS/OEM it hits,
so pull both (these answer the P4/P6 patterns for crashes):
node "$S\fetch-appcenter-crashes.js" enrich --owner authapp-t7qc `
--app Microsoft-Authenticator-Android-Prod-App-Center `
--version 6.2606.3817 --days 14 --top 8 --out "$DATA\crash-enrich.json"
It returns, per top signature, a daily-trend tag (rising/decaying/spike-then-decay/steady
- peak/last day) and an instance-sampled top OS-major + device-model concentration (the aggregate
OS/model endpoints 404, so this samples
errorGroups/{id}/errors). Read it as: OS-concentrated +
rising + broad-across-models ⇒ platform/release suspect; model-concentrated on rugged/obfuscated
frames ⇒ tamper/sideload, not app quality; spike-then-decay ⇒ early-rollout churn, not a HOLD.
- Find genuinely-new crashes (P10) — to answer "what crashes are new in this release," anti-join
the new build's signatures against a union of recent priors (not just the baseline —
diff's
single-baseline new flag false-positives a crash that skipped one version). Then cross-version-
confirm any suspect with signature:
# genuinely-new = java-frame signature absent from ALL priors in the 27-day window (native/hex frames
# are build-unique → tagged new-native?, judged by enrich not the anti-join). Triage on newDevices too.
node "$S\fetch-appcenter-crashes.js" newcrashes --owner authapp-t7qc `
--app Microsoft-Authenticator-Android-Prod-App-Center `
--version 6.2606.3817 --priors 6.2605.3042,6.2605.2973,6.2604.2550,6.2603.1485 `
--days 14 --min-count 5 --devices-new <DEV_FIRST> --out "$DATA\new-crashes.json"
# is THIS crash version-specific or pre-existing? cross-version presence + trend on the new build:
node "$S\fetch-appcenter-crashes.js" signature --owner authapp-t7qc `
--app Microsoft-Authenticator-Android-Prod-App-Center `
--version 6.2606.3817 --priors 6.2605.3042,6.2605.2973,6.2604.2550,6.2603.1485 `
--match "<codeRaw or class.method substring>" --days 27 --trend --out "$DATA\sig.json"
Read newcrashes as: a genuinely-new java frame that is OS-broad and holds a steady gap is a
real release suspect (prove it against the authenticator/ tag diff, P8); a new-native? row, a
newDevices=1 crash-loop, or any signature signature finds still firing on prior versions is
not release-caused. A young build inflates per-1k via a small device denominator — read the raw
count beside the rate.
Lead with the per-1k rate, not crash-share — a signature can take a bigger share of a smaller
crash pool while its per-device rate falls (share alone invents phantom regressions). If the step-0
preflight returned missing/invalid and the engineer chose to skip (or it returned network),
write the "Crash/stability not evaluated" placeholder and name the reason (no token / expired token /
App Center unreachable) — never skip silently.
- Attribute a confirmed new/rising crash to its code (P10 step 6). Once a signature is confirmed
genuinely-new (or a cross-version-confirmed rising per-1k gap) AND first-party + fleet-broad — not a
new-native?, tampered, single-OEM, or pre-existing frame — find the change that introduced it.
Search the EXCEPTION TOKEN, not the crashing class: a crash frame names the object the runtime was
inspecting (the victim), which is usually not the file that broke — a caller handed it to a failing
API. Set -Symbol to the exception/API token from the stack (EntryPoints.get, GeneratedComponent),
and always add -DiffGrep (a git log -G diff-text search) because -GrepRegex matches only the
commit subject and a culprit PR rarely names the subsystem it broke. Map the frame's package to its repo
(com.microsoft.authenticator.*/bastion.*/onlineid.* → authenticator/; identity.common… →
common/; broker → broker/; a dagger.*/androidx.* framework frame → the first-party caller in the
app repo) and correlate over the app's own tag range (find-suspect-prs.ps1 resolves app tags
directly and parses ADO Merged PR NNNNNNNN: → pullrequest URLs):
& "$S\find-suspect-prs.ps1" -Repos authenticator -Range 6.2606.3817..6.2606.4029 `
-Symbol 'EntryPoints.get' -DiffGrep 'EntryPoints|GeneratedComponent'
# secondary: path-log the DI graph (a module changing what it provides breaks consumers silently):
git -C authenticator log --oneline 6.2606.3817..6.2606.4029 -- **/di/ **/dagger/ **/*Module.kt
Emit a crash code-attr card in the #auth-stability section (Originator / Mechanism / Release
range / Likely PRs with honest confidence / Next step), origin-app for a confirmed first-party
regression. Environmental is the LAST resort: only after the exception-token pickaxe, the
-DiffGrep scan, AND the DI path log all come back empty consider an OS-major × build-config
interaction (shrinker/targetSdk/Play Services) — and an OS-concentration signal (e.g. "66.7% Android
16") is not evidence for it (a real caller bug concentrates on the newest OS too). Verified the hard
way: a -Symbol MfaAuthDialogActivity search missed the dagger.hilt.EntryPoints.get culprit and
tempted a wrong "Android-16 × shrinker" verdict; -Symbol 'EntryPoints.get' -DiffGrep found PR 15896454
("TOTP Secret Fix") instantly. See crash-sources.md § "Crash → PR / code attribution" for the full
frame→repo map and the verified dagger.hilt/Android-16 example.
Step 3c — Broker-via-Authenticator span drill-down + release PR correlation (conditional)
The host-app movers table (broker-top-errors-by-host-app) is a device-share (devices hitting a
code anywhere ÷ devices on that version), which dedups a device across all spans and masks a
per-span request-rate rise. So a code can look flat-to-down there while it is climbing inside one
span (e.g. invalid_grant / interaction_required on the silent path). Whenever a server-returned
auth code is suspected — or proactively for invalid_grant/interaction_required — drill down:
- Run
broker-errors-by-host-app-span.kql with <PACKAGE>=com.azure.authenticator,
<FIRST>/<SECOND> = the app versions, and <CODES> = the suspect codes (lower-cased,
comma-separated). It returns per-span_name request-level rates (errored ÷ total in that span).
- If a span rate is up, separate early-rollout churn from a real regression with a daily trend
(rate by version by day): an upgrade spike decays toward baseline as the cohort re-auths; a true
regression holds a steady gap. Report the residual, not just the headline window delta.
- Release PR correlation (for codes whose Originator is eSTS —
invalid_grant,
interaction_required, unauthorized_client, etc. — the broker/common change is the trigger,
not the source of the string). The window is the bundled broker version range, not a date
window. Read git log v<PREV_BROKER>..v<NEW_BROKER> in broker/ and common/ in full (the
range between two releases is small — tens of commits), then narrow with the pickaxe:
& "$S\find-suspect-prs.ps1" -Symbol generateAsymmetricKey -GrepRegex Asymmetric `
-Range v16.1.0..v16.2.0 -RepoRoot "<repo-root>"
Weight device-PoP / PRT / token-cache changes for silent-auth credential rejections (re-keying
or PRT churn makes eSTS reject device-bound RTs with invalid_grant, decaying as devices re-bind).
Exclude PRs that address a different code, are a fix that reduces errors, or are gated to an
SdkType the host app does not use (e.g. MSAL_CPP = OneAuth, not the Authenticator app's own MSAL).
Emit a code-attr attribution card (Originator / Mechanism / Release range / Likely PRs with honest
confidence / Next step) in the #auth-broker section.
Step 3d — Diagnose an Authenticator scenario move before you verdict
A KPI delta (a success-rate drop, an error-count or error-share rise) is a question, not a verdict.
Before any scenario row becomes "regression" prose or a HOLD, run the diagnostic ladder from
assets/docs/investigation-patterns.md. Climb only as far as
the question needs:
- Normalize, then attribute (P1–P3). Convert counts to a rate per initiation (Errors-MV
ErrorCount ÷ outcomes-MV Initiated, matched by version/day). Compare the new-build rate vs the
old-build rate — equal rates with a rising new-build count is rollout substitution, not a
regression. Then apply the code-frozen control: recompute the same rate on the previous
version; if its rate also rose, the cause is environmental (a new Android major, Play
Services / Credential Manager, server/eSTS), not this release.
- Decompose (P4–P6). Re-check on higher-volume days (early-cohort skew + thin-day variance);
break the move by
AppVersion × OsLevel × DeviceInfoMake via the *_Errors_MV_V1 companion.
One OS across all OEMs ⇒ platform driver (often amplified by that OS's adoption growth); one OEM ⇒
device bug; tracks the new version's ramp ⇒ substitution. Classify each reason as benign
(duplicate / "already registered"), abandonment (user/system cancel), or true defect, and
report a defect-only rate — a "drop" that is entirely benign/abandonment is not a quality
regression.
- Drill + prove (P7–P9). For a coarse reason, drill to the structured sub-code in raw
passkeyoperations.AllProperties (e.g. DeviceUnauthenticatedErrorCode — soft codes 5/10/13/14 =
abandonment vs hard 1/7/9 = device/defect). Use .show materialized-view <name> | project Query
to confirm what the metric counts before drilling. If the app is still the suspect, prove it
against the diff: the Authenticator is its own repo (authenticator/, base working, tags like
6.2606.3817 / v6.2605.3042) — git --no-pager diff <prevTag>..<newTag> -- <feature paths>.
Gate logic unchanged ⇒ funnel/population/environment, not code; only a steady, code-correlated,
defect-rate gap earns a WATCH/HOLD. Name in the verdict which rung explained the headline delta.
Step 4 — Compare + classify
Feed payloads to compare-versions.js:
node "$S\compare-versions.js" rows --file "$DATA\broker-reliability-by-version.json" `
--version-col broker_version --first 16.1.0 --second 16.0.1 `
--metrics SilentDevReliability,InteractiveDevReliability --threshold 1.0
node "$S\compare-versions.js" movers --file "$DATA\broker-top-errors-by-version.json" `
--key-col error_code --delta-col shareDeltaPp --lower-is-better true --top 10
node "$S\compare-versions.js" movers --file "$DATA\crash-diff.json" `
--key-col label --first-col basePer1k --second-col newPer1k `
--delta-col rateDeltaPer1k --lower-is-better true --top 10
Use --lower-is-better for latency, error-rate, and error-share metrics (down = good). Pass
--volume-col/--volume-floor so low-volume scenarios classify as low-volume, not regression.
Step 5 — Fill the report in place
Edit the bootstrapped HTML: version strings, window dates, KPI tiles (humanize counts — 585.3M,
not 585300000), table rows, and a verdict callout per app whose prose follows what the
deltas say — and, per Step 3d, names which diagnostic rung explains each headline move
(volume/substitution, code-frozen ⇒ environmental, early-cohort skew, benign/abandonment, or a real
defect-rate gap); prefer a defect-only rate over a raw success-rate "drop" that is entirely benign
or user-cancellation. Keep both app <section>s even if one is "clean/flat". For Authenticator, fill the
#auth-stability section from crash-diff.json (crashes/1k KPIs + the per-1k crash table — surface
exceptionMessage and appCodeFrame in each row, and flag rows whose firstOccurrence is inside the
window as new) and from crash-enrich.json (annotate the top crash cards with the trend tag + top
OS-major/device-model so the reader sees why it moved, per Step 3b); fold the stability verdict into
the Authenticator callout — a spike-then-decay or single-OEM/obfuscated mover is not a HOLD, an
OS-concentrated rising mover broad across models is. For any confirmed new/rising first-party crash,
add a crash code-attr card to #auth-stability (per Step 3b.5 / P10 step 6) — Originator (origin-app
for a confirmed first-party regression; reserve origin-android+origin-env for a genuine OS×build-config
interaction only after the exception-token + diff-grep + DI path-log searches all come up empty) /
Mechanism (name the victim frame and the culprit caller separately) / Release range / Likely PRs with
honest confidence / Next step; model it on the canonical card in the template's #auth-stability section.
If no crash token was available, leave
the section's appendix note that App Center crashes were not pulled. When an Authenticator version
is given, also fill the #auth-broker section ("Broker via Authenticator") from
broker-by-host-app.json + broker-top-errors-by-host-app.json: KPI tiles (device error rate +
silent/interactive reliability for the app-hosted broker) and the per-error_code movers table,
with a verdict on whether the broker regresses because of this app release. Always span-drill
the silent path (Step 3c): fill the span-breakdown sub-table (broker-errors-by-host-app-span.json)
and, when an eSTS code is elevated, the code-attr attribution card — do not write "no broker
regression" off the device-share table alone, since it masks per-span request spikes. If the
fleet-wide Broker section flags a code that this host-scoped view does not, call that out
explicitly (the fleet delta is driven by another host, e.g. Link to Windows). Mark a HOLD when a
regression dominates the headline; note early-rollout caveats (the new version's cohort skews toward
upgrade/network churn — an early spike that decays is "watch," not a clear HOLD). Leave the <head> CSS untouched.
Step 6 — Validate
& "$S\validate-report.ps1" -Path $out -BrokerVersion 16.1.0 -AuthVersion 6.2606.3817
Fix all ERRORS (exit 1). WARNINGS are advisory. Then open the file in a browser to eyeball it.
Gotchas (full list in the cheatsheet)
- Distinct devices (Broker):
dcount_hll(hll_merge(countDevicesHll)) — never sum(countDevices).
- Percentiles (Broker):
percentiles_array_tdigest(tdigest_merge(...)) — never average percentiles.
- A delta is a question, not a verdict (see investigation-patterns.md): before any Authenticator
scenario row becomes "regression," normalize counts to a rate per initiation (P1); compare the
new-build rate vs the old-build rate — a rising new-build count with equal rates is rollout
substitution, not a regression (P2); and run the code-frozen control — if the same rate also
rose on the previous version, it's environmental (OS / Play Services / Credential Manager /
eSTS), not this release (P3). Prove a real suspect against
git diff <prevTag>..<newTag> in
authenticator/ (P8) — unchanged gate logic ⇒ funnel/population, not code.
- Benign failures inflate the "Failed" bucket: the outcome MVs lump expected outcomes (duplicate /
"already registered") and user/system cancellation into
Failed, depressing the headline
success rate without anything breaking. Decompose via the *_Errors_MV_V1 companion (every
scenario has one — Error × OsLevel × AppVersion × DeviceInfoMake, with ErrorCount/ErrorDCount)
and report a defect-only rate; drill to the raw passkeyoperations.AllProperties sub-code (e.g.
DeviceUnauthenticatedErrorCode: 5/10/13/14 = abandonment, 1/7/9 = device/hard) to separate cancel
from defect. A "drop" that is entirely benign/abandonment is not a quality regression.
- Authenticator outcomes: Registration/Auth MVs have only
Initiated/Succeeded/Failed (+DCount);
PN completion needs the init-MV ⋈ _Results_MV_V1 join. MSA NGC vs SA splits on IsNGC on both join sides.
- Volume guard: < ~1K initiates = noise, not a regression.
- Broker per host app: the Broker MVs carry
active_broker_package_name (host) +
AppInfo_Version (= the host app's version for that package, e.g. Authenticator 6.2606.3817,
not broker_version). Filter active_broker_package_name == "com.azure.authenticator" and
compare by AppInfo_Version to attribute broker movement to the app rollout; never attribute a
fleet-wide broker_version delta to an app (Link to Windows ≈122 M devices can dominate it).
- Crashes — share ≠ rate: lead with crashes per 1k active devices (App Center count ÷ Kusto
devices), not crash-share; App Center
errorGroupId is version-scoped so diff joins on the
crash signature, and App Center's native crash-free metric is retired. See crash-sources.md.
- Crashes — page to the total, then read the message and dimensions: there is no aggregate-total
endpoint, so the crash total (rate numerator + share denominator) is only the pages you fetch — a
busy version has 1,300+ groups, so use
--page-cap 0 for a verdict or the rate is undercounted.
exceptionMessage is usually the whole story (a RemoteException reading validateForegroundServiceType
= Android FGS enforcement; a NotSerializableException naming the class). Don't verdict a crash mover on
count alone — enrich its trend (spike-then-decay = rollout churn, not a HOLD) and OS/model
(broad-across-models + OS-concentrated = platform/release; one rugged OEM on obfuscated frames = tamper/sideload).
- Crashes — "new" is the #1 false alarm (
firstOccurrence = rollout date): App Center's
per-version firstOccurrence is when that version shipped, NOT when the signature first appeared,
so a years-old crash shows a first-seen inside your window and a per-1k rate that "rose" (young
build = small device denominator). diff status=new only means "absent from the single baseline."
To actually find new crashes use newcrashes --priors v1,v2,v3,v4 (anti-join vs a union of
priors; java-frame genuinely-new is the signal, native/hex new-native? is build-unique noise,
triage on newDevices), and confirm any suspect with signature --match … --trend (cross-version
presence). A signature still firing on prior versions is pre-existing/environmental, not this release.
Verified: the okhttp FileSystem$1.rename:87 IOException looked new+regressing on 6.2606 but exists
on every version back to 6.2601. See crash-sources.md gotcha #3 and investigation-patterns P10.
- Device-share masks per-span spikes: the host-app movers table dedups a device across all spans,
so a code can look flat/down there while its per-request rate climbs inside one span. Re-slice with
broker-errors-by-host-app-span.kql (request-level rate by span_name) before declaring "no broker
regression." Separate early-rollout decay from a steady gap with a daily trend.
- Release PR correlation: for eSTS-returned codes (
invalid_grant, interaction_required) the
trigger is in the bundled broker version range — read git log v<PREV>..v<NEW> in broker/+common/
in full, then find-suspect-prs.ps1 -Range. Weight device-PoP/PRT/cache changes; exclude different-code
fixes and SdkType-gated PRs (MSAL_CPP = OneAuth, not the Authenticator app).
- Secrets: the App Center token and anything under
~/.android-release-reports/ stay out of
the repo and out of the report — never echo or paste them.
- UTF-8 trap: never write the HTML through a PowerShell
@'…'@ heredoc (it strips emoji/arrows).
Use the edit/create tools or [IO.File]::WriteAllText($p,$t,[System.Text.UTF8Encoding]::new($false)).
- Filename collision: a populated report is never silently overwritten — bootstrap halts unless
-Force.
1---2name: release-monitoring-report3description: Generate a version-over-version release-health report for the Android Broker and/or the Authenticator app as one polished self-contained HTML file. Use this skill when monitoring a rollout — triggers include "monitor this release", "release health report", "broker rollout health", "authenticator rollout", "is this release safe to widen", "what's changing this release and why", "crash regression", "stability report", or "compare a new version vs a previous version". Accepts a Broker version and/or an Authenticator version being rolled out, plus a previous/baseline version per app (or an all-versions baseline), runs Kusto queries for each app to quantify what changed and why, adds an Authenticator crash/stability layer from App Center (crashes per 1k active devices), and writes the HTML to the android-release-reports folder under the user's home directory (outside the workspace so reports are never committed).4---56# Release Monitoring Report78Produce a **version-over-version** release-health report covering the Android **Broker** and/or9the **Authenticator** app in one self-contained HTML file at10`$env:USERPROFILE\android-release-reports\release-report-broker-<bv>-auth-<av>-<YYYY-MM-DD>.html`11(home folder, **outside the workspace** — reports never get committed). Omitted apps drop out12of the filename, so broker-only or authenticator-only runs are first-class.1314The report answers two questions per app: **what changed** this release (KPIs + tables vs the15baseline) and **why** (error-code movers / per-scenario deltas). It ends in a clear **verdict**16per app: SAFE to proceed / WATCH / HOLD.1718The output mirrors the canonical template at19[`assets/templates/report-template.html`](assets/templates/report-template.html) — a real filled20example kept as the structural + visual reference. The Step 1 bootstrap script copies it into21`~/android-release-reports/…` and **you edit it in place** (versions, dates, KPI values, table22rows, verdict prose). Do **not** redesign the layout.2324**Before writing any KQL, read the three reference files:**25- [`assets/docs/kusto-cheatsheet.md`](assets/docs/kusto-cheatsheet.md) — how to run queries26 (auth, `run-kql.ps1` invocation, output JSON shape, token substitution, the end-to-end loop,27 version-resolution recipes, hard gotchas).28- [`assets/queries/README.md`](assets/queries/README.md) — the query catalog: what each `.kql`29 computes, the token convention, and the Authenticator scenario→MV→column map.30- [`assets/docs/investigation-patterns.md`](assets/docs/investigation-patterns.md) — **the diagnostic31 methodology**: the count-vs-rate / version-attribution / code-frozen-control / rollout-cohort /32 benign-vs-real / dimensional-decomposition / drill-to-sub-code / release-tag-diff / MV-introspection /33 crash-version-attribution (P10) patterns, plus the decision flow. A KPI delta — or a crash that looks34 new — is a question, not a verdict; run these before any table row becomes "regression" prose or a HOLD.3536**For the Authenticator crash/stability layer, also read:**37- [`assets/docs/crash-sources.md`](assets/docs/crash-sources.md) — App Center crash pull (auth/token,38 `errorGroups` endpoint, the `nextLink` quirk), the **share-vs-rate trap**, the Kusto rate39 denominator, secret handling, and the deferred Play Console (Phase 2). Authenticator only — the40 Broker is not a store app and has no crash section.4142## Inputs to confirm4344Ask only for what's missing; infer the rest.45461. **Which app(s)** — Broker version rolling out, Authenticator version rolling out, or both.47 At least one is required.482. **Baseline per app** — the previous release version to compare against, OR "auto-resolve the49 previous version" (pick the next-highest-volume recent version), OR "all-versions baseline".503. **Window** — default last **30 days** (Authenticator MVs) / **14 days** (Broker). The new51 version is usually young, so a longer window mostly grows the baseline cohort.524. **Report date** — defaults to today (used in the filename + the "Generated" banner).535. **Authenticator crash layer (optional)** — if an Authenticator version is given, add the54 crash/stability section. It needs an App Center read-only token (`~/.android-release-reports/appcenter.token`,55 `$APPCENTER_API_TOKEN`, or `--token-file`). **Step 3b step 0 auto-runs56 [`preflight-appcenter.ps1`](assets/scripts/preflight-appcenter.ps1) `-Check`** and, if the token is57 missing/expired, posts an **unmissable action block** for the engineer to run `-Setup` once, then58 **blocks on an auto-poll** and won't finalize the report until a token validates or the engineer59 explicitly skips (never a silent skip). The token is a60 **secret** — never echo it or write it into the report.6162If the user gives versions but not the baseline, run the resolution queries first and propose63`<FIRST>` (rolling out) / `<SECOND>` (previous) from volume, then continue.6465## Data sources (summary — full detail in the cheatsheet)6667| App | Cluster | Database | Version dim | Time col |68|-----|---------|----------|-------------|----------|69| Broker | `https://idsharedeus2.kusto.windows.net` | `ad-accounts-android-otel` | `broker_version` (`16.1.0`) | `EventInfo_Time` |70| Authenticator | `https://idsharedeus2.eastus2.kusto.windows.net` | `d496be22d62a46b0a3cf67ea2e736fd8` | `AppVersion` (`6.2606.3817`) | `EventDate` (MVs) |7172`run-kql.ps1` defaults to Broker; pass `-Cluster`/`-Database` for Authenticator. Requires73`az login` (Android Auth Client SDK security group).7475## Bundled assets7677| File | Purpose |78|---|---|79| [`templates/report-template.html`](assets/templates/report-template.html) | Canonical layout — a real filled example. **Edit in place**; do not restyle. The CSS in `<head>` is canonical. |80| [`docs/kusto-cheatsheet.md`](assets/docs/kusto-cheatsheet.md) | Operational runbook: run-kql usage, JSON shape, token substitution, end-to-end loop, gotchas. |81| [`docs/investigation-patterns.md`](assets/docs/investigation-patterns.md) | **Diagnostic methodology** — count-vs-rate, version-attribution-vs-substitution, code-frozen control, rollout-cohort, benign-vs-real classification, dimensional decomposition, drill-to-sub-code, release-tag diff, MV introspection, **crash version-attribution (P10 — is a crash new/release-caused?)**, plus the Authenticator drill ladder (outcomes MV → `*_Errors_MV_V1` → raw `passkeyoperations`) and a decision flow. Apply before calling any delta a regression. |82| [`docs/crash-sources.md`](assets/docs/crash-sources.md) | Authenticator crash layer: App Center auth/endpoint, the three gotchas (`errorGroupId`-is-version-scoped, share-vs-rate, **`firstOccurrence`-is-rollout-date**), the **`newcrashes`/`signature` new-crash detection** flow, Kusto rate denominator, secret handling, Play Console Phase 2. |83| [`queries/README.md`](assets/queries/README.md) | Query catalog + Authenticator scenario→MV→column map. |84| [`queries/*.kql`](assets/queries/) | 6 Broker + 6 Authenticator scenario templates + `authenticator-crash-denominator.kql` (crash-rate denominator), all validated live. Substitute `<TOKENS>` before running. Includes `broker-errors-by-host-app-span.kql` — the per-`span_name` request-rate drill-down that complements the host-app device-share movers. |85| [`scripts/run-kql.ps1`](assets/scripts/run-kql.ps1) | Direct-REST Kusto helper. `-Query`/`-Out` mandatory; `-Cluster`/`-Database` for Authenticator. |86| [`scripts/find-suspect-prs.ps1`](assets/scripts/find-suspect-prs.ps1) | Release PR correlation. **Auth-code** (eSTS) attribution: defaults to `broker/`+`common/` over a broker tag range (`-Range v16.1.0..v16.2.0`; broker uses its own tags, common maps via the broker submodule pointer). **Crash** attribution: `-Repos authenticator` over the app tag range (`-Range 6.2606.3817..6.2606.4029`) — resolves the app's own tags and parses ADO `Merged PR NNNNNNNN:` → pullrequest URLs. Three search streams: `-S` pickaxe + `-DiffGrep` (`git log -G` over diff text) + `--grep` (subject). **For a crash, set `-Symbol` to the exception/API token from the stack (e.g. `EntryPoints.get`), not the crashing class, and always pass `-DiffGrep`** — the crashing class is the victim, the culprit is a caller whose subject rarely names the subsystem. Prints PR ids + URLs for attribution cards. |87| [`scripts/preflight-appcenter.ps1`](assets/scripts/preflight-appcenter.ps1) | App Center token gate for the crash layer. **`-Check`** (non-interactive, the skill auto-runs it) resolves the token (`--token-file` → `$APPCENTER_API_TOKEN` → `~/.android-release-reports/appcenter.token`) and validates it with one `GET /apps/{owner}/{app}`, emitting `STATUS: ok\|missing\|invalid\|no-access\|network` + exit `0/2/3/4/5` (+ `-Json`). Add **`-Wait <sec>` `-IntervalSec <sec>`** to block-poll until a valid token appears (auto-resume, no handshake). **`-Setup`** (interactive — the engineer runs it once in a real terminal) opens the token page, reads the token via `Read-Host -AsSecureString` (never echoed), validates, then saves with a user-only ACL. Never prints the token. |88| [`scripts/fetch-appcenter-crashes.js`](assets/scripts/fetch-appcenter-crashes.js) | Pull Authenticator crash clusters from App Center → run-kql array-form JSON. `groups` (one version) + `diff` (two versions, signature-joined, per-1k rate when given Kusto denominators) + `enrich` (top signatures' daily **trend** + instance-sampled **OS-major/device-model** concentration) + **`newcrashes`** (genuinely-new java-frame signatures via anti-join against a **union of priors**, native/hex frames split out as `new-native?`) + **`signature`** (cross-version presence of one signature + trend — "is crash X version-specific?"). Captures `exceptionMessage`/`appCodeFrame`/`firstOccurrence`, drops `hidden`/`Ignored` groups, and `--page-cap 0` exhausts paging for an accurate total. |89| [`scripts/bootstrap-report.ps1`](assets/scripts/bootstrap-report.ps1) | Copy the template to a version-named file, create `_data/<slug>-<date>/`, stamp the Generated date, prune old `_data`, detect unfilled-stub vs real-report collisions. |90| [`scripts/compare-versions.js`](assets/scripts/compare-versions.js) | Delta + classification engine over run-kql JSON. `rows` mode (version-per-row metrics) and `movers` mode (paired error-share rows). Thresholds + volume guard. |91| [`scripts/validate-report.ps1`](assets/scripts/validate-report.ps1) | Pre-publish validator: stale tokens, mojibake, raw-count leaks, version-string presence, both app sections, verdict callouts. |9293## Workflow9495### Step 1 — Bootstrap96```powershell97$S = ".github/skills/release-monitoring-report/assets/scripts"98$out = & "$S\bootstrap-report.ps1" -BrokerVersion 16.1.0 -AuthVersion 6.2606.381799```100Omit either `-…Version` for a single-app run. The script prints the report path and creates the101`_data/<slug>-<date>/` folder (`$DATA`) for raw query payloads. Re-running on an unfilled stub102is silent; a populated report halts unless you pass `-Force`.103104### Step 2 — Resolve versions105If the baseline wasn't given, run `broker-adoption.kql` and the cheap Authenticator resolver106(cheatsheet § "Version resolution recipes") to pick `<FIRST>`/`<SECOND>` by volume. For an107"all-versions baseline", drop the version filter / list every version in `<VERSIONS>`.108109### Step 3 — Pull queries (both apps)110For each needed `.kql`: read it, substitute tokens (cheatsheet § "Filling tokens"), run via111`run-kql.ps1 -Out $DATA\<name>.json`. Run independent queries in parallel (PowerShell jobs).112Minimum useful set:113- **Broker:** `broker-adoption`, `broker-reliability-by-version`, `broker-error-rate-by-version`,114 `broker-top-errors-by-version` (the "why"), `broker-latency-by-version`.115- **Broker via Authenticator (when an Authenticator version is given):** also run116 `broker-by-host-app` and `broker-top-errors-by-host-app` with `<PACKAGE>=com.azure.authenticator`117 and `<FIRST>`/`<SECOND>` = the **Authenticator app versions** (these MVs key the host's broker on118 `AppInfo_Version`, which for that package is the app version). This isolates whether the broker119 regresses *because of this app rollout*, separate from the fleet-wide `broker_version` view.120- **Authenticator:** `auth-version-resolve` (or cheap fallback), then per top scenario:121 `auth-scenario-success-rate` (Registration/Auth) or `auth-pn-checkforauth-completion` (PN+CFA),122 always alongside `auth-scenario-initiates` for the volume guard. Use `auth-stats` for adoption.123124### Step 3b — Authenticator crash layer (optional)125If an Authenticator version is given, **read126[`assets/docs/crash-sources.md`](assets/docs/crash-sources.md) first**, then gate on the token:1271280. **Preflight the App Center token (do this first — don't silently skip).** Auto-run:129```powershell130pwsh -NoProfile -File "$S\preflight-appcenter.ps1" -Check -Json131```132 Branch on the `STATUS:` line / exit code:133 - **`ok`** (exit 0) → proceed to step 1 (fully automatic, no user action).134 - **`missing`** (2) / **`invalid`** (3) → the token is absent or expired. **Don't write the135 placeholder yet, and DO NOT just tell the engineer and move on** — set up a seamless capture +136 auto-resume. Split this across the run so the engineer's paste window overlaps the Kusto work:137 **prompt EARLY (step a — the moment `-Check` first returns not-ok, even before the main queries),138 then block LATE (step b — the foreground poll, at the crash layer, which is the report's last139 section).** Both are hard requirements; skipping either is a bug:140141 **a. Surface an UNMISSABLE action block.** Post the setup ask as its own dedicated, visually142 prominent message — a `⚠️ ACTION REQUIRED` header, one line stating *"the report's143 crash/stability section is blocked until you do this,"* and the exact command. Do **not** bury144 it mid-paragraph, trail it with "meanwhile I'll continue," or imply the report will finish145 without it — that both makes engineers miss it and tempts you to drop the gate in step b.146 Open the best capture surface for the host and launch `-Setup` there:147 - **Desktop app** (Terminal canvas available) → open a **Terminal canvas** and run148 `pwsh -File "<ABSOLUTE>\preflight-appcenter.ps1" -Setup` in it, so the engineer pastes149 without leaving the app.150 - **VS Code Copilot Chat** → tell the engineer to run that same command in the **integrated151 terminal** (they already have one open).152 - **Copilot CLI / anything else** → surface the command for the engineer's **own terminal**.153154 In every case relay the **ABSOLUTE** script path that `preflight -Check` just printed (its155 message uses `$PSCommandPath`). **Never a repo-relative path** like156 `.github\…\preflight-appcenter.ps1` — the engineer's terminal is usually at their home157 directory, where a relative path fails with *"not recognized as the name of a script file"*.158159 **b. Run the blocking poll in the FOREGROUND as a single step and wait on its result — do NOT160 background it and "check later."** This is the enforcement mechanism, and the most important rule161 in this branch. **Observed failure:** when the poll is launched detached (or merely described as162 running "in parallel"), the agent posts the message, moves on, and the poll *never actually runs* —163 so pasting the token resumes nothing and the crash layer silently never happens. The crash layer is164 the **last** section of the report, so by the time you reach this branch every other Kusto query is165 already done and there is nothing left to parallelize. Run it as an ordinary blocking command and166 treat its return as a hard gate:167 ```powershell168 pwsh -NoProfile -File "$S\preflight-appcenter.ps1" -Check -Json -Wait 180 -IntervalSec 10169 ```170 **Wait for this call to return before writing the crash/stability section or declaring the report171 complete — the blocking call *is* the wait.** It returns `ok` the instant the engineer finishes172 `-Setup` (they never say "done"). On timeout (`missing`/`invalid` after 180s), **stop and explicitly173 ask**: keep waiting (re-run the poll) or skip crashes this run. Never silently skip, and never174 finalize past a pending or timed-out poll without an explicit skip.175 - **`no-access`** (4) → their token is valid but not scoped to `authapp-t7qc`; tell them to generate a176 read-only token in the right org, then `-Setup`.177 - **`network`** (5) → App Center unreachable; note it in the report and skip the crash layer this run.1781. Run `authenticator-crash-denominator.kql` (auth cluster) to get active devices for `<FIRST>`/`<SECOND>`.1792. Pull + pair crashes (signature-joined; pass the two device counts so it computes the per-1k rate).180 Use **`--page-cap 0`** for a verdict so the crash total isn't undercounted (there's no aggregate-total181 endpoint — the total is only the pages fetched, and a busy version has 1,300+ groups):182```powershell183node "$S\fetch-appcenter-crashes.js" diff --owner authapp-t7qc `184 --app Microsoft-Authenticator-Android-Prod-App-Center `185 --version 6.2606.3817 --base 6.2605.3042 --days 14 --page-cap 0 `186 --devices-new <DEV_FIRST> --devices-base <DEV_SECOND> --out "$DATA\crash-diff.json"187```188 The diff now also carries `exceptionMessage`, `appCodeFrame` (`class.method:line`), `firstOccurrence`,189 `appBuild`, `state`, and **drops team-muted `hidden`/`Ignored` groups** by default — fill these into190 the crash cards. **`diff status=new` is only a CANDIDATE, not a verdict** — it means "absent from the191 single `--base`," and `firstOccurrence` is the version's **rollout date**, not the signature's192 app-history first-seen (a years-old crash shows a first-seen inside your window). Confirm any "new"193 with step 4 below.1943. **Enrich the top movers** — the list view can't show a crash's **trend** or **which OS/OEM** it hits,195 so pull both (these answer the P4/P6 patterns for crashes):196```powershell197node "$S\fetch-appcenter-crashes.js" enrich --owner authapp-t7qc `198 --app Microsoft-Authenticator-Android-Prod-App-Center `199 --version 6.2606.3817 --days 14 --top 8 --out "$DATA\crash-enrich.json"200```201 It returns, per top signature, a **daily-trend tag** (`rising`/`decaying`/`spike-then-decay`/`steady`202 + peak/last day) and an instance-sampled **top OS-major + device-model concentration** (the aggregate203 OS/model endpoints 404, so this samples `errorGroups/{id}/errors`). Read it as: OS-concentrated +204 rising + broad-across-models ⇒ platform/release suspect; model-concentrated on rugged/obfuscated205 frames ⇒ tamper/sideload, not app quality; spike-then-decay ⇒ early-rollout churn, not a HOLD.2064. **Find genuinely-new crashes (P10)** — to answer "what crashes are new in this release," anti-join207 the new build's signatures against a **union of recent priors** (not just the baseline — `diff`'s208 single-baseline `new` flag false-positives a crash that skipped one version). Then cross-version-209 confirm any suspect with `signature`:210```powershell211# genuinely-new = java-frame signature absent from ALL priors in the 27-day window (native/hex frames212# are build-unique → tagged new-native?, judged by enrich not the anti-join). Triage on newDevices too.213node "$S\fetch-appcenter-crashes.js" newcrashes --owner authapp-t7qc `214 --app Microsoft-Authenticator-Android-Prod-App-Center `215 --version 6.2606.3817 --priors 6.2605.3042,6.2605.2973,6.2604.2550,6.2603.1485 `216 --days 14 --min-count 5 --devices-new <DEV_FIRST> --out "$DATA\new-crashes.json"217# is THIS crash version-specific or pre-existing? cross-version presence + trend on the new build:218node "$S\fetch-appcenter-crashes.js" signature --owner authapp-t7qc `219 --app Microsoft-Authenticator-Android-Prod-App-Center `220 --version 6.2606.3817 --priors 6.2605.3042,6.2605.2973,6.2604.2550,6.2603.1485 `221 --match "<codeRaw or class.method substring>" --days 27 --trend --out "$DATA\sig.json"222```223 Read `newcrashes` as: a `genuinely-new` **java** frame that is OS-broad and holds a steady gap is a224 real release suspect (prove it against the `authenticator/` tag diff, P8); a `new-native?` row, a225 `newDevices=1` crash-loop, or any signature `signature` finds still firing on prior versions is226 **not** release-caused. A young build inflates per-1k via a small device denominator — read the raw227 **count** beside the rate.228229**Lead with the per-1k rate, not crash-share** — a signature can take a bigger share of a smaller230crash pool while its per-device rate falls (share alone invents phantom regressions). If the step-0231preflight returned `missing`/`invalid` and the engineer chose to skip (or it returned `network`),232write the "Crash/stability not evaluated" placeholder and name the reason (no token / expired token /233App Center unreachable) — never skip silently.2342355. **Attribute a confirmed new/rising crash to its code (P10 step 6).** Once a signature is confirmed236 genuinely-new (or a cross-version-confirmed rising per-1k gap) AND first-party + fleet-broad — not a237 `new-native?`, tampered, single-OEM, or pre-existing frame — find the change that introduced it.238 **Search the EXCEPTION TOKEN, not the crashing class:** a crash frame names the object the runtime was239 inspecting (the *victim*), which is usually not the file that broke — a *caller* handed it to a failing240 API. Set `-Symbol` to the exception/API token from the stack (`EntryPoints.get`, `GeneratedComponent`),241 and **always** add `-DiffGrep` (a `git log -G` diff-text search) because `-GrepRegex` matches only the242 commit subject and a culprit PR rarely names the subsystem it broke. Map the frame's package to its repo243 (`com.microsoft.authenticator.*`/`bastion.*`/`onlineid.*` → `authenticator/`; `identity.common…` →244 `common/`; broker → `broker/`; a `dagger.*`/`androidx.*` framework frame → the first-party caller in the245 app repo) and correlate over the **app's own tag range** (`find-suspect-prs.ps1` resolves app tags246 directly and parses ADO `Merged PR NNNNNNNN:` → pullrequest URLs):247```powershell248& "$S\find-suspect-prs.ps1" -Repos authenticator -Range 6.2606.3817..6.2606.4029 `249 -Symbol 'EntryPoints.get' -DiffGrep 'EntryPoints|GeneratedComponent'250# secondary: path-log the DI graph (a module changing what it provides breaks consumers silently):251git -C authenticator log --oneline 6.2606.3817..6.2606.4029 -- **/di/ **/dagger/ **/*Module.kt252```253 Emit a crash `code-attr` card in the **`#auth-stability`** section (Originator / Mechanism / Release254 range / Likely PRs with honest confidence / Next step), `origin-app` for a confirmed first-party255 regression. **Environmental is the LAST resort:** only after the exception-token pickaxe, the256 `-DiffGrep` scan, AND the DI path log all come back empty consider an **OS-major × build-config**257 interaction (shrinker/`targetSdk`/Play Services) — and an OS-concentration signal (e.g. "66.7% Android258 16") is **not** evidence for it (a real caller bug concentrates on the newest OS too). Verified the hard259 way: a `-Symbol MfaAuthDialogActivity` search missed the `dagger.hilt.EntryPoints.get` culprit and260 tempted a wrong "Android-16 × shrinker" verdict; `-Symbol 'EntryPoints.get' -DiffGrep` found PR 15896454261 ("TOTP Secret Fix") instantly. See `crash-sources.md` § "Crash → PR / code attribution" for the full262 frame→repo map and the verified `dagger.hilt`/Android-16 example.263264### Step 3c — Broker-via-Authenticator span drill-down + release PR correlation (conditional)265The host-app movers table (`broker-top-errors-by-host-app`) is a **device-share** (devices hitting a266code anywhere ÷ devices on that version), which dedups a device across all spans and **masks a267per-span request-rate rise**. So a code can look flat-to-down there while it is climbing inside one268span (e.g. `invalid_grant` / `interaction_required` on the **silent** path). Whenever a server-returned269auth code is suspected — or proactively for `invalid_grant`/`interaction_required` — drill down:2701. Run `broker-errors-by-host-app-span.kql` with `<PACKAGE>=com.azure.authenticator`,271 `<FIRST>`/`<SECOND>` = the app versions, and `<CODES>` = the suspect codes (lower-cased,272 comma-separated). It returns per-`span_name` **request-level** rates (errored ÷ total in that span).2732. If a span rate is up, separate **early-rollout churn** from a real regression with a daily trend274 (rate by version by day): an upgrade spike *decays* toward baseline as the cohort re-auths; a true275 regression holds a steady gap. Report the residual, not just the headline window delta.2763. **Release PR correlation** (for codes whose Originator is **eSTS** — `invalid_grant`,277 `interaction_required`, `unauthorized_client`, etc. — the broker/common change is the *trigger*,278 not the source of the string). The window is the **bundled broker version range**, not a date279 window. Read `git log v<PREV_BROKER>..v<NEW_BROKER>` in `broker/` and `common/` **in full** (the280 range between two releases is small — tens of commits), then narrow with the pickaxe:281```powershell282& "$S\find-suspect-prs.ps1" -Symbol generateAsymmetricKey -GrepRegex Asymmetric `283 -Range v16.1.0..v16.2.0 -RepoRoot "<repo-root>"284```285 Weight **device-PoP / PRT / token-cache** changes for silent-auth credential rejections (re-keying286 or PRT churn makes eSTS reject device-bound RTs with `invalid_grant`, decaying as devices re-bind).287 Exclude PRs that address a *different* code, are a *fix that reduces* errors, or are gated to an288 SdkType the host app does not use (e.g. `MSAL_CPP` = OneAuth, not the Authenticator app's own MSAL).289 Emit a `code-attr` attribution card (Originator / Mechanism / Release range / Likely PRs with honest290 confidence / Next step) in the `#auth-broker` section.291292### Step 3d — Diagnose an Authenticator scenario move before you verdict293A KPI delta (a success-rate drop, an error-count or error-share rise) is a **question**, not a verdict.294Before any scenario row becomes "regression" prose or a HOLD, run the diagnostic ladder from295[`assets/docs/investigation-patterns.md`](assets/docs/investigation-patterns.md). Climb only as far as296the question needs:2971. **Normalize, then attribute (P1–P3).** Convert counts to a **rate per initiation** (Errors-MV298 `ErrorCount` ÷ outcomes-MV `Initiated`, matched by version/day). Compare the **new-build rate vs the299 old-build rate** — equal rates with a rising new-build count is rollout **substitution**, not a300 regression. Then apply the **code-frozen control**: recompute the same rate on the **previous**301 version; if *its* rate also rose, the cause is **environmental** (a new Android major, Play302 Services / Credential Manager, server/eSTS), not this release.3032. **Decompose (P4–P6).** Re-check on **higher-volume days** (early-cohort skew + thin-day variance);304 break the move by `AppVersion × OsLevel × DeviceInfoMake` via the **`*_Errors_MV_V1`** companion.305 One OS across all OEMs ⇒ platform driver (often amplified by that OS's adoption growth); one OEM ⇒306 device bug; tracks the new version's ramp ⇒ substitution. Classify each reason as **benign**307 (duplicate / "already registered"), **abandonment** (user/system cancel), or **true defect**, and308 report a **defect-only** rate — a "drop" that is entirely benign/abandonment is **not** a quality309 regression.3103. **Drill + prove (P7–P9).** For a coarse reason, drill to the structured sub-code in raw311 `passkeyoperations.AllProperties` (e.g. `DeviceUnauthenticatedErrorCode` — soft codes 5/10/13/14 =312 abandonment vs hard 1/7/9 = device/defect). Use `.show materialized-view <name> | project Query`313 to confirm what the metric counts before drilling. If the app is still the suspect, **prove it314 against the diff**: the Authenticator is its own repo (`authenticator/`, base `working`, tags like315 `6.2606.3817` / `v6.2605.3042`) — `git --no-pager diff <prevTag>..<newTag> -- <feature paths>`.316 Gate logic unchanged ⇒ funnel/population/environment, not code; only a steady, code-correlated,317 defect-rate gap earns a WATCH/HOLD. Name in the verdict **which** rung explained the headline delta.318319### Step 4 — Compare + classify320Feed payloads to `compare-versions.js`:321```powershell322node "$S\compare-versions.js" rows --file "$DATA\broker-reliability-by-version.json" `323 --version-col broker_version --first 16.1.0 --second 16.0.1 `324 --metrics SilentDevReliability,InteractiveDevReliability --threshold 1.0325node "$S\compare-versions.js" movers --file "$DATA\broker-top-errors-by-version.json" `326 --key-col error_code --delta-col shareDeltaPp --lower-is-better true --top 10327node "$S\compare-versions.js" movers --file "$DATA\crash-diff.json" `328 --key-col label --first-col basePer1k --second-col newPer1k `329 --delta-col rateDeltaPer1k --lower-is-better true --top 10330```331Use `--lower-is-better` for latency, error-rate, and error-share metrics (down = good). Pass332`--volume-col`/`--volume-floor` so low-volume scenarios classify as `low-volume`, not regression.333334### Step 5 — Fill the report in place335Edit the bootstrapped HTML: version strings, window dates, KPI tiles (humanize counts — `585.3M`,336not `585300000`), table rows, and a **verdict callout per app** whose prose follows what the337deltas say — and, per Step 3d, **names which diagnostic rung** explains each headline move338(volume/substitution, code-frozen ⇒ environmental, early-cohort skew, benign/abandonment, or a real339defect-rate gap); prefer a **defect-only** rate over a raw success-rate "drop" that is entirely benign340or user-cancellation. Keep both app `<section>`s even if one is "clean/flat". For Authenticator, fill the341`#auth-stability` section from `crash-diff.json` (crashes/1k KPIs + the per-1k crash table — surface342`exceptionMessage` and `appCodeFrame` in each row, and flag rows whose `firstOccurrence` is inside the343window as **new**) and from `crash-enrich.json` (annotate the top crash cards with the trend tag + top344OS-major/device-model so the reader sees *why* it moved, per Step 3b); fold the stability verdict into345the Authenticator callout — a spike-then-decay or single-OEM/obfuscated mover is **not** a HOLD, an346OS-concentrated rising mover broad across models is. **For any confirmed new/rising first-party crash,347add a crash `code-attr` card** to `#auth-stability` (per Step 3b.5 / P10 step 6) — Originator (`origin-app`348for a confirmed first-party regression; reserve `origin-android`+`origin-env` for a genuine OS×build-config349interaction only after the exception-token + diff-grep + DI path-log searches all come up empty) /350Mechanism (name the victim frame and the culprit caller separately) / Release range / Likely PRs with351honest confidence / Next step; model it on the canonical card in the template's `#auth-stability` section.352If no crash token was available, leave353the section's appendix note that App Center crashes were not pulled. When an Authenticator version354is given, also fill the **`#auth-broker`** section ("Broker via Authenticator") from355`broker-by-host-app.json` + `broker-top-errors-by-host-app.json`: KPI tiles (device error rate +356silent/interactive reliability for the app-hosted broker) and the per-`error_code` movers table,357with a verdict on whether the broker regresses *because of this app release*. **Always span-drill358the silent path** (Step 3c): fill the span-breakdown sub-table (`broker-errors-by-host-app-span.json`)359and, when an eSTS code is elevated, the `code-attr` attribution card — do **not** write "no broker360regression" off the device-share table alone, since it masks per-span request spikes. If the361fleet-wide Broker section flags a code that this host-scoped view does **not**, call that out362explicitly (the fleet delta is driven by another host, e.g. Link to Windows). Mark a HOLD when a363regression dominates the headline; note early-rollout caveats (the new version's cohort skews toward364upgrade/network churn — an early spike that decays is "watch," not a clear HOLD). Leave the `<head>` CSS untouched.365366### Step 6 — Validate367```powershell368& "$S\validate-report.ps1" -Path $out -BrokerVersion 16.1.0 -AuthVersion 6.2606.3817369```370Fix all ERRORS (exit 1). WARNINGS are advisory. Then open the file in a browser to eyeball it.371372## Gotchas (full list in the cheatsheet)373- **Distinct devices (Broker):** `dcount_hll(hll_merge(countDevicesHll))` — never `sum(countDevices)`.374- **Percentiles (Broker):** `percentiles_array_tdigest(tdigest_merge(...))` — never average percentiles.375- **A delta is a question, not a verdict (see investigation-patterns.md):** before any Authenticator376 scenario row becomes "regression," normalize counts to a **rate per initiation** (P1); compare the377 **new-build rate vs the old-build rate** — a rising new-build *count* with equal rates is rollout378 **substitution**, not a regression (P2); and run the **code-frozen control** — if the same rate also379 rose on the **previous** version, it's environmental (OS / Play Services / Credential Manager /380 eSTS), not this release (P3). Prove a real suspect against `git diff <prevTag>..<newTag>` in381 `authenticator/` (P8) — unchanged gate logic ⇒ funnel/population, not code.382- **Benign failures inflate the "Failed" bucket:** the outcome MVs lump expected outcomes (duplicate /383 "already registered") and user/system **cancellation** into `Failed`, depressing the headline384 success rate without anything breaking. Decompose via the **`*_Errors_MV_V1`** companion (every385 scenario has one — `Error × OsLevel × AppVersion × DeviceInfoMake`, with `ErrorCount`/`ErrorDCount`)386 and report a **defect-only** rate; drill to the raw `passkeyoperations.AllProperties` sub-code (e.g.387 `DeviceUnauthenticatedErrorCode`: 5/10/13/14 = abandonment, 1/7/9 = device/hard) to separate cancel388 from defect. A "drop" that is entirely benign/abandonment is **not** a quality regression.389- **Authenticator outcomes:** Registration/Auth MVs have only `Initiated/Succeeded/Failed` (+DCount);390 PN completion needs the init-MV ⋈ `_Results_MV_V1` join. MSA NGC vs SA splits on `IsNGC` on **both** join sides.391- **Volume guard:** < ~1K initiates = noise, not a regression.392- **Broker per host app:** the Broker MVs carry `active_broker_package_name` (host) +393 `AppInfo_Version` (= the host app's version for that package, e.g. Authenticator `6.2606.3817`,394 not `broker_version`). Filter `active_broker_package_name == "com.azure.authenticator"` and395 compare by `AppInfo_Version` to attribute broker movement to the app rollout; never attribute a396 fleet-wide `broker_version` delta to an app (Link to Windows ≈122 M devices can dominate it).397- **Crashes — share ≠ rate:** lead with crashes per 1k active devices (App Center count ÷ Kusto398 devices), not crash-share; App Center `errorGroupId` is version-scoped so `diff` joins on the399 crash signature, and App Center's native crash-free metric is retired. See `crash-sources.md`.400- **Crashes — page to the total, then read the message and dimensions:** there is **no aggregate-total401 endpoint**, so the crash total (rate numerator + share denominator) is *only* the pages you fetch — a402 busy version has 1,300+ groups, so use `--page-cap 0` for a verdict or the rate is undercounted.403 `exceptionMessage` is usually the whole story (a `RemoteException` reading `validateForegroundServiceType`404 = Android FGS enforcement; a `NotSerializableException` naming the class). Don't verdict a crash mover on405 count alone — `enrich` its **trend** (spike-then-decay = rollout churn, not a HOLD) and **OS/model**406 (broad-across-models + OS-concentrated = platform/release; one rugged OEM on obfuscated frames = tamper/sideload).407- **Crashes — "new" is the #1 false alarm (`firstOccurrence` = rollout date):** App Center's408 per-version `firstOccurrence` is when that *version* shipped, NOT when the signature first appeared,409 so a years-old crash shows a first-seen inside your window and a per-1k rate that "rose" (young410 build = small device denominator). `diff status=new` only means "absent from the single baseline."411 To actually find new crashes use **`newcrashes --priors v1,v2,v3,v4`** (anti-join vs a *union* of412 priors; java-frame `genuinely-new` is the signal, native/hex `new-native?` is build-unique noise,413 triage on `newDevices`), and confirm any suspect with **`signature --match … --trend`** (cross-version414 presence). A signature still firing on prior versions is pre-existing/environmental, not this release.415 Verified: the okhttp `FileSystem$1.rename:87` IOException looked new+regressing on 6.2606 but exists416 on every version back to 6.2601. See `crash-sources.md` gotcha #3 and investigation-patterns P10.417- **Device-share masks per-span spikes:** the host-app movers table dedups a device across all spans,418 so a code can look flat/down there while its per-request rate climbs inside one span. Re-slice with419 `broker-errors-by-host-app-span.kql` (request-level rate by `span_name`) before declaring "no broker420 regression." Separate early-rollout decay from a steady gap with a daily trend.421- **Release PR correlation:** for eSTS-returned codes (`invalid_grant`, `interaction_required`) the422 trigger is in the bundled broker version range — read `git log v<PREV>..v<NEW>` in `broker/`+`common/`423 in full, then `find-suspect-prs.ps1 -Range`. Weight device-PoP/PRT/cache changes; exclude different-code424 fixes and SdkType-gated PRs (`MSAL_CPP` = OneAuth, not the Authenticator app).425- **Secrets:** the App Center token and anything under `~/.android-release-reports/` stay out of426 the repo and out of the report — never echo or paste them.427- **UTF-8 trap:** never write the HTML through a PowerShell `@'…'@` heredoc (it strips emoji/arrows).428 Use the `edit`/`create` tools or `[IO.File]::WriteAllText($p,$t,[System.Text.UTF8Encoding]::new($false))`.429- **Filename collision:** a populated report is never silently overwritten — bootstrap halts unless `-Force`.