Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

/
Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800
Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800

/_astro/*
Cache-Control: public, max-age=31536000, immutable

/assets/*
Cache-Control: public, max-age=31536000, immutable
1 change: 1 addition & 0 deletions src/lib/worker-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type WorkerEnv = {
AUTH0_MCP_AUDIENCE?: string;
NODE_ENV?: string;
APP_HEALTH_INGEST_KEY?: string;
APP_HEALTH_STAGE_SAMPLE_RATE?: string;
APP_HEALTH_ENVIRONMENT?: string;
ASSETS: Fetcher;
};
2 changes: 2 additions & 0 deletions src/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ vi.mock('./lib/auth', () => ({ createAuth: () => ({ handler: mocks.auth }) }));
vi.mock('./lib/auth-api', () => ({ getAuthenticatedUserId: mocks.user }));
vi.mock('./worker/bind-env', () => ({ bindWorkerEnv: vi.fn() }));
vi.mock('./worker/app-health', () => ({
trackAppHealthRequest: vi.fn(),
appHealthStageTimingMiddleware: async (_context: unknown, next: () => Promise<void>) => next(),
appHealthMiddleware: async (_context: unknown, next: () => Promise<void>) => {
mocks.appHealth();
await next();
Expand Down
11 changes: 10 additions & 1 deletion src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import articlesRoutes from './worker/routes/articles';
import keysRoutes from './worker/routes/keys';
import mcpRoutes, { protectedResourceMetadata } from './worker/routes/mcp';
import miscRoutes from './worker/routes/misc';
import { appHealthMiddleware } from './worker/app-health';
import {
appHealthMiddleware,
appHealthStageTimingMiddleware,
trackAppHealthRequest,
} from './worker/app-health';

const SECURITY_HEADERS: Record<string, string> = {
'X-Content-Type-Options': 'nosniff',
Expand All @@ -35,6 +39,10 @@ api.use('*', async (c, next) => {
});

api.use('*', appHealthMiddleware);
// Hono's /prefix/* also matches /prefix, so register links only once.
for (const path of ['/api/links/*', '/api/articles/*', '/api/mcp/*', '/api/keys/*']) {
api.use(path, appHealthStageTimingMiddleware);
}

api.on(['GET', 'HEAD'], '*', async (c, next) => {
const path = c.req.path.replace(/\/{2,}/g, '/').replace(/\/+$/, '');
Expand Down Expand Up @@ -115,6 +123,7 @@ function withSecurityHeaders(response: Response, pathname: string): Response {

export default {
async fetch(request: Request, env: WorkerEnv, ctx: ExecutionContext): Promise<Response> {
trackAppHealthRequest(request);
const url = new URL(request.url);

// Discovery outside /api/* remains separate from product API routes.
Expand Down
128 changes: 125 additions & 3 deletions src/worker/app-health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Hono } from 'hono';
import { describe, expect, it, vi } from 'vitest';

import type { WorkerEnv } from '../lib/worker-env';
import { createReaderAppHealthMiddleware } from './app-health';
import { createReaderAppHealthMiddlewares } from './app-health';

describe('Reader App Health endpoint middleware', () => {
it('is disabled without an ingest key', async () => {
const createClient = vi.fn();
const app = new Hono<{ Bindings: WorkerEnv }>();
app.use('*', createReaderAppHealthMiddleware(createClient));
app.use('*', createReaderAppHealthMiddlewares(createClient).appHealthMiddleware);
app.get('/api/articles/:articleId', (c) => c.json({ ok: true }));

const response = await app.request('/api/articles/private-id?search=secret', {}, {});
Expand All @@ -23,7 +23,7 @@ describe('Reader App Health endpoint middleware', () => {
const waitUntil = vi.fn();
const createClient = vi.fn(() => ({ record, flush }) as never);
const app = new Hono<{ Bindings: WorkerEnv }>();
app.use('*', createReaderAppHealthMiddleware(createClient));
app.use('*', createReaderAppHealthMiddlewares(createClient).appHealthMiddleware);
app.on(['GET', 'POST'], '/api/articles/:articleId', (c) =>
c.json({ content: 'private article body' })
);
Expand Down Expand Up @@ -80,3 +80,125 @@ describe('Reader App Health endpoint middleware', () => {
expect(JSON.stringify(record.mock.calls[1][0])).not.toContain('another-private-query');
});
});

describe('Reader App Health stage timing', () => {
function setup() {
const log = vi.fn();
const record = vi.fn();
const flush = vi.fn().mockResolvedValue(undefined);
const createClient = vi.fn(() => ({ log, record, flush }) as never);
const middleware = createReaderAppHealthMiddlewares(createClient);
const app = new Hono<{ Bindings: WorkerEnv }>();
app.use('*', async (c, next) => {
middleware.trackRequest(c.req.raw);
await next();
});
app.use('*', middleware.appHealthMiddleware);
for (const path of ['/api/links/*', '/api/articles/*', '/api/mcp/*', '/api/keys/*']) {
app.use(path, middleware.stageTimingMiddleware);
}
app.get('/api/links', (c) => c.json({ ok: true }));
app.get('/api/articles/:articleId', (c) => c.json({ ok: true }));
app.get('/landing', (c) => c.text('landing'));
const env = {
APP_HEALTH_INGEST_KEY: 'synthetic-test-key',
APP_HEALTH_STAGE_SAMPLE_RATE: '1',
} as WorkerEnv;
return { app, log, flush, createClient, env };
}

it('logs one bounded template record and shares the cached client', async () => {
const { app, log, flush, createClient, env } = setup();
const waitUntil = vi.fn();
const response = await app.request('/api/articles/123?secret=query', {}, env, {
waitUntil,
} as unknown as ExecutionContext);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ ok: true });
expect(log).toHaveBeenCalledOnce();
expect(log).toHaveBeenCalledWith('api.stage_timing', {
level: 'debug',
props: {
route: '/api/articles/:articleId',
status: 200,
total_ms: expect.any(Number),
edge_cache: 'NONE',
inner_cache: 'NONE',
colo: 'unknown',
cold: 1,
},
});
const props = log.mock.calls[0][1].props;
expect(Number.isInteger(props.total_ms)).toBe(true);
expect(props.total_ms).toBeGreaterThanOrEqual(0);
expect(props.total_ms).toBeLessThanOrEqual(600000);
expect(createClient).toHaveBeenCalledOnce();
expect(flush).toHaveBeenCalledTimes(2);
expect(waitUntil).toHaveBeenCalledTimes(2);
await app.request('/api/links', {}, env);
expect(log).toHaveBeenCalledTimes(2);
expect(log.mock.calls[1][1].props).toMatchObject({ route: '/api/links', cold: 0 });
expect(createClient).toHaveBeenCalledOnce();
});

it.each(['0', undefined])('does not log at rate zero or without a key (%s)', async (rate) => {
const { app, log, env } = setup();
if (rate === undefined) delete env.APP_HEALTH_INGEST_KEY;
else env.APP_HEALTH_STAGE_SAMPLE_RATE = rate;
expect((await app.request('/api/links', {}, env)).status).toBe(200);
expect(log).not.toHaveBeenCalled();
});

it.each(['invalid', '-1', '2', '', undefined])(
'defaults invalid/missing rate %s to 0.1',
async (rate) => {
const { app, log, env } = setup();
env.APP_HEALTH_STAGE_SAMPLE_RATE = rate;
const random = vi.spyOn(Math, 'random').mockReturnValue(0.1);
try {
await app.request('/api/links', {}, env);
expect(log).not.toHaveBeenCalled();
random.mockReturnValue(0.09);
await app.request('/api/links', {}, env);
expect(log).toHaveBeenCalledOnce();
} finally {
random.mockRestore();
}
}
);

it.each([
'/api/links/123',
'/api/links/0123456789abcdef',
'/api/links/12345678-abcd-1234-abcd-123456789012',
`/api/links/${'x'.repeat(121)}`,
])('rejects unsafe route %s', async (route) => {
const { app, log, env } = setup();
app.get(route, (c) => c.text('ok'));
expect((await app.request(route, {}, env)).status).toBe(200);
expect(log).not.toHaveBeenCalled();
});

it('counts an earlier uninstrumented request as warm', async () => {
const { app, log, env } = setup();
await app.request('/landing', {}, env);
expect(log).not.toHaveBeenCalled();
await app.request('/api/links', {}, env);
expect(log.mock.calls[0][1].props.cold).toBe(0);
});

it.each(['create', 'log', 'flush', 'reject'])(
'client %s errors preserve the response',
async (failure) => {
const { app, log, flush, createClient, env } = setup();
const fail = () => {
throw new Error('telemetry unavailable');
};
if (failure === 'create') createClient.mockImplementation(fail);
if (failure === 'log') log.mockImplementation(fail);
if (failure === 'flush') flush.mockImplementation(fail);
if (failure === 'reject') flush.mockRejectedValue(new Error('telemetry unavailable'));
expect((await app.request('/api/links', {}, env)).status).toBe(200);
}
);
});
113 changes: 91 additions & 22 deletions src/worker/app-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,111 @@ import {
type AppHealthClientOptions,
} from '@saas-maker/app-health';
import { honoMiddleware } from '@saas-maker/app-health/hono';
import type { Context, Env } from 'hono';
import type { Context, Env, MiddlewareHandler } from 'hono';
import { routePath } from 'hono/route';

import type { WorkerEnv } from '../lib/worker-env';

type ReaderEnv = Env & { Bindings: WorkerEnv };
type ClientFactory = (options: AppHealthClientOptions) => AppHealthClient;

export function createReaderAppHealthMiddleware(
function stageSampleRate(raw: string | undefined): number {
const parsed = raw?.trim() ? Number(raw.trim()) : Number.NaN;
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : 0.1;
}

function isSafeStageRoute(route: string): boolean {
return (
route.startsWith('/') &&
route.length <= 120 &&
!route
.split('/')
.some((segment) =>
/^(?:\d+|[a-f0-9]{16,}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/i.test(
segment
)
)
);
}

export function createReaderAppHealthMiddlewares(
createClient: ClientFactory = createAppHealthClient
) {
let cachedKey: string | null = null;
let cachedEnvironment = '';
let cachedClient: AppHealthClient | null = null;

return honoMiddleware<ReaderEnv>({
client: (context: Context<ReaderEnv>) => {
const key = context.env.APP_HEALTH_INGEST_KEY?.trim();
if (!key) return null;
const resolveClient = (context: Context<ReaderEnv>) => {
const key = context.env.APP_HEALTH_INGEST_KEY?.trim();
if (!key) return null;

const environment = context.env.APP_HEALTH_ENVIRONMENT?.trim() || 'production';
if (cachedClient && cachedKey === key && cachedEnvironment === environment) {
return cachedClient;
}
const environment = context.env.APP_HEALTH_ENVIRONMENT?.trim() || 'production';
if (cachedClient && cachedKey === key && cachedEnvironment === environment) {
return cachedClient;
}

cachedKey = key;
cachedEnvironment = environment;
cachedClient = createClient({
key,
environment,
endpoint: 'https://ingest.sassmaker.com/v1/ingest',
runtime: 'worker',
disableTimer: true,
});
return cachedClient;
};
let firstRequest = true;
const coldRequests = new WeakSet<Request>();
const trackRequest = (request: Request) => {
if (firstRequest) coldRequests.add(request);
firstRequest = false;
};

cachedKey = key;
cachedEnvironment = environment;
cachedClient = createClient({
key,
environment,
endpoint: 'https://ingest.sassmaker.com/v1/ingest',
runtime: 'worker',
disableTimer: true,
const stageTimingMiddleware: MiddlewareHandler<ReaderEnv> = async (c, next) => {
const start = performance.now();
await next();
// Telemetry must never change the response or expose request data.
try {
const rate = stageSampleRate(c.env.APP_HEALTH_STAGE_SAMPLE_RATE);
if (!c.env.APP_HEALTH_INGEST_KEY?.trim() || rate === 0 || Math.random() >= rate) return;
const route = routePath(c, -1);
if (!isSafeStageRoute(route)) return;
const client = resolveClient(c);
if (!client) return;
const colo = c.req.raw.cf?.colo;
client.log('api.stage_timing', {
level: 'debug',
props: {
route,
status: c.res.status,
total_ms: Math.max(0, Math.min(600000, Math.round(performance.now() - start))),
edge_cache: 'NONE',
inner_cache: 'NONE',
colo: typeof colo === 'string' && /^[A-Za-z0-9]{1,8}$/.test(colo) ? colo : 'unknown',
cold: coldRequests.has(c.req.raw) ? 1 : 0,
},
});
return cachedClient;
},
});
const delivery = client.flush().catch(() => {});
try {
c.executionCtx.waitUntil(delivery);
} catch {
// Local requests may have no execution context; delivery is already guarded.
}
} catch {
// Client creation, logging, and synchronous flush failures are fail-open.
}
};

return {
appHealthMiddleware: honoMiddleware<ReaderEnv>({ client: resolveClient }),
stageTimingMiddleware,
trackRequest,
};
}

export const appHealthMiddleware = createReaderAppHealthMiddleware();
export const {
appHealthMiddleware,
stageTimingMiddleware: appHealthStageTimingMiddleware,
trackRequest: trackAppHealthRequest,
} = createReaderAppHealthMiddlewares();
2 changes: 1 addition & 1 deletion wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ compatibility_date = "2025-04-01"
compatibility_flags = ["nodejs_compat_v2"]

assets = { directory = "dist", binding = "ASSETS", run_worker_first = [
"/*"] }
"/*", "!/_astro/*", "!/assets/*", "!/images/*", "!/footer-art/*"] }

routes = [
{ pattern = "read.significanthobbies.com", custom_domain = true }
Expand Down
Loading