Skip to content

Commit 2648ef6

Browse files
authored
test(e2e): Add event proxy option to allow for event dumps (#13998)
1 parent ab28544 commit 2648ef6

File tree

7 files changed

+139
-1
lines changed

7 files changed

+139
-1
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = {
2121
'examples/**',
2222
'test/manual/**',
2323
'types/**',
24+
'scripts/*.js',
2425
],
2526
reportUnusedDisableDirectives: true,
2627
overrides: [

.github/workflows/build.yml

+9
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,15 @@ jobs:
10351035
overwrite: true
10361036
retention-days: 7
10371037

1038+
- name: Upload E2E Test Event Dumps
1039+
uses: actions/upload-artifact@v4
1040+
if: always()
1041+
with:
1042+
name: playwright-event-dumps-job_e2e_playwright_tests-${{ matrix.test-application }}
1043+
path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/event-dumps
1044+
overwrite: true
1045+
retention-days: 7
1046+
10381047
- name: Upload test results to Codecov
10391048
if: cancelled() == false
10401049
continue-on-error: true

dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ next-env.d.ts
4343
.vscode
4444

4545
test-results
46+
event-dumps

dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
/// <reference types="next/image-types/global" />
33

44
// NOTE: This file should not be edited
5-
// see https://nextjs.org/docs/basic-features/typescript for more information.
5+
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
13
import { startEventProxyServer } from '@sentry-internal/test-utils';
24

5+
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json')));
6+
37
startEventProxyServer({
48
port: 3031,
59
proxyServerName: 'nextjs-15',
10+
envelopeDumpPath: path.join(
11+
process.cwd(),
12+
`event-dumps/next-${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`,
13+
),
614
});

dev-packages/test-utils/src/event-proxy-server.ts

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ interface EventProxyServerOptions {
1717
port: number;
1818
/** The name for the proxy server used for referencing it with listener functions */
1919
proxyServerName: string;
20+
/** A path to optionally output all Envelopes to. Can be used to compare event payloads before and after changes. */
21+
envelopeDumpPath?: string;
2022
}
2123

2224
interface SentryRequestCallbackData {
@@ -167,6 +169,10 @@ export async function startProxyServer(
167169
* option to this server (like this `tunnel: http://localhost:${port option}/`).
168170
*/
169171
export async function startEventProxyServer(options: EventProxyServerOptions): Promise<void> {
172+
if (options.envelopeDumpPath) {
173+
await fs.promises.mkdir(path.dirname(path.resolve(options.envelopeDumpPath)), { recursive: true });
174+
}
175+
170176
await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => {
171177
const data: SentryRequestCallbackData = {
172178
envelope: parseEnvelope(proxyRequestBody),
@@ -183,6 +189,10 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P
183189
listener(dataString);
184190
});
185191

192+
if (options.envelopeDumpPath) {
193+
fs.appendFileSync(path.resolve(options.envelopeDumpPath), `${JSON.stringify(data.envelope)}\n`, 'utf-8');
194+
}
195+
186196
return [
187197
200,
188198
'{}',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/* eslint-disable no-console */
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
if (process.argv.length < 4) {
7+
throw new Error('Please provide an input and output file path as an argument.');
8+
}
9+
10+
const resolvedInputPath = path.resolve(process.argv[2]);
11+
const resolvedOutputPath = path.resolve(process.argv[3]);
12+
13+
const fileContents = fs.readFileSync(resolvedInputPath, 'utf8');
14+
15+
const transactionNodes = [];
16+
17+
fileContents.split('\n').forEach(serializedEnvelope => {
18+
let envelope;
19+
try {
20+
envelope = JSON.parse(serializedEnvelope);
21+
} catch (e) {
22+
return;
23+
// noop
24+
}
25+
26+
const envelopeItems = envelope[1];
27+
28+
envelopeItems.forEach(([envelopeItemHeader, transaction]) => {
29+
if (envelopeItemHeader.type === 'transaction') {
30+
const rootNode = {
31+
runtime: transaction.contexts.runtime?.name,
32+
op: transaction.contexts.trace.op,
33+
name: transaction.transaction,
34+
children: [],
35+
};
36+
37+
const spanMap = new Map();
38+
spanMap.set(transaction.contexts.trace.span_id, rootNode);
39+
40+
transaction.spans.forEach(span => {
41+
const node = {
42+
op: span.data['sentry.op'],
43+
name: span.description,
44+
parent_span_id: span.parent_span_id,
45+
children: [],
46+
};
47+
spanMap.set(span.span_id, node);
48+
});
49+
50+
transaction.spans.forEach(span => {
51+
const node = spanMap.get(span.span_id);
52+
if (node && node.parent_span_id) {
53+
const parentNode = spanMap.get(node.parent_span_id);
54+
parentNode.children.push(node);
55+
}
56+
});
57+
58+
transactionNodes.push(rootNode);
59+
}
60+
});
61+
});
62+
63+
const output = transactionNodes
64+
.sort((a, b) => {
65+
const aSerialized = serializeNode(a);
66+
const bSerialized = serializeNode(b);
67+
if (aSerialized < bSerialized) {
68+
return -1;
69+
} else if (aSerialized > bSerialized) {
70+
return 1;
71+
} else {
72+
return 0;
73+
}
74+
})
75+
.map(node => buildDeterministicStringFromNode(node))
76+
.join('\n\n-----------------------\n\n');
77+
78+
fs.writeFileSync(resolvedOutputPath, output, 'utf-8');
79+
80+
// ------- utility fns ----------
81+
82+
function buildDeterministicStringFromNode(node, depth = 0) {
83+
const mainParts = [];
84+
if (node.runtime) {
85+
mainParts.push(`(${node.runtime})`);
86+
}
87+
mainParts.push(`${node.op ?? 'default'} -`);
88+
mainParts.push(node.name);
89+
const main = mainParts.join(' ');
90+
const children = node.children
91+
.sort((a, b) => {
92+
const aSerialized = serializeNode(a);
93+
const bSerialized = serializeNode(b);
94+
if (aSerialized < bSerialized) {
95+
return -1;
96+
} else if (aSerialized > bSerialized) {
97+
return 1;
98+
} else {
99+
return 0;
100+
}
101+
})
102+
.map(child => '\n' + buildDeterministicStringFromNode(child, depth + 1))
103+
.join('');
104+
return `${main}${children}`.split('\n').join('\n ');
105+
}
106+
107+
function serializeNode(node) {
108+
return [node.op, node.name, node.runtime].join('---');
109+
}

0 commit comments

Comments
 (0)