Skip to content

fix(auth): send the sign-out logout to the instance, and stop its failure reaching Error Tracking - #1680

Open
dawsontoth wants to merge 1 commit into
stagefrom
fix/instance-logout-console-debug
Open

fix(auth): send the sign-out logout to the instance, and stop its failure reaching Error Tracking#1680
dawsontoth wants to merge 1 commit into
stagefrom
fix/instance-logout-console-debug

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Signing out of Studio posts a best-effort logout to every instance and cluster the tab was signed into, and swallows a failure there because the local credentials are already cleared and the sign-out carries on. Two things were wrong with that loop. First, it cleared the entity's potentiallyAuthenticated entry — the only source of its operations URL — and its in-memory Fabric token before building the logout client, so any entity without a Fabric Connect flag (cookie-session or basic-auth sign-in) got a client with no baseURL, and the POST went to Studio's own origin instead of the instance: RUM shows 41 such POSTs to https://fabric.harper.fast/ answered 404 across 17 sessions in the last 30 days, which is the 404 half of #1672. The client is now built before the clears, the same way the manual disconnect in ClusterHome builds it while still signed in; a client that cannot be built is skipped rather than aborting the sweep. With the client captured first, the sweep can clear everything local through signOutLocally — including the stored basic-auth entry and Fabric Connect flag, which the old loop left for the POST's success path, so a failed logout used to keep the instance password in localStorage. Because those POSTs now leave for real hosts, the sweep also stops awaiting them one at a time: every entity is cleared locally up front, the logouts run in parallel with a 10s timeout and without the gateway-retry or token-recovery interceptors, and only then does the caller's central-manager logout follow. Second, the swallowed failure was reported with console.error, which the RUM SDK collects as an error (source: "console"), so the one line whose job was to keep the failure out of the way is what put Failed to log out from <id>, carrying on into Error Tracking (34 events, 13 sessions, 30 days). It now logs at console.debug, the channel the Reo loader already uses for exactly this reason (#1658), and logs only the message, since the full Axios error carries the request's credentials. Closes #1672.

For the human reviewer

  1. The wrong-target fix is in this PR rather than a separate issue. It is pre-existing and not what [RUM] The instance-logout catch that exists to swallow a failure console.errors it straight into Error Tracking (34 events, 13 sessions, 30d) #1672 asked for, but the round-1 cross-model review made the point that demoting the log alone would have silenced the alarm on its own root cause — those 404s were the only trace of the logout never reaching the instance. The change is confined to the one method. If you would rather keep this PR to the log level, say so and I will split it and move the ordering bug to Sign-out sweep: serial retry-armed instance logouts stall the CM logout, a failed logout leaves basic-auth on disk, and a re-added entity is skipped #1679.
  2. The sweep clears stored credentials itself, before the POST confirms. The old loop left Studio:BasicAuth:<id> and Studio:FabricConnect:<id> to onInstanceLogoutSubmit's success path, so a failed logout kept the instance password on disk until logoutOnSuccess wiped localStorage — which never runs if the central-manager logout also fails. Chosen: call signOutLocally per entity, the primitive the rest of the store already uses, now that the client is captured beforehand with its basic auth, Bearer, or proxy URL baked in. The trade is that local credentials are gone even when the remote session survives an unreachable instance; that was already true for the connection state, and the alternative leaves user A's password for user B on a shared browser.
  3. Parallel, bounded logouts instead of serial with the client's defaults. Pre-fix the non-Fabric POSTs 404'd same-origin in milliseconds; pointing them at real hosts meant one stopped instance could hold Sign out — and the central-manager logout behind it — for the client's 60s timeout plus 35s of gateway-retry backoff, per entity. Chosen: run every local clear synchronously first, then the best-effort logouts in parallel, each with a 10s timeout and with the response interceptors removed (no 502/503/504 retries, no replication-failure translation, no expired-token replay — none of which a logout benefits from, and the token path is already a no-op once the in-memory token is cleared). The 10s cap is a policy number; both it and the interceptor removal are applied by mutating the fresh client, which is safe only because getInstanceClient creates a new instance per call — a getInstanceClient({ bestEffort: true }) option that never installs them would make that structural, at the cost of widening a shared helper's signature for one caller. The central-manager logout still waits for the sweep, because proxy-routed Fabric Connect logouts authenticate with the CM session cookie. Residual the review raised: N parallel 10s clocks against one CM origin can starve the last requests under a browser's per-host connection cap on HTTP/1.1; not addressed, since it needs many simultaneously signed-in proxied instances and a slow proxy at once.
  4. Debug for every status, not only 404. [RUM] The instance-logout catch that exists to swallow a failure console.errors it straight into Error Tracking (34 events, 13 sessions, 30d) #1672 left open whether to keep the 500 case visible in Error Tracking, since an instance that answered and failed may still hold a live server-side session. Chosen: debug for both. Error Tracking visibility was never a mitigation for that — Studio cannot act on it — and with the client now actually pointed at the instance, what remains is genuinely unreachable or erroring instances. The alternative is a one-line status check that keeps console.error for non-404s, at the cost of ~18 events/30d staying in Error Tracking.
  5. No replacement RUM signal, and the resource-error path is unchanged. The adjudicator suggested a datadogRum.addAction('instance_logout_failed') beside the console.debug so the fleet-wide rate stays measurable; not done, since it would make the auth store import the Datadog integration and the daily RUM review already watches the @type:resource side. Relatedly, a logout POST that fails against a direct instance URL is also a RUM resource error, and shouldKeepEvent only drops network failures for the proxy URL shape — that is today's beforeSend policy for every direct-instance request, and this PR neither widens nor narrows it. Two related choices the review left to you: console.debug is hidden unless a console shows Verbose, so console.warn (also not collected by RUM as an error) would make a silently failing logout visible to a support engineer reading a customer's console; and no test drives the real useLogout to prove the sweep is awaited before the central-manager /Logout/ — that ordering is load-bearing for proxy-mode entities, whose logout rides the CM session cookie, and a future fire-and-forget refactor would pass this suite.

Two further findings from the review are pre-existing and outside this method's change, so they are tracked in #1679 instead of fixed here: an in-flight establishFabricConnectAuth re-writing auth state after the sweep has run, and an entity re-added mid-sweep being skipped. (#1679's items 1 and 2 — the serial shape and the credential residue — are addressed here.)

Verification

  • Unit, fails-on-base. New signOutFromPotentiallyAuthenticatedInstances.test.ts, eight tests, using the real getInstanceClient with only the network calls mocked. (1) Two signed-in instances: each onInstanceLogoutSubmit call receives a client whose defaults.baseURL is that entity's operations URL and whose timeout is 10s. (2) The captured client, given an adapter that rejects with a 503 the way axios's own adapters do, rejects within a 2s test budget — with the gateway-retry interceptor still installed it sleeps 5s first and the test times out. (3) A Fabric Connect direct-mode entity, established through establishFabricConnectAuth: the client keeps the direct operations URL and its Authorization: Bearer header, and the token is gone afterwards. (4) A Fabric Connect proxy-mode entity routes to the central-manager /HDBInstance/<id>/operation URL, and its flag is cleared afterwards. (5) With two logouts pending, every entity is already signed out locally and both logouts have been issued; the sweep stays unsettled after the first resolves and settles only after the second — dropping await Promise.all, or awaiting only the first, fails it. (6) The first logout rejects with a 500-shaped error: the sweep still reaches the second entity, both end up signed out locally, the captured client carried the entity's basic auth and that stored entry is gone afterwards, console.error is never called, and the message reaches console.debug. (7) A corrupt Studio:BasicAuth:<id> entry makes the client build throw synchronously: that entity is skipped and reported at debug, the next entity still gets its logout. (8) The cloud slot gets no network logout. With authStore.ts reverted to origin/stage, tests 1–3 and 5–7 fail ([ 'ins-a', undefined ] for the target; expected "wrapper" to not be called at all, but actually been called 1 times for console.error); tests 4 and 6 also fail against the intermediate that kept the hand-rolled clear list. All eight pass on the branch.
  • Full gate (Node 24.19.0): npx vitest run — 337 files, 2879 passed, 11 skipped; npx tsc -b exit 0; oxlint and dprint check clean on the changed files.
  • End-to-end: not verified live. The observable outcomes are (a) POSTs to https://fabric.harper.fast/ dropping to zero in RUM, and (b) the Failed to log out from… family reading zero under @error.source:console, both across several daily windows after the next prod deploy carries this and with sign-outs having happened in the window. That is the daily RUM review's check, not something a local run can show.

Complexity: medium

Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer; rounds=8 @ 67bb3f3

Human-Review-Need: 4 (decisions: clearing-local-state-before-the-post-lands, per-request-timeout-not-a-sweep-budget, post-hoc-interceptor-clearing-vs-client-option, console-debug-vs-console-warn, sweep-awaited-before-cm-logout) @ 67bb3f3

…lure reaching Error Tracking

`signOutFromPotentiallyAuthenticatedInstances` posts a best-effort `logout` to
every entity the tab was signed into, but it cleared `potentiallyAuthenticated`
(the only source of `getOperationsUrl`) and the in-memory Fabric token *before*
building the client, so any entity without a Fabric Connect flag got a client
with no `baseURL` and the POST went to Studio's own origin — RUM shows 41 such
POSTs answered 404 across 17 sessions in 30 days. Build the client before the
local clears so the logout reaches the instance, and keep a client that cannot
be built from aborting the sweep: the local clears run regardless.

With the client captured first, the sweep can clear everything local through
`signOutLocally` — including the stored basic-auth entry and Fabric Connect
flag, which the old loop left for the POST's success path, so a failed logout
kept the instance password in localStorage.

Because those POSTs now leave for real hosts, the sweep no longer awaits them
one at a time: every entity is cleared locally up front, the logouts run in
parallel with a 10s timeout and without the gateway-retry or token-recovery
interceptors, and only then does the caller's central-manager logout follow —
one unreachable instance can no longer hold Sign out for a 60s timeout plus
35s of retry backoff per entity.

The failure was then reported with `console.error`, which the RUM SDK collects
as an error (`source: "console"`), putting a failure the catch exists to swallow
into Error Tracking — 34 events across 13 sessions in 30 days. Log it at debug,
as the Reo loader does (#1658), and log only the message: the full Axios error
carries the request credentials.

Closes #1672

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the sign-out process in authStore.ts to execute instance logouts concurrently and clear local state up front, while applying a 10-second timeout and disabling retries to ensure a fast, best-effort logout. It also introduces a new test suite to thoroughly verify these behaviors. The review feedback recommends converting non-Error objects to strings using String(err) in reportLogoutFailure to prevent potential credential leakage from raw objects like Axios configurations.

Comment thread src/features/auth/store/authStore.ts
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 60.01% 8168 / 13609
🔵 Statements 60.47% 8755 / 14477
🔵 Functions 52.74% 2057 / 3900
🔵 Branches 53.93% 5872 / 10888
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/auth/store/authStore.ts 74.16% 59.85% 79.31% 74.53% 101, 187-208, 224-233, 243, 264, 288-299, 306-315, 370-373, 593-599, 633, 637-644, 656, 669-697
Generated in workflow #1852 for commit 67bb3f3 by the Vitest Coverage Report Action

@dawsontoth
dawsontoth marked this pull request as ready for review September 4, 2026 17:10
@dawsontoth
dawsontoth requested a review from a team as a code owner September 4, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RUM] The instance-logout catch that exists to swallow a failure console.errors it straight into Error Tracking (34 events, 13 sessions, 30d)

1 participant