Files
civitai__civitai/docs/auth/session-refresh-debug-instrumentation.md
Briant Diehl 6302cd59a7 docs(auth): consolidate under docs/auth/ + reconcile against shipped code
Move all 25 auth-migration docs (16 from docs/, 9 from docs/plans/) into a single
docs/auth/ front door and fix every cross-link for the new location (24 repo refs
../ -> ../../, 19 inter-doc sibling links, 3 to non-moved docs).

Reconcile doc content against the shipped code (3-audit sweep, then independently
re-verified):
- Cookie minting verified SOUND (names/prefixes/attributes/claim-shape all correct).
- auth-hub-main-app-changes.md / auth-verification-strategy.md: SUPERSEDED / CORRECTION
  banners — shipped model is thin ES256, not fat RS256; an RSA key throws at boot.
- main-app-auth-cutover.md: NextAuth strip marked done; createSessionClient({isRevoked}).
- session-refresh-debug-instrumentation.md: 3/7 revert targets moot (deleted files).
- auth-hub-cutover-review-2026-06-17.md: B2 freshness note corrected (false positive);
  B1/B3/B4/B5/M2 marked fixed.
- auth-prelaunch-action-checklist.md: blocker status updated (B4 = Option 2).
- Minor RS256 -> ES256 / SameSite=Lax / status-flip corrections across the set.

Known: 7 refs to the deleted next-auth-options.ts remain in the two superseded
decision docs (historical context, intentionally left).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:18:08 -06:00

9.7 KiB

Debug Instrumentation Cleanup

This doc tracks temporary diagnostic logs added during two related investigations so they can be cleanly reverted once no longer needed:

  1. Session-refresh diagnostics — identifying the source of unexpected POST /api/auth/session calls observed during a staged production deploy.
  2. Signal/metric render diagnostics — auditing how often signal-driven updates cause React re-renders.

It also lists the permanent fixes that came out of the same investigations, so they're not reverted by mistake.


Permanent fixes — DO NOT revert

These are real bug/perf fixes that should stay in the codebase. Listed here only so they aren't bundled into a "revert all the debug stuff" pass.

File What changed Why it stays
src/components/AppLayout/FeatureLayout.tsx Loader gate changed from status === 'loading' to status === 'loading' && !data Prevents the page from blanking during in-flight update() calls when a session is already loaded.
src/store/signal-topics.store.ts New Zustand store holding registered signal topics Moves topic-list state out of SignalsProvider so subscription changes don't fan out re-renders to every useSignalContext consumer.
src/components/Signals/SignalsProvider.tsx Removed registeredTopics useState + context exposure; register/release now call the store imperatively Same as above — eliminates the cascade where mounting one MetricsLive re-rendered every other one.
src/components/Auction/AuctionUtils.tsx, src/components/Auction/AuctionInfo.tsx, src/pages/testing/metrics-refcount.tsx, src/pages/testing/live-card-parity.tsx Read registeredTopics from useSignalTopicsStore instead of useSignalContext Required follow-through from the SignalsProvider refactor above.
src/components/Signals/MetricSignalsRegistrar.tsx Skips applyDelta when the normalized payload has no truthy values Server emits empty topic-only notifications; without this, every empty signal triggered selector runs across all MetricsLive consumers.
src/store/metric-signals.store.ts applyDelta returns the existing state object when no fields actually changed Defense-in-depth for the same issue — preserves reference identity so Zustand subscribers don't re-run their selectors.

Files to revert (diagnostics only)

1. src/server/auth/session-invalidation.ts

BEING RESOLVED NOW (2026-06-17): the debug block at session-invalidation.ts:50-71 is being REMOVED from the code in this same pass. Its stack.includes('next-auth-options') guard is now dead (that file is deleted), so the block always fired. After this removal, nothing remains here.

Inside refreshSession(), remove the stack capture + Redis write block.

Remove:

// Temporary: capture caller stack so we can surface it via response header.
// Skips the internal JWT 'update' callback path (next-auth-options.ts) which
// re-marks tokens by design and clears its own marker immediately after.
const stack = new Error().stack ?? '';
if (!stack.includes('next-auth-options')) {
  console.warn(`[refreshSession] userId=${userId} sendSignal=${sendSignal}\n${stack}`);
  try {
    // Compact the stack into a single-line, header-safe string and stash in
    // Redis. The session callback will read this and surface it to the
    // client via x-session-refresh-cause.
    const compact = stack
      .split('\n')
      .slice(1, 8)
      .map((s) => s.trim())
      .join(' | ')
      .replace(/[^\x20-\x7E]/g, ' ')
      .slice(0, 1500);
    await sysRedis.set(`${REDIS_SYS_KEYS.SESSION.REFRESH_CAUSE}:${userId}`, compact, {
      EX: 60 * 60,
    });
  } catch {}
}

The function should end with the original log(...) line and the closing brace.

2. src/server/auth/token-refresh.ts

✅ already removed in the NextAuth cutover — no action. token-refresh.ts is deleted, so these console.warn calls are gone with it.

Remove the three console.warn calls inside refreshToken():

  • After tokenState === 'invalid' branch (around line 67-69):

    console.warn(
      `[refreshToken] needsCookieRefresh=true reason=invalid userId=${user.id} tokenId=${tokenId}`
    );
    
  • Inside if (tokenState === 'refresh') block (around line 88-90):

    console.warn(
      `[refreshToken] needsCookieRefresh=true reason=token-state-refresh userId=${user.id} tokenId=${tokenId}`
    );
    
  • Inside the SESSION:ALL global-invalidation branch (around line 106-108):

    console.warn(
      `[refreshToken] needsCookieRefresh=true reason=session-all userId=${user.id} tokenId=${tokenId} asOf=${allInvalidationDateStr} signedAt=${new Date(token.signedAt as number).toISOString()}`
    );
    

3. src/server/auth/get-server-auth-session.ts

✅ already removed in the NextAuth cutover — no action. checkAndSetSessionHeaders (and its needsCookieRefresh/header-setting machinery) is already gone from get-server-auth-session.ts.

Three changes to undo:

  • Drop REDIS_SYS_KEYS from the import (keep sysRedis — but if no other references remain after revert, remove the whole import line):

    import { REDIS_SYS_KEYS, sysRedis } from '~/server/redis/client';
    
  • Revert checkAndSetSessionHeaders to its synchronous form (drop the async, return type back to Session | null, and remove the Redis-read block):

    function checkAndSetSessionHeaders(session: Session | null, res: AuthResponse): Session | null {
      if (session?.needsCookieRefresh) {
        res.setHeader(SESSION_REFRESH_HEADER, 'true');
        res.setHeader(
          'Set-Cookie',
          `${SESSION_REFRESH_COOKIE}=true; Path=/; Max-Age=300; SameSite=Lax`
        );
        delete session.needsCookieRefresh;
      }
      return session;
    }
    
  • Drop the await on the call site:

    req.context.session = checkAndSetSessionHeaders(session, res);
    

4. src/server/redis/client.ts

Goes with §1. Once §1's block is removed (this pass), REFRESH_CAUSE has no writer — drop the key entry too. After §1 + §4, none of this session-refresh instrumentation remains (§2/§3/§5 were already removed in the NextAuth cutover).

Remove the REFRESH_CAUSE entry added to REDIS_SYS_KEYS.SESSION:

SESSION: {
  ALL: 'session:all',
  TOKEN_STATE: 'session:token-state',
  REFRESH_CAUSE: 'session:refresh-cause', // ← remove this line
},

5. src/components/UpdateRequiredWatcher/UpdateRequiredWatcher.tsx

✅ already removed in the NextAuth cutover — no action. The session-refresh header handler these logs lived in is gone (invalidation now propagates via signals), so there is nothing to revert here.

Remove the three console.warn lines inside the fetch interceptor's session-refresh header handler:

// eslint-disable-next-line no-console
console.warn('[session-refresh] triggered by response from', response.url);
const cause = response.headers.get('x-session-refresh-cause');
// eslint-disable-next-line no-console
if (cause) console.warn('[session-refresh] cause:', cause);

The handler should return to its original form starting with sessionRefreshPending = true;.

6. src/components/Signals/MetricSignalsRegistrar.tsx

Two diagnostic-only changes (the empty-payload short-circuit itself stays — it's a permanent fix listed above):

  • Drop the signalDebug import added for diagnostics:

    import { signalDebug } from '~/components/Signals/signalDebug';
    
  • Inside handleMetricUpdate, drop the diagnostic line and the hasChange flag passed to it. Simplify back to:

    const hasChange = Object.values(updates).some((v) => !!v);
    if (!hasChange) return;
    applyDelta(entityType, entityId, updates);
    

    …removing only the signalDebug('metric:update received', { ... }); line between hasChange and the early return. Keep the short-circuit.

These render-time debug logs predate this investigation but were exercised heavily during it. They're noisy in steady state and good candidates to remove once you're done verifying.

  • Remove signalDebug('Metrics render', ...) and the renders ref it depends on at the top of the Metrics component.
  • Remove the signalDebug('MetricsLive mount/unmount', ...) useEffect and the signalDebug('MetricsLive render', ...) call in the MetricsLive component, plus its renders ref.
  • If nothing else in this file calls signalDebug, drop the import.

The recommended keep-set for ongoing diagnostics — modest volume, useful signal — is: metric:update received (after the diagnostic line is removed, this one specifically is gone too — remove only if you also want to keep the short-circuit's silent behavior), registerTopic / releaseTopic (low-volume subscription transitions in SignalsProvider.tsx), and the useMetricSubscription effect: subscribe/unsubscribe lines.

Redis cleanup (optional)

The session-refresh instrumentation writes per-user keys at session:refresh-cause:{userId} with a 1-hour TTL. They expire on their own; no cleanup is required. If you want to flush them immediately after revert:

redis-cli --scan --pattern 'session:refresh-cause:*' | xargs redis-cli del