diff --git a/examples/trpc/src/server/api/routers/post.ts b/examples/trpc/src/server/api/routers/post.ts index 16735175..aceab3f8 100644 --- a/examples/trpc/src/server/api/routers/post.ts +++ b/examples/trpc/src/server/api/routers/post.ts @@ -1,32 +1,31 @@ -import { z } from "zod"; +import { z } from 'zod'; -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { createTRPCRouter, publicProcedure } from '~/server/api/trpc'; let post = { id: 1, - name: "Hello World", + name: 'Hello World', }; export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), + hello: publicProcedure.input(z.object({ text: z.string() })).query(({ ctx, input }) => { + ctx.log.info('Hello from the `hello` procedure', { input }); + return { + greeting: `Hello ${input.text}`, + }; + }), - create: publicProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ input }) => { - // simulate a slow db call - await new Promise((resolve) => setTimeout(resolve, 1000)); + create: publicProcedure.input(z.object({ name: z.string().min(1) })).mutation(async ({ ctx, input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); - post = { id: post.id + 1, name: input.name }; - return post; - }), + post = { id: post.id + 1, name: input.name }; + ctx.log.info('Hello from `create` procedure.', { post }); + return post; + }), - getLatest: publicProcedure.query(() => { + getLatest: publicProcedure.query((opts) => { + opts.ctx.log.info('Hello from `getLatest` procedure.', { latestPost: post }); return post; }), }); diff --git a/examples/trpc/src/server/api/trpc.ts b/examples/trpc/src/server/api/trpc.ts index 8984c306..787e8b81 100644 --- a/examples/trpc/src/server/api/trpc.ts +++ b/examples/trpc/src/server/api/trpc.ts @@ -6,10 +6,11 @@ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will * need to use are documented accordingly near the end. */ -import { initTRPC } from "@trpc/server"; -import { type NextRequest } from "next/server"; -import superjson from "superjson"; -import { ZodError } from "zod"; +import { initTRPC } from '@trpc/server'; +import superjson from 'superjson'; +import { ZodError } from 'zod'; +import { type NextRequest } from 'next/server'; +import { axiomTRPCMiddleware, type axiomTRPCMiddlewareCtx } from 'next-axiom'; /** * 1. CONTEXT @@ -48,9 +49,18 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createTRPCContext = (opts: { req: NextRequest }) => { // Fetch stuff that depends on the request - return createInnerTRPCContext({ - headers: opts.req.headers, - }); + return { + req: opts.req, + axiomTRPCMeta: { + foo: 'bar', + random: Math.random(), + }, + ...createInnerTRPCContext({ + headers: opts.req.headers, + }), + // optional, but gives better errors if the context + // doesn't match what the axiomTRPCMiddleware expects + } satisfies axiomTRPCMiddlewareCtx; }; /** @@ -68,8 +78,7 @@ const t = initTRPC.context().create({ ...shape, data: { ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, + zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, @@ -96,4 +105,8 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; + +// we probably want all procedures to log, so we attach the axiomMiddleware to a base procedure +const baseProcedure = t.procedure.use(axiomTRPCMiddleware); + +export const publicProcedure = baseProcedure; diff --git a/package-lock.json b/package-lock.json index 1cf91a28..03564246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.1.0", "license": "MIT", "dependencies": { + "@trpc/server": "^10.43.3", "whatwg-fetch": "^3.6.2" }, "devDependencies": { @@ -1252,6 +1253,17 @@ "node": ">= 10" } }, + "node_modules/@trpc/server": { + "version": "10.43.3", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.43.3.tgz", + "integrity": "sha512-U7HsNtgjupNvPzH6ho4rPlIXZ2t5PctOH6pmFX4jv4YJi98RjGuOhHUNhiiVb8KUw6Kuh5EHTAv7cUV+igbMuQ==", + "funding": [ + "https://trpc.io/sponsor" + ], + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", @@ -5628,6 +5640,11 @@ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true }, + "@trpc/server": { + "version": "10.43.3", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.43.3.tgz", + "integrity": "sha512-U7HsNtgjupNvPzH6ho4rPlIXZ2t5PctOH6pmFX4jv4YJi98RjGuOhHUNhiiVb8KUw6Kuh5EHTAv7cUV+igbMuQ==" + }, "@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", diff --git a/package.json b/package.json index bf75e5ba..63bfb6b4 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "typescript": "^5.1.6" }, "dependencies": { + "@trpc/server": "^10.43.3", "whatwg-fetch": "^3.6.2" } } diff --git a/src/index.ts b/src/index.ts index 9524d924..28362908 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from './config'; export { withAxiom, type AxiomRequest, withAxiomNextConfig, withAxiomRouteHandler } from './withAxiom'; export * from './webVitals'; export { useLogger } from './hooks'; +export { axiomTRPCMiddleware, type axiomTRPCMiddlewareCtx } from './trpc'; diff --git a/src/trpc.ts b/src/trpc.ts new file mode 100644 index 00000000..5c17d656 --- /dev/null +++ b/src/trpc.ts @@ -0,0 +1,52 @@ +import { experimental_standaloneMiddleware } from '@trpc/server'; +import { Logger, type RequestReport } from './logger'; +import { type NextRequest } from 'next/server'; + +export type axiomTRPCMiddlewareCtx = { + /** + * TODO: + * I think it's probably better to pass req at the root instead of axiomTRPCMeta + * so that it can also be used in other places instead of possibly needing to be passed twice + */ + req: Request | NextRequest; + /** + * TODO: + * figure out the best name for this - it's anything you want to stick on all logs + * that are sent throughout the duration of this procedure. + */ + axiomTRPCMeta: Record; +}; + +export const axiomTRPCMiddleware = experimental_standaloneMiddleware<{ + ctx: axiomTRPCMiddlewareCtx; +}>().create((opts) => { + const { req } = opts.ctx; + + let region = ''; + if ('geo' in req) { + region = req.geo?.region ?? ''; + } + + const report: RequestReport = { + startTime: new Date().getTime(), + path: req.url, + method: req.method, + host: req.headers.get('host'), + userAgent: req.headers.get('user-agent'), + scheme: 'https', + ip: req.headers.get('x-forwarded-for'), + region, + }; + + const log = new Logger({ + args: { + input: opts.rawInput, // TODO: put something if nullish? + axiomTRPCMeta: opts.ctx.axiomTRPCMeta, + }, + req: report, + }); + + return opts.next({ + ctx: { log }, + }); +});