perf(api): cut ambient-bootstrap trpc volume on api-primary (~10%) (#2464)
* perf(api): cut ambient-bootstrap trpc volume on api-primary
The app fires a cluster of per-bootstrap trpc calls that dominate
api-primary request volume. Two are pure overhead (data is SSR-available
or globally cached) yet hit the network on every page load:
- user.getSettings (~106/s): four ambient consumers (chat icon + three
migration/nav alerts) forced `staleTime: 0` on every mount, solely to
observe when the batched getFeatureFlags fetch landed (flash-avoidance).
Decouple that: FeatureFlagsProvider now exposes useFeatureFlagsReady()
(the per-user flag overlay already runs once per bootstrap), and the
consumers gate on it while reading dismissedAlerts from the SSR-seeded
cache — kept current by the dismiss mutation's existing optimistic
setData. Removes the per-mount getSettings round-trip (and a forced
getBuzzAccount refetch in YellowBuzzMigrationNotice).
- system.getBrowsingSettingAddons (~35/s): global, redis-cached, identical
for all users. SSR-inject it in _app getInitialProps and pass as
initialData to the outermost provider; the nested mount reads the primed
cache. The client query no longer fires.
Net: ~140/s of ~1,433/s api-primary trpc volume (~10%) removed, all
pure-overhead procedures — each cut also drops that procedure's
per-request middleware tax (auth, flag eval) off the single JS thread.
useFeatureFlagsReady is additive; existing useFeatureFlags() consumers are
unchanged. Trade-off: a cross-device alert dismissal won't reflect on an
already-open tab until reload (acceptable for temporary migration notices).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(alerts): reconcile getSettings cache on dismiss (invalidate after optimistic update)
The ambient migration/nav alerts read dismissedAlerts from the SSR-seeded
getSettings cache without a per-mount refetch (the volume cut in this PR). The
dismiss mutation's optimistic setData spreads `...old`, so if the cached base
was incomplete (e.g. a failed SSR /api/user/settings snapshot returning {}), it
could persist a truncated settings object and a stale dismissed state.
Add onSettled -> utils.user.getSettings.invalidate() to each dismiss mutation:
one refetch per dismiss (a rare user action, NOT per mount) restores the full
authoritative settings object and confirms the server-side dismissal. Does not
reintroduce the bootstrap-volume this PR removes.
Does not address cross-tab live propagation (separate per-tab caches) or the
failed-initial-SSR-snapshot first render — both inherent and low-impact for
one-time migration notices.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(preview): SSR-inject browsingSettingsAddons regression guard for #2464
Adds a preview-only Playwright spec (preview-bootstrap.spec.ts) following the
playwright.preview.config.ts (PREVIEW_URL), excluded from local `pnpm test` by
the default config's `**/preview-*.ts` testIgnore.
Asserts the half of #2464 that is deterministic and login-free:
- `browsingSettingsAddons` IS present in __NEXT_DATA__.props.pageProps (SSR
inject from _app.getInitialProps), AND
- NO `system.getBrowsingSettingAddons` tRPC request fires on bootstrap (the
non-batched httpLink puts the procedure name in the request path). Covered
anonymously (it fired for anon in the old code) and for a gate-passing mod.
The authed `user.getSettings` no-per-mount-refetch / chat-no-flash / alert
dismiss-persist / getFeatureFlags fail-open cases are timing/visual/state
dependent and flaky as live-preview assertions, so they're documented as a
manual checklist in the spec rather than written as theatre.
Wires the new spec into playwright.preview.config.ts (testMatch + preview-smoke
project glob).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(preview): fix bootstrap spec goto — domcontentloaded, not networkidle
`page.goto(path, { waitUntil: 'networkidle' })` never settled on this app
(continuous background requests — signals/polling/beacons), so both bootstrap
tests hung to the 45s navigation timeout and failed on the preview run (#2464
run pr-preview-2464-pjk4z). The request listener already captures every tRPC
call regardless of load state, so switch to domcontentloaded + a 2.5s settle
window — same captured request set, no doomed idle wait.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf+fix(api): parallelize SSR fetches; guard alerts against failed-settings snapshot
From audit #3:
- _app getInitialProps awaited getBrowsingSettingAddons() sequentially after
getFeatureFlagsAsync despite no dependency — on every full render's critical
path. Resolve both (and their dynamic imports) via Promise.all.
- The three migration/nav alerts gated visibility on useFeatureFlagsReady() and
read dismissedAlerts from the SSR-seeded getSettings cache. On the rare path
where the SSR /api/user/settings snapshot fails (endpoint returns {} so the
destructured settings is undefined, hence initialData undefined), the query
self-heals via a mount fetch, but until it lands dismissedAlerts is undefined
and a previously dismissed alert could briefly re-show. Add a "settings is
defined" guard so the alert waits for that fetch on the failed path only; on
the normal SSR-seeded path settings is defined immediately (no delay, no
per-mount refetch reintroduced).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:16:52 -05:00
|
|
|
import { expect, test } from '@playwright/test';
|
|
|
|
|
import { storageStatePath } from './preview-fixtures';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Regression guard for PR #2464 (perf/cut-ambient-bootstrap-trpc-volume): cutting
|
|
|
|
|
* per-bootstrap tRPC volume on api-primary. Runs only under
|
|
|
|
|
* playwright.preview.config.ts (needs PREVIEW_URL).
|
|
|
|
|
*
|
|
|
|
|
* What this file CAN assert deterministically (and does):
|
|
|
|
|
* 1. ANON SSR-inject of `browsingSettingsAddons`. _app.getInitialProps now
|
|
|
|
|
* fetches the redis-cached global addon list server-side and seeds it into
|
|
|
|
|
* `__NEXT_DATA__.props.pageProps.browsingSettingsAddons`, passing it to
|
|
|
|
|
* BrowsingSettingsAddonsProvider as `initialData`. So the client must NOT
|
|
|
|
|
* fire `system.getBrowsingSettingAddons` on bootstrap. This fired for anon
|
|
|
|
|
* in the old code, so it's verifiable WITHOUT login. The tRPC client is
|
|
|
|
|
* non-batched (src/utils/trpc.ts httpLink), so the procedure name is in the
|
|
|
|
|
* request path — a substring match on the URL is a reliable network probe.
|
|
|
|
|
* The anon home page 307s to /login, but `_app` getInitialProps runs for
|
|
|
|
|
* every page, so the SSR payload + provider mount happen on /login too.
|
|
|
|
|
* 2. The same SSR-inject for a logged-in (gate-passing) user.
|
|
|
|
|
*
|
|
|
|
|
* What is LEFT AS MANUAL (documented, not faked here — see checklist at bottom):
|
|
|
|
|
* - "user.getSettings is no longer force-refetched per mount" by the four
|
|
|
|
|
* ambient consumers (chat icon + 3 migration/nav alerts). getSettings fires
|
|
|
|
|
* for many unrelated reasons during a session, so a count-based "did it
|
|
|
|
|
* refetch?" assertion against a live preview is flaky and would be theatre.
|
|
|
|
|
* - Chat-icon no-flash, alert render/dismiss/persist, and the
|
|
|
|
|
* getFeatureFlags-error fail-open path. These are timing/visual and
|
|
|
|
|
* user-state dependent; assert by hand.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const ADDONS_PROCEDURE = 'system.getBrowsingSettingAddons';
|
|
|
|
|
|
fix(auth): preview-only legacy-session fallback so preview smoke can authenticate (#2786)
* fix(auth): preview-only legacy-session fallback so preview smoke can authenticate
The first-party OAuth cutover (#2712 + follow-ups) moved user resolution off the
app DB onto getSessionUserById, which reads the shared session cache then the
centralized hub (auth.civitai.com) — both backed by the PRODUCTION identity store.
PR previews run against the dev DB clone, where the ci-smoke-* smoke users are
seeded (datapacket-talos seed-smoke-test-users CronJob) but the hub has no row for
them. So getLegacySession decoded the minted legacy cookie fine, extracted the
userId, then getSessionUserById returned null (hub 404) → null session → the _app
route guard 307'd every authenticated request to /login → auth.civitai.com → back
→ ERR_TOO_MANY_REDIRECTS. Result: ALL ~53 authenticated preview smoke tests failed
(observed on pr-2781..2784 previews; #2773's smoke was green before the cutover).
Production is unaffected — real users resolve via the hub normally.
Fix: in getLegacySession, when getSessionUserById misses AND IS_PREVIEW, fall back
to the rich `user` embedded in the minted legacy cookie — exactly what the
pre-cutover gate did (read token.user straight from the cookie, no DB hit). Gated
on IS_PREVIEW so production NEVER trusts the embedded user (it must resolve via the
hub); zero production blast radius. Updated the preview-auth.setup.ts header to
document the new mechanism.
This restores pre-cutover preview behaviour as a stopgap; a longer-term option is
to teach the hub/session-client a preview identity source, but that's the migration
owner's call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(preview): repoint anonymous SSR-inject landing /login -> /preview-restricted
The anonymous SSR-injection smoke tests (preview-bootstrap, preview-ssr-inject)
used ANON_LANDING='/login' — pre-cutover the in-app /login PAGE rendered through
_app, so __NEXT_DATA__ carried the SSR-injected browsingSettingsAddons /
announcements / getLiveNow. The first-party-OAuth cutover (remove in-app login UI)
made /login a server-side redirect to the hub: it no longer renders, so the anon
landing seeded nothing → the tests failed (and, with AUTH_JWT_ISSUER unset on the
preview, looped).
/preview-restricted is the preview gate's OTHER allow-listed exception
(resolveAuthGuard: `path !== '/preview-restricted'`) and is a plain page (no custom
getServerSideProps) that renders via _app for anyone — anonymous included. Repoint
ANON_LANDING there so the same _app SSR-inject path is exercised, keeping the
coverage instead of dropping it.
(Companion to PR #2786's preview-only legacy-session fallback for the AUTHENTICATED
loop, and the datapacket-talos preview ConfigMap getting AUTH_JWT_ISSUER so /login
forwards to the hub instead of degrading to / on regular previews.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:36:57 -05:00
|
|
|
// A page that renders for anonymous traffic without clearing the preview gate, so
|
|
|
|
|
// _app.getInitialProps (and thus the SSR inject + provider mount) runs — exactly
|
|
|
|
|
// the bootstrap path we want to probe. Post first-party-OAuth cutover, /login is a
|
|
|
|
|
// server-side REDIRECT to the hub (no in-app render), so it can't be the landing
|
|
|
|
|
// anymore; /preview-restricted is the gate's other allow-listed exception
|
|
|
|
|
// (resolveAuthGuard: `path !== '/preview-restricted'`) and renders via _app for
|
|
|
|
|
// anyone, anonymous included.
|
|
|
|
|
const ANON_LANDING = '/preview-restricted';
|
perf(api): cut ambient-bootstrap trpc volume on api-primary (~10%) (#2464)
* perf(api): cut ambient-bootstrap trpc volume on api-primary
The app fires a cluster of per-bootstrap trpc calls that dominate
api-primary request volume. Two are pure overhead (data is SSR-available
or globally cached) yet hit the network on every page load:
- user.getSettings (~106/s): four ambient consumers (chat icon + three
migration/nav alerts) forced `staleTime: 0` on every mount, solely to
observe when the batched getFeatureFlags fetch landed (flash-avoidance).
Decouple that: FeatureFlagsProvider now exposes useFeatureFlagsReady()
(the per-user flag overlay already runs once per bootstrap), and the
consumers gate on it while reading dismissedAlerts from the SSR-seeded
cache — kept current by the dismiss mutation's existing optimistic
setData. Removes the per-mount getSettings round-trip (and a forced
getBuzzAccount refetch in YellowBuzzMigrationNotice).
- system.getBrowsingSettingAddons (~35/s): global, redis-cached, identical
for all users. SSR-inject it in _app getInitialProps and pass as
initialData to the outermost provider; the nested mount reads the primed
cache. The client query no longer fires.
Net: ~140/s of ~1,433/s api-primary trpc volume (~10%) removed, all
pure-overhead procedures — each cut also drops that procedure's
per-request middleware tax (auth, flag eval) off the single JS thread.
useFeatureFlagsReady is additive; existing useFeatureFlags() consumers are
unchanged. Trade-off: a cross-device alert dismissal won't reflect on an
already-open tab until reload (acceptable for temporary migration notices).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(alerts): reconcile getSettings cache on dismiss (invalidate after optimistic update)
The ambient migration/nav alerts read dismissedAlerts from the SSR-seeded
getSettings cache without a per-mount refetch (the volume cut in this PR). The
dismiss mutation's optimistic setData spreads `...old`, so if the cached base
was incomplete (e.g. a failed SSR /api/user/settings snapshot returning {}), it
could persist a truncated settings object and a stale dismissed state.
Add onSettled -> utils.user.getSettings.invalidate() to each dismiss mutation:
one refetch per dismiss (a rare user action, NOT per mount) restores the full
authoritative settings object and confirms the server-side dismissal. Does not
reintroduce the bootstrap-volume this PR removes.
Does not address cross-tab live propagation (separate per-tab caches) or the
failed-initial-SSR-snapshot first render — both inherent and low-impact for
one-time migration notices.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(preview): SSR-inject browsingSettingsAddons regression guard for #2464
Adds a preview-only Playwright spec (preview-bootstrap.spec.ts) following the
playwright.preview.config.ts (PREVIEW_URL), excluded from local `pnpm test` by
the default config's `**/preview-*.ts` testIgnore.
Asserts the half of #2464 that is deterministic and login-free:
- `browsingSettingsAddons` IS present in __NEXT_DATA__.props.pageProps (SSR
inject from _app.getInitialProps), AND
- NO `system.getBrowsingSettingAddons` tRPC request fires on bootstrap (the
non-batched httpLink puts the procedure name in the request path). Covered
anonymously (it fired for anon in the old code) and for a gate-passing mod.
The authed `user.getSettings` no-per-mount-refetch / chat-no-flash / alert
dismiss-persist / getFeatureFlags fail-open cases are timing/visual/state
dependent and flaky as live-preview assertions, so they're documented as a
manual checklist in the spec rather than written as theatre.
Wires the new spec into playwright.preview.config.ts (testMatch + preview-smoke
project glob).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(preview): fix bootstrap spec goto — domcontentloaded, not networkidle
`page.goto(path, { waitUntil: 'networkidle' })` never settled on this app
(continuous background requests — signals/polling/beacons), so both bootstrap
tests hung to the 45s navigation timeout and failed on the preview run (#2464
run pr-preview-2464-pjk4z). The request listener already captures every tRPC
call regardless of load state, so switch to domcontentloaded + a 2.5s settle
window — same captured request set, no doomed idle wait.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf+fix(api): parallelize SSR fetches; guard alerts against failed-settings snapshot
From audit #3:
- _app getInitialProps awaited getBrowsingSettingAddons() sequentially after
getFeatureFlagsAsync despite no dependency — on every full render's critical
path. Resolve both (and their dynamic imports) via Promise.all.
- The three migration/nav alerts gated visibility on useFeatureFlagsReady() and
read dismissedAlerts from the SSR-seeded getSettings cache. On the rare path
where the SSR /api/user/settings snapshot fails (endpoint returns {} so the
destructured settings is undefined, hence initialData undefined), the query
self-heals via a mount fetch, but until it lands dismissedAlerts is undefined
and a previously dismissed alert could briefly re-show. Add a "settings is
defined" guard so the alert waits for that fetch on the failed path only; on
the normal SSR-seeded path settings is defined immediately (no delay, no
per-mount refetch reintroduced).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:16:52 -05:00
|
|
|
|
|
|
|
|
// A core page a gate-passing user lands on directly (no /login bounce).
|
|
|
|
|
const AUTHED_LANDING = '/models';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Navigate to `path`, recording every tRPC request URL seen during the load and
|
|
|
|
|
* for a short settle window after, then return them. We watch `request` (fired
|
|
|
|
|
* the moment the client issues it) so we catch the call even if it errors.
|
|
|
|
|
*/
|
|
|
|
|
async function collectTrpcRequests(
|
|
|
|
|
page: import('@playwright/test').Page,
|
|
|
|
|
path: string
|
|
|
|
|
): Promise<string[]> {
|
|
|
|
|
const trpcUrls: string[] = [];
|
|
|
|
|
const onRequest = (req: import('@playwright/test').Request) => {
|
|
|
|
|
const url = req.url();
|
|
|
|
|
if (url.includes('/api/trpc/')) trpcUrls.push(url);
|
|
|
|
|
};
|
|
|
|
|
page.on('request', onRequest);
|
|
|
|
|
try {
|
|
|
|
|
// NOT 'networkidle': the app keeps background requests alive (signals /
|
|
|
|
|
// polling / beacons), so the network never goes idle and goto would hang to
|
|
|
|
|
// the navigation timeout. We don't need it — the `request` listener above
|
|
|
|
|
// captures every tRPC call regardless of load state; a fixed settle window
|
|
|
|
|
// after DOMContentLoaded covers the bootstrap burst + any deferred mount.
|
|
|
|
|
await page.goto(path, { waitUntil: 'domcontentloaded' });
|
|
|
|
|
await page.waitForTimeout(2500);
|
|
|
|
|
} finally {
|
|
|
|
|
page.off('request', onRequest);
|
|
|
|
|
}
|
|
|
|
|
return trpcUrls;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Read `__NEXT_DATA__.props.pageProps.browsingSettingsAddons` from the page. */
|
|
|
|
|
async function readBrowsingSettingsAddons(page: import('@playwright/test').Page) {
|
|
|
|
|
return page.evaluate(() => {
|
|
|
|
|
const el = document.getElementById('__NEXT_DATA__');
|
|
|
|
|
if (!el?.textContent) return { present: false, value: undefined as unknown };
|
|
|
|
|
const data = JSON.parse(el.textContent);
|
|
|
|
|
const value = data?.props?.pageProps?.browsingSettingsAddons;
|
|
|
|
|
return { present: typeof value !== 'undefined', value };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test.describe('SSR-injected browsingSettingsAddons (anonymous)', () => {
|
|
|
|
|
// Explicitly anonymous: no minted session cookie.
|
|
|
|
|
test.use({ storageState: { cookies: [], origins: [] } });
|
|
|
|
|
|
|
|
|
|
test('addons are seeded into __NEXT_DATA__ and not fetched on bootstrap', async ({ page }) => {
|
|
|
|
|
const trpcUrls = await collectTrpcRequests(page, ANON_LANDING);
|
|
|
|
|
|
|
|
|
|
// (a) SSR payload carries the addon list.
|
|
|
|
|
const addons = await readBrowsingSettingsAddons(page);
|
|
|
|
|
expect(addons.present, 'browsingSettingsAddons present in __NEXT_DATA__ pageProps').toBe(true);
|
|
|
|
|
expect(Array.isArray(addons.value), 'browsingSettingsAddons is a list').toBe(true);
|
|
|
|
|
|
|
|
|
|
// (b) The client never issues the now-SSR-injected procedure on bootstrap.
|
|
|
|
|
const addonRequests = trpcUrls.filter((u) => u.includes(ADDONS_PROCEDURE));
|
|
|
|
|
expect(
|
|
|
|
|
addonRequests,
|
|
|
|
|
`no ${ADDONS_PROCEDURE} tRPC request should fire on bootstrap (it is SSR-injected); saw:\n${addonRequests.join(
|
|
|
|
|
'\n'
|
|
|
|
|
)}`
|
|
|
|
|
).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe('SSR-injected browsingSettingsAddons (logged-in)', () => {
|
|
|
|
|
test.use({ storageState: storageStatePath('mod') });
|
|
|
|
|
|
|
|
|
|
test('addons are seeded and not fetched on bootstrap for a gate-passing user', async ({
|
|
|
|
|
page,
|
|
|
|
|
}) => {
|
|
|
|
|
const trpcUrls = await collectTrpcRequests(page, AUTHED_LANDING);
|
|
|
|
|
|
|
|
|
|
const addons = await readBrowsingSettingsAddons(page);
|
|
|
|
|
expect(addons.present, 'browsingSettingsAddons present in __NEXT_DATA__ pageProps').toBe(true);
|
|
|
|
|
expect(Array.isArray(addons.value), 'browsingSettingsAddons is a list').toBe(true);
|
|
|
|
|
|
|
|
|
|
const addonRequests = trpcUrls.filter((u) => u.includes(ADDONS_PROCEDURE));
|
|
|
|
|
expect(
|
|
|
|
|
addonRequests,
|
|
|
|
|
`no ${ADDONS_PROCEDURE} tRPC request should fire on bootstrap (it is SSR-injected); saw:\n${addonRequests.join(
|
|
|
|
|
'\n'
|
|
|
|
|
)}`
|
|
|
|
|
).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* MANUAL CHECKLIST — behaviours of #2464 that are not auto-asserted above.
|
|
|
|
|
* (Auth IS supported by this harness, but these are timing/visual/state cases
|
|
|
|
|
* that would be flaky or no-op as live-preview assertions; verify by hand.)
|
|
|
|
|
*
|
|
|
|
|
* [ ] Chat icon no-flash: logged-in user WITH chat disabled — the chat icon
|
|
|
|
|
* must NOT appear-then-disappear on load. It should gate on
|
|
|
|
|
* useFeatureFlagsReady() and only render once the user flag overlay settles.
|
2026-06-29 13:45:35 -06:00
|
|
|
* [ ] No per-mount getSettings refetch: with the ambient consumers mounted
|
|
|
|
|
* (chat icon + NavTidyNotice + YellowBuzzMigrationNotice), client-side nav
|
|
|
|
|
* between pages must NOT trigger
|
perf(api): cut ambient-bootstrap trpc volume on api-primary (~10%) (#2464)
* perf(api): cut ambient-bootstrap trpc volume on api-primary
The app fires a cluster of per-bootstrap trpc calls that dominate
api-primary request volume. Two are pure overhead (data is SSR-available
or globally cached) yet hit the network on every page load:
- user.getSettings (~106/s): four ambient consumers (chat icon + three
migration/nav alerts) forced `staleTime: 0` on every mount, solely to
observe when the batched getFeatureFlags fetch landed (flash-avoidance).
Decouple that: FeatureFlagsProvider now exposes useFeatureFlagsReady()
(the per-user flag overlay already runs once per bootstrap), and the
consumers gate on it while reading dismissedAlerts from the SSR-seeded
cache — kept current by the dismiss mutation's existing optimistic
setData. Removes the per-mount getSettings round-trip (and a forced
getBuzzAccount refetch in YellowBuzzMigrationNotice).
- system.getBrowsingSettingAddons (~35/s): global, redis-cached, identical
for all users. SSR-inject it in _app getInitialProps and pass as
initialData to the outermost provider; the nested mount reads the primed
cache. The client query no longer fires.
Net: ~140/s of ~1,433/s api-primary trpc volume (~10%) removed, all
pure-overhead procedures — each cut also drops that procedure's
per-request middleware tax (auth, flag eval) off the single JS thread.
useFeatureFlagsReady is additive; existing useFeatureFlags() consumers are
unchanged. Trade-off: a cross-device alert dismissal won't reflect on an
already-open tab until reload (acceptable for temporary migration notices).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(alerts): reconcile getSettings cache on dismiss (invalidate after optimistic update)
The ambient migration/nav alerts read dismissedAlerts from the SSR-seeded
getSettings cache without a per-mount refetch (the volume cut in this PR). The
dismiss mutation's optimistic setData spreads `...old`, so if the cached base
was incomplete (e.g. a failed SSR /api/user/settings snapshot returning {}), it
could persist a truncated settings object and a stale dismissed state.
Add onSettled -> utils.user.getSettings.invalidate() to each dismiss mutation:
one refetch per dismiss (a rare user action, NOT per mount) restores the full
authoritative settings object and confirms the server-side dismissal. Does not
reintroduce the bootstrap-volume this PR removes.
Does not address cross-tab live propagation (separate per-tab caches) or the
failed-initial-SSR-snapshot first render — both inherent and low-impact for
one-time migration notices.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(preview): SSR-inject browsingSettingsAddons regression guard for #2464
Adds a preview-only Playwright spec (preview-bootstrap.spec.ts) following the
playwright.preview.config.ts (PREVIEW_URL), excluded from local `pnpm test` by
the default config's `**/preview-*.ts` testIgnore.
Asserts the half of #2464 that is deterministic and login-free:
- `browsingSettingsAddons` IS present in __NEXT_DATA__.props.pageProps (SSR
inject from _app.getInitialProps), AND
- NO `system.getBrowsingSettingAddons` tRPC request fires on bootstrap (the
non-batched httpLink puts the procedure name in the request path). Covered
anonymously (it fired for anon in the old code) and for a gate-passing mod.
The authed `user.getSettings` no-per-mount-refetch / chat-no-flash / alert
dismiss-persist / getFeatureFlags fail-open cases are timing/visual/state
dependent and flaky as live-preview assertions, so they're documented as a
manual checklist in the spec rather than written as theatre.
Wires the new spec into playwright.preview.config.ts (testMatch + preview-smoke
project glob).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(preview): fix bootstrap spec goto — domcontentloaded, not networkidle
`page.goto(path, { waitUntil: 'networkidle' })` never settled on this app
(continuous background requests — signals/polling/beacons), so both bootstrap
tests hung to the 45s navigation timeout and failed on the preview run (#2464
run pr-preview-2464-pjk4z). The request listener already captures every tRPC
call regardless of load state, so switch to domcontentloaded + a 2.5s settle
window — same captured request set, no doomed idle wait.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf+fix(api): parallelize SSR fetches; guard alerts against failed-settings snapshot
From audit #3:
- _app getInitialProps awaited getBrowsingSettingAddons() sequentially after
getFeatureFlagsAsync despite no dependency — on every full render's critical
path. Resolve both (and their dynamic imports) via Promise.all.
- The three migration/nav alerts gated visibility on useFeatureFlagsReady() and
read dismissedAlerts from the SSR-seeded getSettings cache. On the rare path
where the SSR /api/user/settings snapshot fails (endpoint returns {} so the
destructured settings is undefined, hence initialData undefined), the query
self-heals via a mount fetch, but until it lands dismissedAlerts is undefined
and a previously dismissed alert could briefly re-show. Add a "settings is
defined" guard so the alert waits for that fetch on the failed path only; on
the normal SSR-seeded path settings is defined immediately (no delay, no
per-mount refetch reintroduced).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:16:52 -05:00
|
|
|
* a `user.getSettings` refetch per mount (they now read the SSR-seeded
|
|
|
|
|
* cache + gate on useFeatureFlagsReady, not staleTime:0 + isFetched).
|
|
|
|
|
* [ ] Alert render/dismiss/persist: a migration/nav alert renders from
|
|
|
|
|
* dismissedAlerts, dismissing it fires exactly one getSettings refetch via
|
|
|
|
|
* the new onSettled invalidate, and it stays dismissed across reloads.
|
|
|
|
|
* [ ] getFeatureFlags error fail-open: if user.getFeatureFlags errors,
|
|
|
|
|
* useFeatureFlagsReady() still flips true (gated on isFetched, which covers
|
|
|
|
|
* success OR error), so chat/alerts are not stuck hidden.
|
|
|
|
|
*/
|