From 2648ef63b71cbdfd03359c188470ae14cd481a4e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 21 Oct 2024 10:22:59 +0200 Subject: [PATCH] test(e2e): Add event proxy option to allow for event dumps (#13998) --- .eslintrc.js | 1 + .github/workflows/build.yml | 9 ++ .../test-applications/nextjs-15/.gitignore | 1 + .../test-applications/nextjs-15/next-env.d.ts | 2 +- .../nextjs-15/start-event-proxy.mjs | 8 ++ .../test-utils/src/event-proxy-server.ts | 10 ++ ...malize-e2e-test-dump-transaction-events.js | 109 ++++++++++++++++++ 7 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 scripts/normalize-e2e-test-dump-transaction-events.js diff --git a/.eslintrc.js b/.eslintrc.js index a9fb5f421af5..53944e16d9dc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,6 +21,7 @@ module.exports = { 'examples/**', 'test/manual/**', 'types/**', + 'scripts/*.js', ], reportUnusedDisableDirectives: true, overrides: [ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce139e9e7787..299ed59cd220 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1035,6 +1035,15 @@ jobs: overwrite: true retention-days: 7 + - name: Upload E2E Test Event Dumps + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-event-dumps-job_e2e_playwright_tests-${{ matrix.test-application }} + path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/event-dumps + overwrite: true + retention-days: 7 + - name: Upload test results to Codecov if: cancelled() == false continue-on-error: true diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore index e799cc33c4e7..ebdbfc025b6a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore @@ -43,3 +43,4 @@ next-env.d.ts .vscode test-results +event-dumps diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts index 4f11a03dc6cc..40c3d68096c2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs index 90d736790faa..959b40d253e8 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs @@ -1,6 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; import { startEventProxyServer } from '@sentry-internal/test-utils'; +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + startEventProxyServer({ port: 3031, proxyServerName: 'nextjs-15', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/next-${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), }); diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 17922a4f90aa..448dd6e34ef0 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -17,6 +17,8 @@ interface EventProxyServerOptions { port: number; /** The name for the proxy server used for referencing it with listener functions */ proxyServerName: string; + /** A path to optionally output all Envelopes to. Can be used to compare event payloads before and after changes. */ + envelopeDumpPath?: string; } interface SentryRequestCallbackData { @@ -167,6 +169,10 @@ export async function startProxyServer( * option to this server (like this `tunnel: http://localhost:${port option}/`). */ export async function startEventProxyServer(options: EventProxyServerOptions): Promise { + if (options.envelopeDumpPath) { + await fs.promises.mkdir(path.dirname(path.resolve(options.envelopeDumpPath)), { recursive: true }); + } + await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => { const data: SentryRequestCallbackData = { envelope: parseEnvelope(proxyRequestBody), @@ -183,6 +189,10 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P listener(dataString); }); + if (options.envelopeDumpPath) { + fs.appendFileSync(path.resolve(options.envelopeDumpPath), `${JSON.stringify(data.envelope)}\n`, 'utf-8'); + } + return [ 200, '{}', diff --git a/scripts/normalize-e2e-test-dump-transaction-events.js b/scripts/normalize-e2e-test-dump-transaction-events.js new file mode 100644 index 000000000000..ba06a63fa020 --- /dev/null +++ b/scripts/normalize-e2e-test-dump-transaction-events.js @@ -0,0 +1,109 @@ +/* eslint-disable no-console */ + +const fs = require('fs'); +const path = require('path'); + +if (process.argv.length < 4) { + throw new Error('Please provide an input and output file path as an argument.'); +} + +const resolvedInputPath = path.resolve(process.argv[2]); +const resolvedOutputPath = path.resolve(process.argv[3]); + +const fileContents = fs.readFileSync(resolvedInputPath, 'utf8'); + +const transactionNodes = []; + +fileContents.split('\n').forEach(serializedEnvelope => { + let envelope; + try { + envelope = JSON.parse(serializedEnvelope); + } catch (e) { + return; + // noop + } + + const envelopeItems = envelope[1]; + + envelopeItems.forEach(([envelopeItemHeader, transaction]) => { + if (envelopeItemHeader.type === 'transaction') { + const rootNode = { + runtime: transaction.contexts.runtime?.name, + op: transaction.contexts.trace.op, + name: transaction.transaction, + children: [], + }; + + const spanMap = new Map(); + spanMap.set(transaction.contexts.trace.span_id, rootNode); + + transaction.spans.forEach(span => { + const node = { + op: span.data['sentry.op'], + name: span.description, + parent_span_id: span.parent_span_id, + children: [], + }; + spanMap.set(span.span_id, node); + }); + + transaction.spans.forEach(span => { + const node = spanMap.get(span.span_id); + if (node && node.parent_span_id) { + const parentNode = spanMap.get(node.parent_span_id); + parentNode.children.push(node); + } + }); + + transactionNodes.push(rootNode); + } + }); +}); + +const output = transactionNodes + .sort((a, b) => { + const aSerialized = serializeNode(a); + const bSerialized = serializeNode(b); + if (aSerialized < bSerialized) { + return -1; + } else if (aSerialized > bSerialized) { + return 1; + } else { + return 0; + } + }) + .map(node => buildDeterministicStringFromNode(node)) + .join('\n\n-----------------------\n\n'); + +fs.writeFileSync(resolvedOutputPath, output, 'utf-8'); + +// ------- utility fns ---------- + +function buildDeterministicStringFromNode(node, depth = 0) { + const mainParts = []; + if (node.runtime) { + mainParts.push(`(${node.runtime})`); + } + mainParts.push(`${node.op ?? 'default'} -`); + mainParts.push(node.name); + const main = mainParts.join(' '); + const children = node.children + .sort((a, b) => { + const aSerialized = serializeNode(a); + const bSerialized = serializeNode(b); + if (aSerialized < bSerialized) { + return -1; + } else if (aSerialized > bSerialized) { + return 1; + } else { + return 0; + } + }) + .map(child => '\n' + buildDeterministicStringFromNode(child, depth + 1)) + .join(''); + return `${main}${children}`.split('\n').join('\n '); +} + +function serializeNode(node) { + return [node.op, node.name, node.runtime].join('---'); +}