{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "middleware-next", "title": "Next.js Route Handler Wrapper", "description": "Route-handler wrapper (withAmplio) that owns the request wide event - this is not Next.js middleware.ts.", "type": "registry:lib", "docs": "Wrap route handlers: `export const GET = withAmplio(handler)`. In a create-t3-app layout, `npx @useamplio/cli@alpha init --wire` edits src/app/api/trpc/[trpc]/route.ts for you. Also exports withAmplioRender(name, fn) to give RSC page renders an ambient page.render spine so server-caller calls and facade events correlate.", "dependencies": [ "@useamplio/amplio@^0.1.0-alpha.15" ], "files": [ { "path": "registry/middleware/next.ts", "target": "~/telemetry/middleware/next.ts", "type": "registry:lib", "content": "// Side-effect import: ensures init() from telemetry/logger runs in every module\n// graph that uses this middleware (next dev --turbo compiles instrumentation.ts\n// and route bundles separately, which would otherwise drop events silently).\nimport \"../logger\";\n\nimport {\n createLogger,\n createRequestId,\n createRequestLogger,\n getLogger,\n runWithLogger,\n scheduleFlush,\n type Logger,\n} from \"@useamplio/amplio\";\nimport type { NextRequest } from \"next/server\";\n\nexport interface WithAmplioOptions {\n waitUntil?: (promise: Promise) => void;\n}\n\n// The `never[]` rest constraint accepts any handler shape (route context params\n// included) without resorting to `any`, which trips lint/suspicious/noExplicitAny.\nexport function withAmplio<\n T extends (request: NextRequest, ...args: never[]) => Promise,\n>(handler: T, options?: WithAmplioOptions): T {\n const wrapped = (async (request: NextRequest, ...rest: never[]) => {\n const requestLogger = createRequestLogger({\n method: request.method,\n path: request.nextUrl.pathname,\n }).set({\n http: {\n search: request.nextUrl.search || undefined,\n },\n });\n\n return runWithLogger(requestLogger, async () => {\n try {\n const response = await handler(request, ...rest);\n if (!requestLogger.sealed) {\n requestLogger.set({\n http: { status: response.status },\n status: response.status,\n });\n requestLogger.emit();\n scheduleFlush(options);\n }\n return response;\n } catch (error) {\n if (!requestLogger.sealed) {\n requestLogger.error(error, { status: 500 });\n requestLogger.emit();\n scheduleFlush(options);\n }\n throw error;\n }\n });\n }) as T;\n\n return wrapped;\n}\n\n// Server-side accessor for the ambient request logger. Named get*, not use*:\n// this is not a React hook and never runs on the client.\nexport function getRequestLogger(): Logger {\n return getLogger();\n}\n\n// Next throws for control flow (redirect(), notFound()); those errors carry a\n// digest and are not render failures.\nfunction nextControlFlowDigest(error: unknown): string | undefined {\n if (error !== null && typeof error === \"object\" && \"digest\" in error) {\n const digest = (error as { digest?: unknown }).digest;\n if (typeof digest === \"string\" && digest.startsWith(\"NEXT_\")) {\n return digest;\n }\n }\n return undefined;\n}\n\n/**\n * RSC render spine. Server components do not pass through withAmplio, so a\n * page render otherwise produces uncorrelated rows: standalone trpc.request\n * spines per server-caller call and request_id-less facade events. Wrapping\n * the page establishes an ambient `page.render` spine — amplioTrpcMiddleware\n * annotates it instead of creating standalone spines, and logger.event(Def)\n * rows emitted during the render share its request_id.\n *\n * // src/app/page.tsx\n * export default withAmplioRender(\"home\", async function Home() { … });\n *\n * Note: `next build` static generation also runs the render — those rows are\n * tagged `build_phase: true` by the runtime.\n */\nexport function withAmplioRender(\n page: string,\n render: (...args: A) => R | Promise,\n options?: WithAmplioOptions,\n): (...args: A) => Promise {\n return async (...args: A) => {\n const renderLogger = createLogger({\n event: \"page.render\",\n \"@event\": \"page.render\",\n request_id: createRequestId(),\n page: { name: page },\n });\n\n return runWithLogger(renderLogger, async () => {\n try {\n const result = await render(...args);\n if (!renderLogger.sealed) {\n renderLogger.set({ success: true });\n renderLogger.emit();\n scheduleFlush(options);\n }\n return result;\n } catch (error) {\n if (!renderLogger.sealed) {\n const digest = nextControlFlowDigest(error);\n if (digest) {\n // redirect()/notFound() — record the outcome, not a failure.\n renderLogger.set({ page: { interrupted: digest } });\n } else {\n renderLogger.error(error);\n }\n renderLogger.emit();\n scheduleFlush(options);\n }\n throw error;\n }\n });\n };\n}\n" } ] }