Skip to content

Commit c5fce74

Browse files
msonnbcodex
andauthored
test(e2e): Preserve static Express coverage (#24155)
Preserve transaction-based Express coverage in a dedicated static-lifecycle app before migrating the original app to span streaming. part of #23800 Co-authored-by: GPT-6 <codex@openai.com>
1 parent fe8a4a3 commit c5fce74

14 files changed

Lines changed: 1371 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "node-express-static-app",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "tsc",
7+
"start": "node dist/app.js",
8+
"test": "playwright test",
9+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
10+
"test:build": "pnpm install && pnpm build",
11+
"test:assert": "pnpm test"
12+
},
13+
"dependencies": {
14+
"@modelcontextprotocol/sdk": "^1.26.0",
15+
"@sentry/node": "file:../../packed/sentry-node-packed.tgz",
16+
"@trpc/server": "10.45.4",
17+
"@trpc/client": "10.45.4",
18+
"@types/express": "^4.17.21",
19+
"@types/node": "^18.19.1",
20+
"express": "^4.21.2",
21+
"typescript": "~5.0.0",
22+
"zod": "~3.25.0"
23+
},
24+
"devDependencies": {
25+
"@playwright/test": "~1.56.0",
26+
"@sentry-internal/test-utils": "link:../../../test-utils",
27+
"@sentry/core": "file:../../packed/sentry-core-packed.tgz"
28+
},
29+
"resolutions": {
30+
"@types/qs": "6.9.17"
31+
},
32+
"volta": {
33+
"extends": "../../package.json"
34+
},
35+
"sentryTest": {
36+
"variants": [
37+
{
38+
"build-command": "E2E_TEST_OTEL_SETUP=true pnpm test:build",
39+
"assert-command": "E2E_TEST_OTEL_SETUP=true pnpm test:assert",
40+
"label": "node-express (tracer provider)"
41+
}
42+
]
43+
}
44+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import * as Sentry from '@sentry/node';
2+
// Keep the dedicated MCP server evaluation ahead of initialization without loading Express before its instrumentation.
3+
import './mcpCapturePolicyServer';
4+
5+
declare global {
6+
namespace globalThis {
7+
var transactionIds: string[];
8+
}
9+
}
10+
11+
Sentry.init({
12+
traceLifecycle: 'static',
13+
environment: 'qa', // dynamic sampling bias to keep transactions
14+
dsn: process.env.E2E_TEST_DSN,
15+
includeLocalVariables: true,
16+
debug: !!process.env.DEBUG,
17+
tunnel: `http://localhost:3031/`, // proxy server
18+
tracesSampleRate: 1,
19+
dataCollection: {
20+
genAI: {
21+
inputs: false,
22+
outputs: false,
23+
},
24+
},
25+
// Opt into the Sentry OpenTelemetry tracer provider in the "(tracer provider)" e2e variant.
26+
// Leaving it `undefined` otherwise keeps the SDK's default (no provider).
27+
enableOpenTelemetrySetup: process.env.E2E_TEST_OTEL_SETUP === 'true' ? true : undefined,
28+
integrations: [
29+
Sentry.nativeNodeFetchIntegration({
30+
headersToSpanAttributes: {
31+
responseHeaders: ['content-length'],
32+
},
33+
}),
34+
],
35+
});
36+
37+
import { TRPCError, initTRPC } from '@trpc/server';
38+
import * as trpcExpress from '@trpc/server/adapters/express';
39+
import express from 'express';
40+
import { z } from 'zod';
41+
import { mcpRouter } from './mcp';
42+
43+
const app = express();
44+
const port = 3030;
45+
46+
app.use(express.json());
47+
48+
app.use(mcpRouter);
49+
50+
app.get('/crash-in-with-monitor/:id', async (req, res) => {
51+
try {
52+
await Sentry.withMonitor('express-crash', async () => {
53+
throw new Error(`This is an exception withMonitor: ${req.params.id}`);
54+
});
55+
res.sendStatus(200);
56+
} catch (error: any) {
57+
res.status(500);
58+
res.send({ message: error.message, pid: process.pid });
59+
}
60+
});
61+
62+
app.get('/test-success', function (req, res) {
63+
res.send({ version: 'v1' });
64+
});
65+
66+
app.get('/test-log', function (req, res) {
67+
Sentry.logger.debug('Accessed /test-log route');
68+
res.send({ message: 'Log sent' });
69+
});
70+
71+
app.get('/test-param/:param', function (req, res) {
72+
res.send({ paramWas: req.params.param });
73+
});
74+
75+
app.get('/test-transaction', function (_req, res) {
76+
Sentry.startSpan({ name: 'test-span' }, () => undefined);
77+
78+
res.send({ status: 'ok' });
79+
});
80+
81+
app.get('/test-outgoing-fetch', async function (_req, res) {
82+
const response = await fetch('http://localhost:3030/test-success');
83+
const data = await response.json();
84+
res.send(data);
85+
});
86+
app.get('/test-error', async function (req, res) {
87+
const exceptionId = Sentry.captureException(new Error('This is an error'));
88+
89+
await Sentry.flush(2000);
90+
91+
res.send({ exceptionId });
92+
});
93+
94+
app.get('/test-exception/:id', function (req, _res) {
95+
throw new Error(`This is an exception with id ${req.params.id}`);
96+
});
97+
98+
app.get('/test-local-variables-uncaught', function (req, res) {
99+
const randomVariableToRecord = Math.random();
100+
throw new Error(`Uncaught Local Variable Error - ${JSON.stringify({ randomVariableToRecord })}`);
101+
});
102+
103+
app.get('/test-local-variables-caught', function (req, res) {
104+
const randomVariableToRecord = Math.random();
105+
106+
let exceptionId: string;
107+
try {
108+
throw new Error('Local Variable Error');
109+
} catch (e) {
110+
exceptionId = Sentry.captureException(e);
111+
}
112+
113+
res.send({ exceptionId, randomVariableToRecord });
114+
});
115+
116+
// @ts-ignore
117+
app.use(function onError(err, req, res, next) {
118+
// The error id is attached to `res.sentry` to be returned
119+
// and optionally displayed to the user for support.
120+
res.statusCode = 500;
121+
res.end(res.sentry + '\n');
122+
});
123+
124+
app.listen(port, () => {
125+
console.log(`Example app listening on port ${port}`);
126+
});
127+
128+
Sentry.addEventProcessor(event => {
129+
global.transactionIds = global.transactionIds || [];
130+
131+
if (event.type === 'transaction') {
132+
const eventId = event.event_id;
133+
134+
if (eventId) {
135+
global.transactionIds.push(eventId);
136+
}
137+
}
138+
139+
return event;
140+
});
141+
142+
export const t = initTRPC.context<Context>().create();
143+
144+
const procedure = t.procedure.use(Sentry.trpcMiddleware({ attachRpcInput: true }));
145+
146+
export const appRouter = t.router({
147+
getSomething: procedure.input(z.string()).query(opts => {
148+
return { id: opts.input, name: 'Bilbo' };
149+
}),
150+
createSomething: procedure.mutation(async () => {
151+
await new Promise(resolve => setTimeout(resolve, 400));
152+
return { success: true };
153+
}),
154+
crashSomething: procedure
155+
.input(z.object({ nested: z.object({ nested: z.object({ nested: z.string() }) }) }))
156+
.mutation(() => {
157+
throw new Error('I crashed in a trpc handler');
158+
}),
159+
badRequest: procedure.mutation(() => {
160+
throw new TRPCError({ code: 'BAD_REQUEST', cause: new Error('Bad Request') });
161+
}),
162+
});
163+
164+
export type AppRouter = typeof appRouter;
165+
166+
const createContext = () => ({ someStaticValue: 'asdf' });
167+
type Context = Awaited<ReturnType<typeof createContext>>;
168+
169+
app.use(
170+
'/trpc',
171+
trpcExpress.createExpressMiddleware({
172+
router: appRouter,
173+
createContext,
174+
}),
175+
);

0 commit comments

Comments
 (0)