mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-09-16 18:42:03 +00:00
feat(pokedex): add opt-in Server-Timing for the pokédex load
POKEDEX_PERFORMANCE=true reports auth, ownership, scopes, entries and catches with fixed labels only: no IDs, query strings, cookies or entry content ever reach the header. Auth is measured in the hook, where the session is actually resolved, and handed to the page load through locals.
This commit is contained in:
Vendored
+1
@@ -13,6 +13,7 @@ declare global {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
supabase: SupabaseClient;
|
||||
pokedexAuthMs?: number;
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>;
|
||||
userid: string;
|
||||
buildDate: string;
|
||||
|
||||
@@ -30,17 +30,20 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
let sessionPromise: ReturnType<App.Locals['safeGetSession']> | null = null;
|
||||
event.locals.safeGetSession = () => {
|
||||
sessionPromise ??= (async () => {
|
||||
const authStarted = performance.now();
|
||||
const {
|
||||
data: { user },
|
||||
error
|
||||
} = await event.locals.supabase.auth.getUser();
|
||||
if (error) {
|
||||
event.locals.pokedexAuthMs = performance.now() - authStarted;
|
||||
return { session: null, user: null };
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session }
|
||||
} = await event.locals.supabase.auth.getSession();
|
||||
event.locals.pokedexAuthMs = performance.now() - authStarted;
|
||||
return { session, user };
|
||||
})();
|
||||
return sessionPromise;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
/** Opt-in, fixed labels only: never log IDs, query strings, cookies or entry content. */
|
||||
export class PokedexPerformance {
|
||||
private started = performance.now();
|
||||
private durations: Record<string, number> = {};
|
||||
readonly enabled = env.POKEDEX_PERFORMANCE === 'true';
|
||||
async measure<T>(
|
||||
stage: 'auth' | 'ownership' | 'scopes' | 'entries' | 'catches',
|
||||
run: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const start = performance.now();
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
if (this.enabled) this.durations[stage] = performance.now() - start;
|
||||
}
|
||||
}
|
||||
recordAuth(duration: number | undefined) {
|
||||
if (this.enabled && duration !== undefined) this.durations.auth = duration;
|
||||
}
|
||||
prepare<T>(run: () => T): T {
|
||||
const start = performance.now();
|
||||
try {
|
||||
return run();
|
||||
} finally {
|
||||
if (this.enabled) this.durations.prepare = performance.now() - start;
|
||||
}
|
||||
}
|
||||
finish(): string | undefined {
|
||||
if (!this.enabled) return undefined;
|
||||
this.durations.total = performance.now() - this.started;
|
||||
return Object.entries(this.durations)
|
||||
.map(([name, duration]) => `${name};dur=${duration.toFixed(1)}`)
|
||||
.join(', ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { PokedexPerformance } from '$lib/server/pokedexPerformance';
|
||||
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
describe('opt-in stage timing', () => {
|
||||
it('emits no timing header without explicit opt-in', async () => {
|
||||
vi.stubEnv('POKEDEX_PERFORMANCE', 'false');
|
||||
const timing = new PokedexPerformance();
|
||||
expect(await timing.measure('entries', async () => 'result')).toBe('result');
|
||||
expect(timing.prepare(() => 3)).toBe(3);
|
||||
timing.recordAuth(10);
|
||||
expect(timing.finish()).toBeUndefined();
|
||||
});
|
||||
it('records failed stages without exposing exception content', async () => {
|
||||
vi.stubEnv('POKEDEX_PERFORMANCE', 'true');
|
||||
const timing = new PokedexPerformance();
|
||||
await expect(
|
||||
timing.measure('catches', async () => {
|
||||
throw new Error('private note');
|
||||
})
|
||||
).rejects.toThrow('private note');
|
||||
expect(() =>
|
||||
timing.prepare(() => {
|
||||
throw new Error('private ID');
|
||||
})
|
||||
).toThrow('private ID');
|
||||
timing.recordAuth(undefined);
|
||||
timing.recordAuth(12.34);
|
||||
const header = timing.finish()!;
|
||||
expect(header).toContain('auth;dur=12.3');
|
||||
expect(header).toMatch(/catches;dur=\d+\.\d/);
|
||||
expect(header).toMatch(/prepare;dur=\d+\.\d/);
|
||||
expect(header).toMatch(/total;dur=\d+\.\d/);
|
||||
expect(header).not.toContain('private');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user