From cadcb75eba9f0dd3a56158b06fc1c675033fbe57 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 8 Jul 2025 20:03:25 +0200 Subject: [PATCH 01/11] test(cloudflare): Add integration tests --- .github/workflows/build.yml | 25 ++ .gitignore | 1 + .../cloudflare-integration-tests/.eslintrc.js | 35 +++ .../cloudflare-integration-tests/expect.ts | 78 ++++++ .../cloudflare-integration-tests/package.json | 27 ++ .../cloudflare-integration-tests/runner.ts | 249 ++++++++++++++++++ .../suites/basic/index.ts | 16 ++ .../suites/basic/test.ts | 32 +++ .../suites/basic/wrangler.jsonc | 6 + .../tsconfig.json | 12 + .../vite.config.mts | 31 +++ .../node-integration-tests/package.json | 1 + .../node-integration-tests/utils/runner.ts | 2 +- dev-packages/test-utils/package.json | 3 + dev-packages/test-utils/src/index.ts | 1 + .../utils => test-utils/src}/server.ts | 0 dev-packages/test-utils/tsconfig.json | 4 +- package.json | 1 + yarn.lock | 6 +- 19 files changed, 527 insertions(+), 3 deletions(-) create mode 100644 dev-packages/cloudflare-integration-tests/.eslintrc.js create mode 100644 dev-packages/cloudflare-integration-tests/expect.ts create mode 100644 dev-packages/cloudflare-integration-tests/package.json create mode 100644 dev-packages/cloudflare-integration-tests/runner.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/basic/index.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/basic/test.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc create mode 100644 dev-packages/cloudflare-integration-tests/tsconfig.json create mode 100644 dev-packages/cloudflare-integration-tests/vite.config.mts rename dev-packages/{node-integration-tests/utils => test-utils/src}/server.ts (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca8ebedfdd75..4d6e882e26b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -747,6 +747,30 @@ jobs: directory: dev-packages/node-integration-tests token: ${{ secrets.CODECOV_TOKEN }} + job_cloudflare_integration_tests: + name: Cloudflare Integration Tests + needs: [job_get_metadata, job_build] + if: needs.job_build.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v4 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + - name: Restore caches + uses: ./.github/actions/restore-cache + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + + - name: Run integration tests + working-directory: dev-packages/cloudflare-integration-tests + run: yarn test + job_remix_integration_tests: name: Remix (Node ${{ matrix.node }}) Tests needs: [job_get_metadata, job_build] @@ -1095,6 +1119,7 @@ jobs: job_deno_unit_tests, job_node_unit_tests, job_node_integration_tests, + job_cloudflare_integration_tests, job_browser_playwright_tests, job_browser_loader_tests, job_remix_integration_tests, diff --git a/.gitignore b/.gitignore index f784704ac31a..f381e7e6e24d 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ packages/gatsby/gatsby-node.d.ts # intellij *.iml +/**/.wrangler/* diff --git a/dev-packages/cloudflare-integration-tests/.eslintrc.js b/dev-packages/cloudflare-integration-tests/.eslintrc.js new file mode 100644 index 000000000000..899a60f9a2bd --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/.eslintrc.js @@ -0,0 +1,35 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['*.ts'], + parserOptions: { + project: ['tsconfig.json'], + sourceType: 'module', + }, + }, + { + files: ['suites/**/*.ts', 'suites/**/*.mjs'], + globals: { + fetch: 'readonly', + }, + rules: { + '@typescript-eslint/typedef': 'off', + // Explicitly allow ts-ignore with description for Node integration tests + // Reason: We run these tests on TS3.8 which doesn't support `@ts-expect-error` + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-ignore': 'allow-with-description', + 'ts-expect-error': true, + }, + ], + // We rely on having imports after init() is called for OTEL + 'import/first': 'off', + }, + }, + ], +}; diff --git a/dev-packages/cloudflare-integration-tests/expect.ts b/dev-packages/cloudflare-integration-tests/expect.ts new file mode 100644 index 000000000000..6050ff6816c4 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/expect.ts @@ -0,0 +1,78 @@ +import type { Contexts, Envelope, Event, SdkInfo } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; +import { expect } from 'vitest'; + +export const UUID_MATCHER = expect.stringMatching(/^[0-9a-f]{32}$/); +export const UUID_V4_MATCHER = expect.stringMatching( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, +); +export const SHORT_UUID_MATCHER = expect.stringMatching(/^[0-9a-f]{16}$/); +export const ISO_DATE_MATCHER = expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + +function dropUndefinedKeys>(obj: T): T { + for (const [key, value] of Object.entries(obj)) { + if (value === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete obj[key]; + } + } + return obj; +} + +function getSdk(): SdkInfo { + return { + integrations: expect.any(Array), + name: 'sentry.javascript.cloudflare', + packages: [ + { + name: 'npm:@sentry/cloudflare', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; +} + +function defaultContexts(eventContexts: Contexts = {}): Contexts { + return dropUndefinedKeys({ + trace: { + trace_id: UUID_MATCHER, + span_id: SHORT_UUID_MATCHER, + }, + cloud_resource: { 'cloud.provider': 'cloudflare' }, + culture: { timezone: expect.any(String) }, + runtime: { name: 'cloudflare' }, + ...eventContexts, + }); +} + +export function expectedEvent(event: Event): Event { + return dropUndefinedKeys({ + event_id: UUID_MATCHER, + timestamp: expect.any(Number), + environment: 'production', + platform: 'javascript', + sdk: getSdk(), + ...event, + contexts: defaultContexts(event.contexts), + }); +} + +export function eventEnvelope(event: Event): Envelope { + return [ + { + event_id: UUID_MATCHER, + sent_at: ISO_DATE_MATCHER, + sdk: { name: 'sentry.javascript.cloudflare', version: SDK_VERSION }, + trace: { + environment: event.environment || 'production', + public_key: 'public', + trace_id: UUID_MATCHER, + sample_rate: expect.any(String), + sampled: expect.any(String), + transaction: expect.any(String), + }, + }, + [[{ type: 'event' }, expectedEvent(event)]], + ]; +} diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json new file mode 100644 index 000000000000..4f5633ba9e0e --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -0,0 +1,27 @@ +{ + "name": "@sentry-internal/cloudflare-integration-tests", + "version": "9.35.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "private": true, + "scripts": { + "lint": "eslint . --format stylish", + "fix": "eslint . --format stylish --fix", + "test": "vitest run", + "test:watch": "yarn test --watch" + }, + "dependencies": { + "@sentry/cloudflare": "9.35.0" + }, + "devDependencies": { + "@sentry-internal/test-utils": "9.35.0", + "@cloudflare/workers-types": "^4.20250708.0", + "vitest": "^3.2.4", + "wrangler": "4.22.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/cloudflare-integration-tests/runner.ts b/dev-packages/cloudflare-integration-tests/runner.ts new file mode 100644 index 000000000000..34ed9498970f --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/runner.ts @@ -0,0 +1,249 @@ +import type { Envelope, EnvelopeItemType } from '@sentry/core'; +import { normalize } from '@sentry/core'; +import { createBasicSentryServer } from '@sentry-internal/test-utils'; +import { spawn } from 'child_process'; +import { existsSync } from 'fs'; +import { join } from 'path'; +import { inspect } from 'util'; +import { expect } from 'vitest'; + +/** Promise only resolves when fn returns true */ +async function waitFor(fn: () => boolean, timeout = 10_000, message = 'Timed out waiting'): Promise { + let remaining = timeout; + while (fn() === false) { + await new Promise(resolve => setTimeout(resolve, 100)); + remaining -= 100; + if (remaining < 0) { + throw new Error(message); + } + } +} + +type VoidFunction = () => void; + +type Expected = Envelope | ((envelope: Envelope) => void); + +type StartResult = { + completed(): Promise; + makeRequest( + method: 'get' | 'post', + path: string, + options?: { headers?: Record; data?: BodyInit; expectError?: boolean }, + ): Promise; +}; + +/** Creates a test runner */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createRunner(...paths: string[]) { + const testPath = join(...paths); + + if (!existsSync(testPath)) { + throw new Error(`Test scenario not found: ${testPath}`); + } + + const cleanupSteps = new Set(); + const expectedEnvelopes: Expected[] = []; + // By default, we ignore session & sessions + const ignored: Set = new Set(['session', 'sessions', 'client_report']); + + return { + expect: function (expected: Expected) { + expectedEnvelopes.push(expected); + return this; + }, + expectN: function (n: number, expected: Expected) { + for (let i = 0; i < n; i++) { + expectedEnvelopes.push(expected); + } + return this; + }, + ignore: function (...types: EnvelopeItemType[]) { + types.forEach(t => ignored.add(t)); + return this; + }, + unignore: function (...types: EnvelopeItemType[]) { + for (const t of types) { + ignored.delete(t); + } + return this; + }, + start: function (): StartResult { + let isComplete = false; + let completeError: Error | undefined; + + const expectedEnvelopeCount = expectedEnvelopes.length; + + let envelopeCount = 0; + let scenarioServerPort: number | undefined; + let child: ReturnType | undefined; + + function complete(error?: Error): void { + if (isComplete) { + return; + } + + isComplete = true; + completeError = error || undefined; + for (const step of cleanupSteps) { + step(); + } + } + + /** Called after each expect callback to check if we're complete */ + function expectCallbackCalled(): void { + envelopeCount++; + if (envelopeCount === expectedEnvelopeCount) { + complete(); + } + } + + function newEnvelope(envelope: Envelope): void { + if (process.env.DEBUG) log('newEnvelope', inspect(envelope, false, null, true)); + + const envelopeItemType = envelope[1][0][0].type; + + if (ignored.has(envelopeItemType)) { + return; + } + + const expected = expectedEnvelopes.shift(); + + // Catch any error or failed assertions and pass them to done to end the test quickly + try { + if (!expected) { + return; + } + + if (typeof expected === 'function') { + expected(envelope); + } else { + expect(envelope).toEqual(expected); + } + expectCallbackCalled(); + } catch (e) { + complete(e as Error); + } + } + + createBasicSentryServer(newEnvelope) + .then(([mockServerPort, mockServerClose]) => { + if (mockServerClose) { + cleanupSteps.add(() => { + mockServerClose(); + }); + } + + // const env = { ...process.env, ...withEnv, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` }; + + const SENTRY_DSN = `http://public@localhost:${mockServerPort}/1337`; + + if (process.env.DEBUG) log('starting scenario', { testPath, SENTRY_DSN }); + + const wranglerConfigPath = join(testPath, 'wrangler.jsonc'); + + child = spawn('wrangler', ['dev', '--config', wranglerConfigPath, '--var', `SENTRY_DSN:${SENTRY_DSN}`]); + + child.on('error', e => { + // eslint-disable-next-line no-console + console.error('Error starting child process:', e); + complete(e); + }); + + cleanupSteps.add(() => { + child?.kill(); + }); + + if (process.env.DEBUG) { + child.stderr?.on('data', (data: Buffer) => { + log('stderr line', data.toString()); + }); + } + + child.stdout?.on('data', (data: Buffer) => { + if (scenarioServerPort === undefined) { + const line = data.toString(); + const result = line.match(/Ready on http:\/\/localhost:(\d+)/); + if (result?.[1]) { + scenarioServerPort = parseInt(result[1], 10); + } + } + + if (process.env.DEBUG) { + log('stdout line', data.toString()); + } + }); + + // Pass error to done to end the test quickly + child.on('error', e => { + if (process.env.DEBUG) log('scenario error', e); + complete(e); + }); + }) + .catch(e => complete(e)); + + return { + completed: async function (): Promise { + await waitFor(() => isComplete, 120_000, 'Timed out waiting for test to complete'); + + if (completeError) { + throw completeError; + } + }, + makeRequest: async function ( + method: 'get' | 'post', + path: string, + options: { headers?: Record; data?: BodyInit; expectError?: boolean } = {}, + ): Promise { + try { + await waitFor(() => scenarioServerPort !== undefined, 10_000, 'Timed out waiting for server port'); + } catch (e) { + complete(e as Error); + return; + } + + const url = `http://localhost:${scenarioServerPort}${path}`; + const body = options.data; + const headers = options.headers || {}; + const expectError = options.expectError || false; + + if (process.env.DEBUG) log('making request', method, url, headers, body); + + try { + const res = await fetch(url, { headers, method, body }); + + if (!res.ok) { + if (!expectError) { + complete(new Error(`Expected request to "${path}" to succeed, but got a ${res.status} response`)); + } + + return; + } + + if (expectError) { + complete(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`)); + return; + } + + if (res.headers.get('content-type')?.includes('application/json')) { + return await res.json(); + } + + return (await res.text()) as T; + } catch (e) { + if (expectError) { + return; + } + + complete(e as Error); + return; + } + }, + }; + }, + }; +} + +function log(...args: unknown[]): void { + // eslint-disable-next-line no-console + console.log(...args.map(arg => normalize(arg))); +} diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/index.ts b/dev-packages/cloudflare-integration-tests/suites/basic/index.ts new file mode 100644 index 000000000000..08c497cf1172 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/basic/index.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/cloudflare'; + +interface Env { + SENTRY_DSN: string; +} + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + }), + { + async fetch(_request, _env, _ctx) { + throw new Error('This is a test error from the Cloudflare integration tests'); + }, + }, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/test.ts b/dev-packages/cloudflare-integration-tests/suites/basic/test.ts new file mode 100644 index 000000000000..1bd7b4bac094 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/basic/test.ts @@ -0,0 +1,32 @@ +import { expect, it } from 'vitest'; +import { eventEnvelope } from '../../expect'; +import { createRunner } from '../../runner'; + +it('Basic error in fetch handler', async () => { + const runner = createRunner(__dirname) + .expect( + eventEnvelope({ + level: 'error', + exception: { + values: [ + { + type: 'Error', + value: 'This is a test error from the Cloudflare integration tests', + stacktrace: { + frames: expect.any(Array), + }, + mechanism: { type: 'cloudflare', handled: false }, + }, + ], + }, + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.any(String), + }, + }), + ) + .start(); + await runner.makeRequest('get', '/', { expectError: true }); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc new file mode 100644 index 000000000000..24fb2861023d --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "worker-name", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_compat"] +} diff --git a/dev-packages/cloudflare-integration-tests/tsconfig.json b/dev-packages/cloudflare-integration-tests/tsconfig.json new file mode 100644 index 000000000000..38816b36116e --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["suites/**/*.ts", "*.ts"], + + "compilerOptions": { + // Although this seems wrong to include `DOM` here, it's necessary to make + // global fetch available in tests in lower Node versions. + "lib": ["ES2020"], + "esModuleInterop": true, + } +} diff --git a/dev-packages/cloudflare-integration-tests/vite.config.mts b/dev-packages/cloudflare-integration-tests/vite.config.mts new file mode 100644 index 000000000000..ead6315065a3 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/vite.config.mts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vitest/config'; +import baseConfig from '../../vite/vite.config'; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + coverage: { + enabled: false, + }, + isolate: false, + include: ['./suites/**/test.ts'], + testTimeout: 15000, + // Ensure we can see debug output when DEBUG=true + ...(process.env.DEBUG + ? { + disableConsoleIntercept: true, + silent: false, + } + : {}), + // By default Vitest uses child processes to run tests but all our tests + // already run in their own processes. We use threads instead because the + // overhead is significantly less. + pool: 'threads', + reporters: process.env.DEBUG + ? ['default', { summary: false }] + : process.env.GITHUB_ACTIONS + ? ['dot', 'github-actions'] + : ['verbose'], + }, +}); diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 9d6ff6d10bfb..a5c3f1c0da6a 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -71,6 +71,7 @@ "yargs": "^16.2.0" }, "devDependencies": { + "@sentry-internal/test-utils": "9.35.0", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", "@types/node-schedule": "^2.1.7", diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 1006d71bf3f0..c13f8ee17c26 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -12,6 +12,7 @@ import type { TransactionEvent, } from '@sentry/core'; import { normalize } from '@sentry/core'; +import { createBasicSentryServer } from '@sentry-internal/test-utils'; import { execSync, spawn, spawnSync } from 'child_process'; import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import { join } from 'path'; @@ -27,7 +28,6 @@ import { assertSentrySessions, assertSentryTransaction, } from './assertions'; -import { createBasicSentryServer } from './server'; const CLEANUP_STEPS = new Set(); diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 0842cb9060b9..cfda73aaac1c 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -43,6 +43,9 @@ "peerDependencies": { "@playwright/test": "~1.50.0" }, + "dependencies": { + "express": "^4.21.1" + }, "devDependencies": { "@playwright/test": "~1.50.0", "@sentry/core": "9.35.0" diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index 6cfbe61d9306..e9ae76f592ed 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -10,3 +10,4 @@ export { } from './event-proxy-server'; export { getPlaywrightConfig } from './playwright-config'; +export { createBasicSentryServer } from './server'; diff --git a/dev-packages/node-integration-tests/utils/server.ts b/dev-packages/test-utils/src/server.ts similarity index 100% rename from dev-packages/node-integration-tests/utils/server.ts rename to dev-packages/test-utils/src/server.ts diff --git a/dev-packages/test-utils/tsconfig.json b/dev-packages/test-utils/tsconfig.json index 7b0fa87fc45b..43f50e435628 100644 --- a/dev-packages/test-utils/tsconfig.json +++ b/dev-packages/test-utils/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "target": "ES2022", - "module": "ES2022" + "module": "ES2022", + // package-specific options + "esModuleInterop": true, }, "include": ["src/**/*.ts"] } diff --git a/package.json b/package.json index d6b8178ecc71..1d061239bffa 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "dev-packages/bundle-analyzer-scenarios", "dev-packages/e2e-tests", "dev-packages/node-integration-tests", + "dev-packages/cloudflare-integration-tests", "dev-packages/test-utils", "dev-packages/size-limit-gh-action", "dev-packages/clear-cache-gh-action", diff --git a/yarn.lock b/yarn.lock index f461fa573525..b02b707b5fdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2688,6 +2688,11 @@ resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250620.0.tgz#a22e635a631212963b84e315191614b20c4ad317" integrity sha512-EVvRB/DJEm6jhdKg+A4Qm4y/ry1cIvylSgSO3/f/Bv161vldDRxaXM2YoQQWFhLOJOw0qtrHsKOD51KYxV1XCw== +"@cloudflare/workers-types@^4.20250708.0": + version "4.20250708.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250708.0.tgz#80b7087a6bd475ec4b29591be310913f2d76caf9" + integrity sha512-jEIHy5lPtGFumnRWNR4tE9lZvR+E+DRGvSKe/C32uAlk9n7fjGfyCGN1QSCn7MTEZjXLWTck3K0fsmOjt6VWCQ== + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -27468,7 +27473,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" From 63013ad1eb7716cb4aff6978e9d55dbf61a3daf3 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 8 Jul 2025 20:55:47 +0200 Subject: [PATCH 02/11] Don't run with the unit tests... --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1d061239bffa..cce14e6080f7 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,10 @@ "dedupe-deps:check": "yarn-deduplicate yarn.lock --list --fail", "dedupe-deps:fix": "yarn-deduplicate yarn.lock", "postpublish": "lerna run --stream --concurrency 1 postpublish", - "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test", - "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test:unit", + "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,cloudflare-integration-tests}\" test", + "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,cloudflare-integration-tests}\" test:unit", "test:update-snapshots": "lerna run test:update-snapshots", - "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\"", + "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,cloudflare-integration-tests}\"", "test:pr:browser": "UNIT_TEST_ENV=browser ts-node ./scripts/ci-unit-tests.ts --affected", "test:pr:node": "UNIT_TEST_ENV=node ts-node ./scripts/ci-unit-tests.ts --affected", "test:ci:browser": "UNIT_TEST_ENV=browser ts-node ./scripts/ci-unit-tests.ts", From 581d0a43c7eb80c2bdbe66516c6ee025034eb0f7 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 9 Jul 2025 12:43:28 +0200 Subject: [PATCH 03/11] Fix node integration tests --- .../node-integration-tests/utils/server.ts | 48 +++++++++++++++++++ dev-packages/test-utils/src/server.ts | 46 ------------------ 2 files changed, 48 insertions(+), 46 deletions(-) create mode 100644 dev-packages/node-integration-tests/utils/server.ts diff --git a/dev-packages/node-integration-tests/utils/server.ts b/dev-packages/node-integration-tests/utils/server.ts new file mode 100644 index 000000000000..a1ba3f522fb1 --- /dev/null +++ b/dev-packages/node-integration-tests/utils/server.ts @@ -0,0 +1,48 @@ +import express from 'express'; +import type { AddressInfo } from 'net'; + +type HeaderAssertCallback = (headers: Record) => void; + +/** Creates a test server that can be used to check headers */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createTestServer() { + const gets: Array<[string, HeaderAssertCallback, number]> = []; + let error: unknown | undefined; + + return { + get: function (path: string, callback: HeaderAssertCallback, result = 200) { + gets.push([path, callback, result]); + return this; + }, + start: async (): Promise<[string, () => void]> => { + const app = express(); + + for (const [path, callback, result] of gets) { + app.get(path, (req, res) => { + try { + callback(req.headers); + } catch (e) { + error = e; + } + + res.status(result).send(); + }); + } + + return new Promise(resolve => { + const server = app.listen(0, () => { + const address = server.address() as AddressInfo; + resolve([ + `http://localhost:${address.port}`, + () => { + server.close(); + if (error) { + throw error; + } + }, + ]); + }); + }); + }, + }; +} diff --git a/dev-packages/test-utils/src/server.ts b/dev-packages/test-utils/src/server.ts index 92e0477c845c..b8941b4b0c32 100644 --- a/dev-packages/test-utils/src/server.ts +++ b/dev-packages/test-utils/src/server.ts @@ -37,49 +37,3 @@ export function createBasicSentryServer(onEnvelope: (env: Envelope) => void): Pr }); }); } - -type HeaderAssertCallback = (headers: Record) => void; - -/** Creates a test server that can be used to check headers */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function createTestServer() { - const gets: Array<[string, HeaderAssertCallback, number]> = []; - let error: unknown | undefined; - - return { - get: function (path: string, callback: HeaderAssertCallback, result = 200) { - gets.push([path, callback, result]); - return this; - }, - start: async (): Promise<[string, () => void]> => { - const app = express(); - - for (const [path, callback, result] of gets) { - app.get(path, (req, res) => { - try { - callback(req.headers); - } catch (e) { - error = e; - } - - res.status(result).send(); - }); - } - - return new Promise(resolve => { - const server = app.listen(0, () => { - const address = server.address() as AddressInfo; - resolve([ - `http://localhost:${address.port}`, - () => { - server.close(); - if (error) { - throw error; - } - }, - ]); - }); - }); - }, - }; -} From 072e6e1116493e03b858bb505aaa50256731e4d5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 9 Jul 2025 15:23:43 +0200 Subject: [PATCH 04/11] Improve test runner --- .../cloudflare-integration-tests/runner.ts | 156 ++++++++---------- .../vite.config.mts | 2 +- 2 files changed, 73 insertions(+), 85 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/runner.ts b/dev-packages/cloudflare-integration-tests/runner.ts index 34ed9498970f..a3f0beed2f4f 100644 --- a/dev-packages/cloudflare-integration-tests/runner.ts +++ b/dev-packages/cloudflare-integration-tests/runner.ts @@ -7,19 +7,41 @@ import { join } from 'path'; import { inspect } from 'util'; import { expect } from 'vitest'; -/** Promise only resolves when fn returns true */ -async function waitFor(fn: () => boolean, timeout = 10_000, message = 'Timed out waiting'): Promise { - let remaining = timeout; - while (fn() === false) { - await new Promise(resolve => setTimeout(resolve, 100)); - remaining -= 100; - if (remaining < 0) { - throw new Error(message); - } +const CLEANUP_STEPS = new Set<() => void>(); + +export function cleanupChildProcesses(): void { + for (const step of CLEANUP_STEPS) { + step(); } + CLEANUP_STEPS.clear(); } -type VoidFunction = () => void; +process.on('exit', cleanupChildProcesses); + +function deferredPromise( + done?: () => void, +): { resolve: (val: T) => void; reject: (reason?: unknown) => void; promise: Promise } { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = (val: T) => { + done?.(); + res(val); + }; + reject = (reason: Error) => { + done?.(); + rej(reason); + }; + }); + if (!resolve || !reject) { + throw new Error('Failed to create deferred promise'); + } + return { + resolve, + reject, + promise, + }; +} type Expected = Envelope | ((envelope: Envelope) => void); @@ -41,7 +63,6 @@ export function createRunner(...paths: string[]) { throw new Error(`Test scenario not found: ${testPath}`); } - const cleanupSteps = new Set(); const expectedEnvelopes: Expected[] = []; // By default, we ignore session & sessions const ignored: Set = new Set(['session', 'sessions', 'client_report']); @@ -68,32 +89,18 @@ export function createRunner(...paths: string[]) { return this; }, start: function (): StartResult { - let isComplete = false; - let completeError: Error | undefined; - + const { resolve, reject, promise: isComplete } = deferredPromise(cleanupChildProcesses); const expectedEnvelopeCount = expectedEnvelopes.length; let envelopeCount = 0; - let scenarioServerPort: number | undefined; + const { resolve: setWorkerPort, promise: workerPortPromise } = deferredPromise(); let child: ReturnType | undefined; - function complete(error?: Error): void { - if (isComplete) { - return; - } - - isComplete = true; - completeError = error || undefined; - for (const step of cleanupSteps) { - step(); - } - } - /** Called after each expect callback to check if we're complete */ function expectCallbackCalled(): void { envelopeCount++; if (envelopeCount === expectedEnvelopeCount) { - complete(); + resolve(); } } @@ -121,87 +128,68 @@ export function createRunner(...paths: string[]) { } expectCallbackCalled(); } catch (e) { - complete(e as Error); + reject(e); } } createBasicSentryServer(newEnvelope) .then(([mockServerPort, mockServerClose]) => { if (mockServerClose) { - cleanupSteps.add(() => { + CLEANUP_STEPS.add(() => { mockServerClose(); }); } - // const env = { ...process.env, ...withEnv, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` }; - - const SENTRY_DSN = `http://public@localhost:${mockServerPort}/1337`; - - if (process.env.DEBUG) log('starting scenario', { testPath, SENTRY_DSN }); - - const wranglerConfigPath = join(testPath, 'wrangler.jsonc'); - - child = spawn('wrangler', ['dev', '--config', wranglerConfigPath, '--var', `SENTRY_DSN:${SENTRY_DSN}`]); + if (process.env.DEBUG) log('Starting scenario', testPath); + + const stdio: ('inherit' | 'ipc' | 'ignore')[] = process.env.DEBUG + ? ['inherit', 'inherit', 'inherit', 'ipc'] + : ['ignore', 'ignore', 'ignore', 'ipc']; + + child = spawn( + 'wrangler', + [ + 'dev', + '--config', + join(testPath, 'wrangler.jsonc'), + '--show-interactive-dev-session', + 'false', + '--var', + `SENTRY_DSN:http://public@localhost:${mockServerPort}/1337`, + ], + { stdio }, + ); + + CLEANUP_STEPS.add(() => { + child?.kill(); + }); child.on('error', e => { // eslint-disable-next-line no-console console.error('Error starting child process:', e); - complete(e); + reject(e); }); - cleanupSteps.add(() => { - child?.kill(); - }); - - if (process.env.DEBUG) { - child.stderr?.on('data', (data: Buffer) => { - log('stderr line', data.toString()); - }); - } - - child.stdout?.on('data', (data: Buffer) => { - if (scenarioServerPort === undefined) { - const line = data.toString(); - const result = line.match(/Ready on http:\/\/localhost:(\d+)/); - if (result?.[1]) { - scenarioServerPort = parseInt(result[1], 10); - } - } - - if (process.env.DEBUG) { - log('stdout line', data.toString()); + child.on('message', (message: string) => { + const msg = JSON.parse(message) as { event: string; port?: number }; + if (msg.event === 'DEV_SERVER_READY' && typeof msg.port === 'number') { + setWorkerPort(msg.port); + if (process.env.DEBUG) log('worker ready on port', msg.port); } }); - - // Pass error to done to end the test quickly - child.on('error', e => { - if (process.env.DEBUG) log('scenario error', e); - complete(e); - }); }) - .catch(e => complete(e)); + .catch(e => reject(e)); return { completed: async function (): Promise { - await waitFor(() => isComplete, 120_000, 'Timed out waiting for test to complete'); - - if (completeError) { - throw completeError; - } + return isComplete; }, makeRequest: async function ( method: 'get' | 'post', path: string, options: { headers?: Record; data?: BodyInit; expectError?: boolean } = {}, ): Promise { - try { - await waitFor(() => scenarioServerPort !== undefined, 10_000, 'Timed out waiting for server port'); - } catch (e) { - complete(e as Error); - return; - } - - const url = `http://localhost:${scenarioServerPort}${path}`; + const url = `http://localhost:${await workerPortPromise}${path}`; const body = options.data; const headers = options.headers || {}; const expectError = options.expectError || false; @@ -213,14 +201,14 @@ export function createRunner(...paths: string[]) { if (!res.ok) { if (!expectError) { - complete(new Error(`Expected request to "${path}" to succeed, but got a ${res.status} response`)); + reject(new Error(`Expected request to "${path}" to succeed, but got a ${res.status} response`)); } return; } if (expectError) { - complete(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`)); + reject(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`)); return; } @@ -234,7 +222,7 @@ export function createRunner(...paths: string[]) { return; } - complete(e as Error); + reject(e); return; } }, diff --git a/dev-packages/cloudflare-integration-tests/vite.config.mts b/dev-packages/cloudflare-integration-tests/vite.config.mts index ead6315065a3..cfa15b12c3f1 100644 --- a/dev-packages/cloudflare-integration-tests/vite.config.mts +++ b/dev-packages/cloudflare-integration-tests/vite.config.mts @@ -10,7 +10,7 @@ export default defineConfig({ }, isolate: false, include: ['./suites/**/test.ts'], - testTimeout: 15000, + testTimeout: 20_000, // Ensure we can see debug output when DEBUG=true ...(process.env.DEBUG ? { From 32ee76fb4b1c45638e6379159c86a5d92487cfa5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 9 Jul 2025 15:28:01 +0200 Subject: [PATCH 05/11] Run it all the time --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d6e882e26b6..ba50d8edc855 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -750,7 +750,6 @@ jobs: job_cloudflare_integration_tests: name: Cloudflare Integration Tests needs: [job_get_metadata, job_build] - if: needs.job_build.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-24.04 timeout-minutes: 15 steps: From f763100e1ebf8655c1e038a30befd49e59210377 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 9 Jul 2025 15:29:50 +0200 Subject: [PATCH 06/11] Fix the versions from the merge --- dev-packages/cloudflare-integration-tests/package.json | 4 ++-- dev-packages/node-integration-tests/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 4f5633ba9e0e..e18560305d09 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -13,10 +13,10 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@sentry/cloudflare": "9.35.0" + "@sentry/cloudflare": "9.36.0" }, "devDependencies": { - "@sentry-internal/test-utils": "9.35.0", + "@sentry-internal/test-utils": "9.36.0", "@cloudflare/workers-types": "^4.20250708.0", "vitest": "^3.2.4", "wrangler": "4.22.0" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 75b685d118f3..0c4ac2cc3110 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -71,7 +71,7 @@ "yargs": "^16.2.0" }, "devDependencies": { - "@sentry-internal/test-utils": "9.35.0", + "@sentry-internal/test-utils": "9.36.0", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", "@types/node-schedule": "^2.1.7", From adbac7914a1dc40b885329a33d93fc3a4c36c1a7 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 9 Jul 2025 19:05:25 +0200 Subject: [PATCH 07/11] Fix versions --- dev-packages/cloudflare-integration-tests/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index e18560305d09..3f8a38767596 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/cloudflare-integration-tests", - "version": "9.35.0", + "version": "9.36.0", "license": "MIT", "engines": { "node": ">=18" From 6bc5e1e246cd77f1898f9268b9798697dc50e571 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 9 Jul 2025 21:51:13 +0200 Subject: [PATCH 08/11] PR review Co-authored-by: 0xbad0c0d3 <0xbad0c0d3@gmail.com> --- dev-packages/cloudflare-integration-tests/runner.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/runner.ts b/dev-packages/cloudflare-integration-tests/runner.ts index a3f0beed2f4f..849b011250f9 100644 --- a/dev-packages/cloudflare-integration-tests/runner.ts +++ b/dev-packages/cloudflare-integration-tests/runner.ts @@ -25,11 +25,9 @@ function deferredPromise( let reject; const promise = new Promise((res, rej) => { resolve = (val: T) => { - done?.(); res(val); }; reject = (reason: Error) => { - done?.(); rej(reason); }; }); @@ -39,7 +37,7 @@ function deferredPromise( return { resolve, reject, - promise, + promise: promise.finally(() => done?.()), }; } From a79ba2814ddeab609f56dc5ba7e317302142e58c Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 15 Jul 2025 11:13:06 +0200 Subject: [PATCH 09/11] Fix versions after bad merge --- dev-packages/cloudflare-integration-tests/package.json | 6 +++--- dev-packages/node-integration-tests/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 3f8a38767596..4c28622b6a4d 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/cloudflare-integration-tests", - "version": "9.36.0", + "version": "9.38.0", "license": "MIT", "engines": { "node": ">=18" @@ -13,10 +13,10 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@sentry/cloudflare": "9.36.0" + "@sentry/cloudflare": "9.38.0" }, "devDependencies": { - "@sentry-internal/test-utils": "9.36.0", + "@sentry-internal/test-utils": "9.38.0", "@cloudflare/workers-types": "^4.20250708.0", "vitest": "^3.2.4", "wrangler": "4.22.0" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 93d27274f525..b3f7a19a1d16 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -71,7 +71,7 @@ "yargs": "^16.2.0" }, "devDependencies": { - "@sentry-internal/test-utils": "9.36.0", + "@sentry-internal/test-utils": "9.38.0", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", "@types/node-schedule": "^2.1.7", From a2b082d91115a0224a800011076384a17883a4f0 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 22 Jul 2025 11:55:11 +0200 Subject: [PATCH 10/11] Use link --- dev-packages/cloudflare-integration-tests/package.json | 2 +- dev-packages/node-integration-tests/package.json | 2 +- yarn.lock | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 9f9e385378b8..36dc8101a8c0 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -16,7 +16,7 @@ "@sentry/cloudflare": "9.40.0" }, "devDependencies": { - "@sentry-internal/test-utils": "9.40.0", + "@sentry-internal/test-utils": "link:../../../test-utils", "@cloudflare/workers-types": "^4.20250708.0", "vitest": "^3.2.4", "wrangler": "4.22.0" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 0042a6c24b5b..2717beb074ea 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -71,7 +71,7 @@ "yargs": "^16.2.0" }, "devDependencies": { - "@sentry-internal/test-utils": "9.40.0", + "@sentry-internal/test-utils": "link:../../../test-utils", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", "@types/node-schedule": "^2.1.7", diff --git a/yarn.lock b/yarn.lock index 7d54d9317e06..5e3cfd1a2e97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6923,6 +6923,10 @@ fflate "^0.4.4" mitt "^3.0.0" +"@sentry-internal/test-utils@link:../test-utils": + version "0.0.0" + uid "" + "@sentry/babel-plugin-component-annotate@2.22.6": version "2.22.6" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.6.tgz#829d6caf2c95c1c46108336de4e1049e6521435e" From 55ecbc1807ab92d0ee883c937ffc24c97191893f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 22 Jul 2025 12:22:33 +0200 Subject: [PATCH 11/11] Fix paths --- dev-packages/cloudflare-integration-tests/package.json | 2 +- dev-packages/node-integration-tests/package.json | 2 +- yarn.lock | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 36dc8101a8c0..a974d4c9d8ef 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -16,7 +16,7 @@ "@sentry/cloudflare": "9.40.0" }, "devDependencies": { - "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry-internal/test-utils": "link:../test-utils", "@cloudflare/workers-types": "^4.20250708.0", "vitest": "^3.2.4", "wrangler": "4.22.0" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 2717beb074ea..a225fe3da6f3 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -71,7 +71,7 @@ "yargs": "^16.2.0" }, "devDependencies": { - "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry-internal/test-utils": "link:../test-utils", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", "@types/node-schedule": "^2.1.7", diff --git a/yarn.lock b/yarn.lock index 5e3cfd1a2e97..548ae99ab6d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6923,9 +6923,10 @@ fflate "^0.4.4" mitt "^3.0.0" -"@sentry-internal/test-utils@link:../test-utils": - version "0.0.0" - uid "" +"@sentry-internal/test-utils@link:dev-packages/test-utils": + version "9.40.0" + dependencies: + express "^4.21.1" "@sentry/babel-plugin-component-annotate@2.22.6": version "2.22.6"