Observe one existing Next.js route
Use this recipe in an existing Next.js App Router application on a current patched release with a supported Node.js deployment. The after API has been stable since Next.js 15.1; that minimum API version is not a recommendation to install an old unpatched release. It observes the handler returning a Response or throwing; it does not measure delivery to the browser, completion of a streamed body, or a background business task.
Before you change the route
Choose one route you own and an approved operation you can safely run. Keep its authentication, validation and application code. Set TELEMETRY_API_KEY only in the server environment, never with a NEXT_PUBLIC_ prefix. An anonymous key from the homepage can send and query; an account key needs read-and-write scope for the verification step. Use an ingest-only key for an application that only sends.
Add the server helper
Save this as lib/with-telemetry.js. Use a static route template that contains no customer identifier. The generated invocation ID identifies this one handler call, not a person or a logical job. No URL, body, cookies, headers or error messages are sent.
import { after } from 'next/server';
import { randomUUID } from 'node:crypto';
function warnTelemetry(message) {
try {
console.warn(message);
} catch {
// Diagnostic failure must not change the application outcome.
}
}
export function withTelemetry(handler, routeTemplate) {
return async function observedHandler(request, context) {
const started = performance.now();
const invocationId = randomUUID();
let outcome = 'threw';
let statusCode = null;
try {
const response = await handler(request, context);
outcome = 'returned_response';
statusCode = response.status;
return response;
} finally {
const event = {
invocation_id: invocationId,
route_template: routeTemplate,
method: request.method,
handler_outcome: outcome,
status_code: statusCode,
latency_ms: performance.now() - started,
environment: process.env.NODE_ENV || 'development',
};
try {
after(async () => {
const key = process.env.TELEMETRY_API_KEY;
if (!key) {
warnTelemetry('Telemetry key missing; event not sent');
return;
}
try {
const response = await fetch('https://api.telemetry.sh/log', {
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
signal: AbortSignal.timeout(2000),
body: JSON.stringify({ table: 'nextjs_route_observed', data: event }),
});
if (!response.ok) warnTelemetry('Telemetry event send rejected');
} catch {
warnTelemetry('Telemetry event send failed');
}
});
} catch {
warnTelemetry('Telemetry background scheduling failed');
}
}
};
}
Wrap your existing handler
Rename the existing exported GET function to existingGET without changing its body. Then export the wrapper below. Adapt the method and static route template to the actual route; do not create a dummy successful handler to finish setup.
import { withTelemetry } from "@/lib/with-telemetry";
export const GET = withTelemetry(existingGET, "/api/reports/:id");
Verify an actual invocation
Run the approved operation through your application, then query the table in Telemetry. Match the route, method, time, outcome and status to the operation you just observed. HTTP acceptance alone is not verification. An offline fixture, a demo route or an agent QA run is not a customer integration; keep fictional sends in telemetry_quickstart.
SELECT invocation_id, route_template, method, handler_outcome,
status_code, latency_ms, environment, timestamp_utc
FROM nextjs_route_observed
WHERE timestamp_utc >= now() - INTERVAL '15 minutes'
ORDER BY timestamp_utc DESC
LIMIT 20;
What this does not guarantee
Next.js after schedules the send after the response finishes. It is not a durable queue: platform duration limits, shutdowns, timeouts and network failures can lose events. The send has a two-second timeout and fixed, redacted warning messages; it makes no automatic retry or deduplication promise. Keep existing logs and use a durable delivery path for required audit records.
latency_ms ends when the handler returns or throws, before event delivery. returned_response may include a 4xx or 5xx response. threw can include framework redirects and control flow, so it is not automatically a server failure. Missing keys and telemetry failures must not replace the original response or error. Check the helper in your own runtime before rollout.
Framework reference and next steps
The Next.js after reference documents version and deployment support. Static export is unsupported; adapters need explicit support. Response preservation, thrown errors and failed telemetry delivery were checked in an isolated Next.js 16.3.8 development App Router app, with event delivery intercepted and external networking disabled. This does not verify live Telemetry ingestion or your production host. Continue with event-ingestion verification and the JavaScript SDK guide when you need broader instrumentation.