From e4dba19ac12523c47c25db33753f3f266f1fe8be Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Tue, 29 Sep 2026 04:29:39 +0530 Subject: [PATCH] Add optional App Health endpoint telemetry --- package.json | 1 + pnpm-lock.yaml | 19 ++++++++ src/lib/worker-env.ts | 2 + src/worker.ts | 3 ++ src/worker/app-health.test.ts | 82 +++++++++++++++++++++++++++++++++++ src/worker/app-health.ts | 45 +++++++++++++++++++ 6 files changed, 152 insertions(+) create mode 100644 src/worker/app-health.test.ts create mode 100644 src/worker/app-health.ts diff --git a/package.json b/package.json index 1011db2..ac546b6 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@radix-ui/react-dropdown-menu": "2.1.24", "@radix-ui/react-label": "2.1.15", "@radix-ui/react-slot": "1.3.3", + "@saas-maker/app-health": "https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz", "@saas-maker/feedback": "^0.2.1", "@tailwindcss/typography": "0.5.20", "@tanstack/react-query": "5.101.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e80afae..d3a5363 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@radix-ui/react-slot': specifier: 1.3.3 version: 1.3.3(@types/react@19.2.17)(react@19.2.8) + '@saas-maker/app-health': + specifier: https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz + version: https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz(hono@4.13.1) '@saas-maker/feedback': specifier: ^0.2.1 version: 0.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -2435,6 +2438,18 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@saas-maker/app-health@https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz': + resolution: {tarball: https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz} + version: 0.4.0 + peerDependencies: + express: '>=4.19.0 <6' + hono: '>=4.10.0 <5' + peerDependenciesMeta: + express: + optional: true + hono: + optional: true + '@saas-maker/feedback@0.2.1': resolution: {integrity: sha512-ajtrH7t/hhzxrNzX2BU0i5nfGpXNu7w4EQCFBajsuFn255O29trRCu3VNL1NoQhWwypjEEdeaK4DqoEY4zd7rw==} peerDependencies: @@ -6892,6 +6907,10 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@saas-maker/app-health@https://github.com/sass-maker/app-health/releases/download/node-v0.4.0/saas-maker-app-health-0.4.0.tgz(hono@4.13.1)': + optionalDependencies: + hono: 4.13.1 + '@saas-maker/feedback@0.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: react: 19.2.8 diff --git a/src/lib/worker-env.ts b/src/lib/worker-env.ts index d94014c..985705a 100644 --- a/src/lib/worker-env.ts +++ b/src/lib/worker-env.ts @@ -14,6 +14,8 @@ export type WorkerEnv = { LOCAL_AI_URL?: string; CLI_BRIDGE_URL?: string; NODE_ENV?: string; + APP_HEALTH_INGEST_KEY?: string; + APP_HEALTH_ENVIRONMENT?: string; PDFS_BUCKET: R2Bucket; ASSETS: Fetcher; }; diff --git a/src/worker.ts b/src/worker.ts index 0605bc6..167b917 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -16,6 +16,7 @@ import miscRoutes from './worker/routes/misc'; import pdfRoutes from './worker/routes/pdf'; import rssRoutes from './worker/routes/rss'; import shareRoutes from './worker/routes/share'; +import { appHealthMiddleware } from './worker/app-health'; const SECURITY_HEADERS: Record = { 'X-Content-Type-Options': 'nosniff', @@ -40,6 +41,8 @@ api.use('*', async (c, next) => { await next(); }); +api.use('*', appHealthMiddleware); + api.use('/api/*', async (c, next) => { await next(); const response = c.res; diff --git a/src/worker/app-health.test.ts b/src/worker/app-health.test.ts new file mode 100644 index 0000000..f649e12 --- /dev/null +++ b/src/worker/app-health.test.ts @@ -0,0 +1,82 @@ +import { Hono } from 'hono'; +import { describe, expect, it, vi } from 'vitest'; + +import type { WorkerEnv } from '../lib/worker-env'; +import { createReaderAppHealthMiddleware } 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.get('/api/articles/:articleId', (c) => c.json({ ok: true })); + + const response = await app.request('/api/articles/private-id?search=secret', {}, {}); + + expect(response.status).toBe(200); + expect(createClient).not.toHaveBeenCalled(); + }); + + it('records only the matched route summary and preserves the response', async () => { + const record = vi.fn(); + const flush = vi.fn().mockResolvedValue(undefined); + const waitUntil = vi.fn(); + const createClient = vi.fn(() => ({ record, flush }) as never); + const app = new Hono<{ Bindings: WorkerEnv }>(); + app.use('*', createReaderAppHealthMiddleware(createClient)); + app.on(['GET', 'POST'], '/api/articles/:articleId', (c) => + c.json({ content: 'private article body' }) + ); + + const response = await app.request( + '/api/articles/private-id?search=private-query', + { + method: 'POST', + headers: { Authorization: 'Bearer private-token', Cookie: 'session=private-cookie' }, + body: 'private request body', + }, + { APP_HEALTH_INGEST_KEY: 'synthetic-test-key' } as WorkerEnv, + { waitUntil } as unknown as ExecutionContext + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ content: 'private article body' }); + expect(createClient).toHaveBeenCalledWith({ + key: 'synthetic-test-key', + environment: 'production', + endpoint: 'https://ingest.sassmaker.com/v1/ingest', + runtime: 'worker', + disableTimer: true, + }); + expect(record).toHaveBeenCalledOnce(); + const event = record.mock.calls[0][0]; + expect(event).toMatchObject({ + method: 'POST', + route: '/api/articles/:articleId', + status_code: 200, + }); + expect(JSON.stringify(event)).not.toContain('private-id'); + expect(JSON.stringify(event)).not.toContain('private-query'); + expect(JSON.stringify(event)).not.toContain('private-token'); + expect(JSON.stringify(event)).not.toContain('private-cookie'); + expect(JSON.stringify(event)).not.toContain('private request body'); + expect(JSON.stringify(event)).not.toContain('private article body'); + expect(flush).toHaveBeenCalledOnce(); + expect(waitUntil).toHaveBeenCalledOnce(); + + const secondResponse = await app.request( + '/api/articles/another-private-id?search=another-private-query', + {}, + { APP_HEALTH_INGEST_KEY: 'synthetic-test-key' } as WorkerEnv, + { waitUntil } as unknown as ExecutionContext + ); + + expect(secondResponse.status).toBe(200); + expect(createClient).toHaveBeenCalledOnce(); + expect(record).toHaveBeenCalledTimes(2); + expect(waitUntil).toHaveBeenCalledTimes(2); + expect(record.mock.calls[1][0].route).toBe('/api/articles/:articleId'); + expect(JSON.stringify(record.mock.calls[1][0])).not.toContain('another-private-id'); + expect(JSON.stringify(record.mock.calls[1][0])).not.toContain('another-private-query'); + }); +}); diff --git a/src/worker/app-health.ts b/src/worker/app-health.ts new file mode 100644 index 0000000..57fe692 --- /dev/null +++ b/src/worker/app-health.ts @@ -0,0 +1,45 @@ +import { + createAppHealthClient, + type AppHealthClient, + type AppHealthClientOptions, +} from '@saas-maker/app-health'; +import { honoMiddleware } from '@saas-maker/app-health/hono'; +import type { Context, Env } from 'hono'; + +import type { WorkerEnv } from '../lib/worker-env'; + +type ReaderEnv = Env & { Bindings: WorkerEnv }; +type ClientFactory = (options: AppHealthClientOptions) => AppHealthClient; + +export function createReaderAppHealthMiddleware( + createClient: ClientFactory = createAppHealthClient +) { + let cachedKey: string | null = null; + let cachedEnvironment = ''; + let cachedClient: AppHealthClient | null = null; + + return honoMiddleware({ + client: (context: Context) => { + 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; + } + + cachedKey = key; + cachedEnvironment = environment; + cachedClient = createClient({ + key, + environment, + endpoint: 'https://ingest.sassmaker.com/v1/ingest', + runtime: 'worker', + disableTimer: true, + }); + return cachedClient; + }, + }); +} + +export const appHealthMiddleware = createReaderAppHealthMiddleware();