Skip to content

Commit 3980730

Browse files
author
Luca Forstner
authored
feat: Add trpcMiddleware back to serverside SDKs (#11374)
1 parent f2b1f3c commit 3980730

File tree

14 files changed

+264
-6
lines changed

14 files changed

+264
-6
lines changed

MIGRATION.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,7 @@ Sentry.getGlobalScope().addEventProcessor(event => {
571571

572572
The `lastEventId` function has been removed. See [below](./MIGRATION.md#deprecate-lasteventid) for more details.
573573

574-
#### Remove `void` from transport return types
574+
#### Removal of `void` from transport return types
575575

576576
The `send` method on the `Transport` interface now always requires a `TransportMakeRequestResponse` to be returned in
577577
the promise. This means that the `void` return type is no longer allowed.
@@ -590,7 +590,7 @@ interface Transport {
590590
}
591591
```
592592

593-
#### Remove `addGlobalEventProcessor` in favor of `addEventProcessor`
593+
#### Removal of `addGlobalEventProcessor` in favor of `addEventProcessor`
594594

595595
In v8, we are removing the `addGlobalEventProcessor` function in favor of `addEventProcessor`.
596596

@@ -610,6 +610,23 @@ addEventProcessor(event => {
610610
});
611611
```
612612

613+
#### Removal of `Sentry.Handlers.trpcMiddleware()` in favor of `Sentry.trpcMiddleware()`
614+
615+
The Sentry tRPC middleware got moved from `Sentry.Handlers.trpcMiddleware()` to `Sentry.trpcMiddleware()`. Functionally
616+
they are the same:
617+
618+
```js
619+
// v7
620+
import * as Sentry from '@sentry/node';
621+
Sentry.Handlers.trpcMiddleware();
622+
```
623+
624+
```js
625+
// v8
626+
import * as Sentry from '@sentry/node';
627+
Sentry.trpcMiddleware();
628+
```
629+
613630
### Browser SDK (Browser, React, Vue, Angular, Ember, etc.)
614631

615632
Removed top-level exports: `Offline`, `makeXHRTransport`, `BrowserTracing`, `wrap`

dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ for (const dependent of dependentsToCheck) {
9999
}
100100

101101
if (Object.keys(missingExports).length > 0) {
102-
console.error('\n❌ Found missing exports from @sentry/node in the following packages:\n');
102+
console.log('\n❌ Found missing exports from @sentry/node in the following packages:\n');
103103
console.log(JSON.stringify(missingExports, null, 2));
104104
process.exit(1);
105105
}

dev-packages/e2e-tests/test-applications/node-express-app/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
"test:assert": "pnpm test"
1212
},
1313
"dependencies": {
14+
"@sentry/core": "latest || *",
1415
"@sentry/node": "latest || *",
1516
"@sentry/types": "latest || *",
16-
"express": "4.19.2",
17+
"@trpc/server": "10.45.2",
18+
"@trpc/client": "10.45.2",
1719
"@types/express": "4.17.17",
1820
"@types/node": "18.15.1",
19-
"typescript": "4.9.5"
21+
"express": "4.19.2",
22+
"typescript": "4.9.5",
23+
"zod": "^3.22.4"
2024
},
2125
"devDependencies": {
2226
"@playwright/test": "^1.27.1",

dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as Sentry from '@sentry/node';
2+
import { TRPCError, initTRPC } from '@trpc/server';
3+
import * as trpcExpress from '@trpc/server/adapters/express';
24
import express from 'express';
5+
import { z } from 'zod';
36

47
declare global {
58
namespace globalThis {
@@ -94,3 +97,36 @@ Sentry.addEventProcessor(event => {
9497

9598
return event;
9699
});
100+
101+
export const t = initTRPC.context<Context>().create();
102+
103+
const procedure = t.procedure.use(Sentry.trpcMiddleware({ attachRpcInput: true }));
104+
105+
export const appRouter = t.router({
106+
getSomething: procedure.input(z.string()).query(opts => {
107+
return { id: opts.input, name: 'Bilbo' };
108+
}),
109+
createSomething: procedure.mutation(async () => {
110+
await new Promise(resolve => setTimeout(resolve, 400));
111+
return { success: true };
112+
}),
113+
crashSomething: procedure.mutation(() => {
114+
throw new Error('I crashed in a trpc handler');
115+
}),
116+
dontFindSomething: procedure.mutation(() => {
117+
throw new TRPCError({ code: 'NOT_FOUND', cause: new Error('Page not found') });
118+
}),
119+
});
120+
121+
export type AppRouter = typeof appRouter;
122+
123+
const createContext = () => ({ someStaticValue: 'asdf' });
124+
type Context = Awaited<ReturnType<typeof createContext>>;
125+
126+
app.use(
127+
'/trpc',
128+
trpcExpress.createExpressMiddleware({
129+
router: appRouter,
130+
createContext,
131+
}),
132+
);

dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { expect, test } from '@playwright/test';
2+
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
23
import axios, { AxiosError, AxiosResponse } from 'axios';
3-
import { waitForError } from '../event-proxy-server';
4+
import { waitForError, waitForTransaction } from '../event-proxy-server';
5+
import type { AppRouter } from '../src/app';
46

57
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
68
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
@@ -130,3 +132,96 @@ test('Should record uncaught exceptions with local variable', async ({ baseURL }
130132

131133
expect(frames[frames.length - 1].vars?.randomVariableToRecord).toBeDefined();
132134
});
135+
136+
test('Should record transaction for trpc query', async ({ baseURL }) => {
137+
const transactionEventPromise = waitForTransaction('node-express-app', transactionEvent => {
138+
return transactionEvent.transaction === 'trpc/getSomething';
139+
});
140+
141+
const trpcClient = createTRPCProxyClient<AppRouter>({
142+
links: [
143+
httpBatchLink({
144+
url: `${baseURL}/trpc`,
145+
}),
146+
],
147+
});
148+
149+
await trpcClient.getSomething.query('foobar');
150+
151+
await expect(transactionEventPromise).resolves.toBeDefined();
152+
const transaction = await transactionEventPromise;
153+
154+
expect(transaction.contexts?.trpc).toMatchObject({
155+
procedure_type: 'query',
156+
input: 'foobar',
157+
});
158+
});
159+
160+
test('Should record transaction for trpc mutation', async ({ baseURL }) => {
161+
const transactionEventPromise = waitForTransaction('node-express-app', transactionEvent => {
162+
return transactionEvent.transaction === 'trpc/createSomething';
163+
});
164+
165+
const trpcClient = createTRPCProxyClient<AppRouter>({
166+
links: [
167+
httpBatchLink({
168+
url: `${baseURL}/trpc`,
169+
}),
170+
],
171+
});
172+
173+
await trpcClient.createSomething.mutate();
174+
175+
await expect(transactionEventPromise).resolves.toBeDefined();
176+
const transaction = await transactionEventPromise;
177+
178+
expect(transaction.contexts?.trpc).toMatchObject({
179+
procedure_type: 'mutation',
180+
});
181+
});
182+
183+
test('Should record transaction and error for a crashing trpc handler', async ({ baseURL }) => {
184+
const transactionEventPromise = waitForTransaction('node-express-app', transactionEvent => {
185+
return transactionEvent.transaction === 'trpc/crashSomething';
186+
});
187+
188+
const errorEventPromise = waitForError('node-express-app', errorEvent => {
189+
return !!errorEvent?.exception?.values?.some(exception => exception.value?.includes('I crashed in a trpc handler'));
190+
});
191+
192+
const trpcClient = createTRPCProxyClient<AppRouter>({
193+
links: [
194+
httpBatchLink({
195+
url: `${baseURL}/trpc`,
196+
}),
197+
],
198+
});
199+
200+
await expect(trpcClient.crashSomething.mutate()).rejects.toBeDefined();
201+
202+
await expect(transactionEventPromise).resolves.toBeDefined();
203+
await expect(errorEventPromise).resolves.toBeDefined();
204+
});
205+
206+
test('Should record transaction and error for a trpc handler that returns a status code', async ({ baseURL }) => {
207+
const transactionEventPromise = waitForTransaction('node-express-app', transactionEvent => {
208+
return transactionEvent.transaction === 'trpc/dontFindSomething';
209+
});
210+
211+
const errorEventPromise = waitForError('node-express-app', errorEvent => {
212+
return !!errorEvent?.exception?.values?.some(exception => exception.value?.includes('Page not found'));
213+
});
214+
215+
const trpcClient = createTRPCProxyClient<AppRouter>({
216+
links: [
217+
httpBatchLink({
218+
url: `${baseURL}/trpc`,
219+
}),
220+
],
221+
});
222+
223+
await expect(trpcClient.dontFindSomething.mutate()).rejects.toBeDefined();
224+
225+
await expect(transactionEventPromise).resolves.toBeDefined();
226+
await expect(errorEventPromise).resolves.toBeDefined();
227+
});

packages/aws-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export {
9090
spotlightIntegration,
9191
initOpenTelemetry,
9292
spanToJSON,
93+
trpcMiddleware,
9394
} from '@sentry/node';
9495

9596
export {

packages/bun/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export {
111111
spotlightIntegration,
112112
initOpenTelemetry,
113113
spanToJSON,
114+
trpcMiddleware,
114115
} from '@sentry/node';
115116

116117
export {

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,4 @@ export { metricsDefault } from './metrics/exports-default';
105105
export { BrowserMetricsAggregator } from './metrics/browser-aggregator';
106106
export { getMetricSummaryJsonForSpan } from './metrics/metric-summary';
107107
export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch';
108+
export { trpcMiddleware } from './trpc';

packages/core/src/trpc.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { isThenable, normalize } from '@sentry/utils';
2+
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
4+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
5+
captureException,
6+
setContext,
7+
startSpanManual,
8+
} from '.';
9+
import { getClient } from './currentScopes';
10+
11+
interface SentryTrpcMiddlewareOptions {
12+
/** Whether to include procedure inputs in reported events. Defaults to `false`. */
13+
attachRpcInput?: boolean;
14+
}
15+
16+
export interface SentryTrpcMiddlewareArguments<T> {
17+
path?: unknown;
18+
type?: unknown;
19+
next: () => T;
20+
rawInput?: unknown;
21+
}
22+
23+
const trpcCaptureContext = { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } };
24+
25+
/**
26+
* Sentry tRPC middleware that captures errors and creates spans for tRPC procedures.
27+
*/
28+
export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) {
29+
return function <T>(opts: SentryTrpcMiddlewareArguments<T>): T {
30+
const { path, type, next, rawInput } = opts;
31+
const client = getClient();
32+
const clientOptions = client && client.getOptions();
33+
34+
const trpcContext: Record<string, unknown> = {
35+
procedure_type: type,
36+
};
37+
38+
if (options.attachRpcInput !== undefined ? options.attachRpcInput : clientOptions && clientOptions.sendDefaultPii) {
39+
trpcContext.input = normalize(rawInput);
40+
}
41+
42+
setContext('trpc', trpcContext);
43+
44+
function captureIfError(nextResult: unknown): void {
45+
// TODO: Set span status based on what TRPCError was encountered
46+
if (
47+
typeof nextResult === 'object' &&
48+
nextResult !== null &&
49+
'ok' in nextResult &&
50+
!nextResult.ok &&
51+
'error' in nextResult
52+
) {
53+
captureException(nextResult.error, trpcCaptureContext);
54+
}
55+
}
56+
57+
return startSpanManual(
58+
{
59+
name: `trpc/${path}`,
60+
op: 'rpc.server',
61+
forceTransaction: true,
62+
attributes: {
63+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
64+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.rpc.trpc',
65+
},
66+
},
67+
span => {
68+
let maybePromiseResult;
69+
try {
70+
maybePromiseResult = next();
71+
} catch (e) {
72+
captureException(e, trpcCaptureContext);
73+
span.end();
74+
throw e;
75+
}
76+
77+
if (isThenable(maybePromiseResult)) {
78+
return maybePromiseResult.then(
79+
nextResult => {
80+
captureIfError(nextResult);
81+
span.end();
82+
return nextResult;
83+
},
84+
e => {
85+
captureException(e, trpcCaptureContext);
86+
span.end();
87+
throw e;
88+
},
89+
) as T;
90+
} else {
91+
captureIfError(maybePromiseResult);
92+
span.end();
93+
return maybePromiseResult;
94+
}
95+
},
96+
);
97+
};
98+
}

packages/google-cloud-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export {
9090
spotlightIntegration,
9191
initOpenTelemetry,
9292
spanToJSON,
93+
trpcMiddleware,
9394
} from '@sentry/node';
9495

9596
export {

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export {
105105
withActiveSpan,
106106
getRootSpan,
107107
spanToJSON,
108+
trpcMiddleware,
108109
} from '@sentry/core';
109110

110111
export type {

packages/remix/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export {
9494
setupHapiErrorHandler,
9595
spotlightIntegration,
9696
setupFastifyErrorHandler,
97+
trpcMiddleware,
9798
} from '@sentry/node';
9899

99100
// Keeping the `*` exports for backwards compatibility and types

packages/sveltekit/src/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export {
6969
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
7070
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
7171
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
72+
trpcMiddleware,
7273
} from '@sentry/node';
7374

7475
// We can still leave this for the carrier init and type exports

packages/vercel-edge/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export {
6969
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
7070
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
7171
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
72+
trpcMiddleware,
7273
} from '@sentry/core';
7374

7475
export { VercelEdgeClient } from './client';

0 commit comments

Comments
 (0)