From 146254e575ba2335c96674c4bcacbcf8a2462866 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 8 Oct 2026 15:15:32 +0200 Subject: [PATCH] test(node-integration): Replace `conditionalTest` with a `supports` helper `supports(requirements)` returns whether the Node version and the runtime that runs the scenarios meet the requirements of the tests, for use in `test.runIf` and `describe.runIf`. It replaces `conditionalTest` and the `RUNTIME` checks in `skipIf`. `skipRuntimes` becomes `runtimes`, the runtimes that run the tests. Co-Authored-By: Claude Opus 5.5 --- .../node-suites/excludes.ts | 3 +- .../node-suites/excludes.ts | 3 +- dev-packages/node-integration-tests/README.md | 4 +- .../suites/express/client-abort/test.ts | 45 +++++---- .../suites/public-api/LocalVariables/test.ts | 96 +++++++++++-------- .../public-api/OnUncaughtException/test.ts | 4 +- .../onUnhandledRejectionIntegration/test.ts | 53 +++++----- .../suites/tracing/flue/test.ts | 6 +- .../graphql-tracing-channel/resolvers/test.ts | 6 +- .../tracing/graphql-tracing-channel/test.ts | 6 +- .../useOperationNameForRootSpan/test.ts | 6 +- .../suites/tracing/httpIntegration/test.ts | 4 +- .../continued-trace-segment/test.ts | 7 +- .../suites/tracing/mastra/test.ts | 6 +- .../suites/tracing/neon/test.ts | 4 +- .../no-parent-span-client-report/test.ts | 4 +- .../suites/tracing/openai/v7/test.ts | 6 +- .../suites/tracing/pi-durable/test.ts | 8 +- .../suites/tracing/postgres/test.ts | 4 +- .../suites/tracing/postgresjs/test.ts | 10 +- .../suites/tracing/prisma-orm-v8/test.ts | 9 +- .../tracing/requests/traceparent/test.ts | 47 ++++----- .../suites/tracing/tedious/test.ts | 6 +- .../node-integration-tests/utils/index.ts | 41 ++++---- 24 files changed, 207 insertions(+), 181 deletions(-) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index a883dbf49870..86aa8f9a65e1 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -1,5 +1,6 @@ // Node suites that do not run on Bun, relative to `node-integration-tests`. A single test that -// fails on Bun is skipped with `test.skipIf` on `RUNTIME` in the Node suite, not listed here. +// fails on Bun runs only on the other runtimes, with `test.runIf(supports({ runtimes }))` in the +// Node suite, and is not listed here. // Node-only features: ANR and native thread watchdogs, child processes, the AWS Lambda Node runtime, // `node:sqlite`, which `flue` needs, and the Vercel keep-alive, which needs `http.server.response.finish`. diff --git a/dev-packages/deno-integration-tests/node-suites/excludes.ts b/dev-packages/deno-integration-tests/node-suites/excludes.ts index 14205893d332..b2a6b1b851a8 100644 --- a/dev-packages/deno-integration-tests/node-suites/excludes.ts +++ b/dev-packages/deno-integration-tests/node-suites/excludes.ts @@ -1,5 +1,6 @@ // Node suites that do not run on Deno, relative to `node-integration-tests`. A single test that -// fails on Deno is skipped with `test.skipIf` on `RUNTIME` in the Node suite, not listed here. +// fails on Deno runs only on the other runtimes, with `test.runIf(supports({ runtimes }))` in the +// Node suite, and is not listed here. // Node-only features: ANR and native thread watchdogs, child process and worker thread breadcrumbs. const NODE_ONLY = ['suites/anr/test.ts', 'suites/breadcrumbs/**', 'suites/thread-blocked-native/test.ts']; diff --git a/dev-packages/node-integration-tests/README.md b/dev-packages/node-integration-tests/README.md index ac35f0ea54c0..67272bf5f6b0 100644 --- a/dev-packages/node-integration-tests/README.md +++ b/dev-packages/node-integration-tests/README.md @@ -67,8 +67,8 @@ arriving after its condition is satisfied. `dev-packages/bun-integration-tests` and `dev-packages/deno-integration-tests` run every suite of this package on Bun and Deno as well. When you add a suite, check that it passes on both. If it cannot, add it to -`node-suites/excludes.ts` of the runtime's package with the reason, or skip the single tests that fail with -`test.skipIf(RUNTIME === 'bun')` or `test.skipIf(RUNTIME === 'deno')`. +`node-suites/excludes.ts` of the runtime's package with the reason, or run the single tests that fail only on the +other runtimes, for example `test.runIf(supports({ runtimes: ['node', 'deno'] }))` for a test that fails on Bun. ## Running Tests Locally diff --git a/dev-packages/node-integration-tests/suites/express/client-abort/test.ts b/dev-packages/node-integration-tests/suites/express/client-abort/test.ts index b9cac9af42d8..5206399712ac 100644 --- a/dev-packages/node-integration-tests/suites/express/client-abort/test.ts +++ b/dev-packages/node-integration-tests/suites/express/client-abort/test.ts @@ -1,5 +1,5 @@ import { afterAll, describe, expect } from 'vitest'; -import { RUNTIME } from '../../../utils'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; describe('express client abort', () => { @@ -10,27 +10,30 @@ describe('express client abort', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { // Bun 1.3.14 emits no `close` on the response when the client aborts, so the `http.server` // span never ends either. Bun 1.4.0 emits it. - test.skipIf(RUNTIME === 'bun')('ends the request handler span when the client aborts', async () => { - const runner = createRunner() - .unordered() - .expect({ - span: container => { - const segmentSpan = container.items.find(item => item.is_segment); - const handlerSpan = container.items.find( - item => item.attributes['express.type']?.value === 'request_handler', - ); + test.runIf(supports({ runtimes: ['node', 'deno'] }))( + 'ends the request handler span when the client aborts', + async () => { + const runner = createRunner() + .unordered() + .expect({ + span: container => { + const segmentSpan = container.items.find(item => item.is_segment); + const handlerSpan = container.items.find( + item => item.attributes['express.type']?.value === 'request_handler', + ); - expect(segmentSpan?.name).toBe('GET /test/slow'); - expect(handlerSpan).toBeDefined(); - expect(handlerSpan!.parent_span_id).toBe(segmentSpan!.span_id); - // Ends when the client aborts, not when the handler's 500ms of work finishes. - expect(handlerSpan!.end_timestamp - handlerSpan!.start_timestamp).toBeLessThan(0.4); - }, - }) - .start(); + expect(segmentSpan?.name).toBe('GET /test/slow'); + expect(handlerSpan).toBeDefined(); + expect(handlerSpan!.parent_span_id).toBe(segmentSpan!.span_id); + // Ends when the client aborts, not when the handler's 500ms of work finishes. + expect(handlerSpan!.end_timestamp - handlerSpan!.start_timestamp).toBeLessThan(0.4); + }, + }) + .start(); - await runner.makeRequest('get', '/test/slow', { timeout: 100, expectError: true }); - await runner.completed(); - }); + await runner.makeRequest('get', '/test/slow', { timeout: 100, expectError: true }); + await runner.completed(); + }, + ); }); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 8873d746159a..a078bfed26a2 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -2,7 +2,7 @@ import { mkdirSync, rmdirSync, unlinkSync, writeFileSync } from 'fs'; import * as path from 'path'; import { afterAll, beforeAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -import { RUNTIME } from '../../../utils'; +import { supports } from '../../../utils'; const EXPECTED_LOCAL_VARIABLES_EVENT = { exception: { @@ -81,24 +81,27 @@ module.exports = { out_of_app_function };`, }); // Bun and Deno: the error events have no local variables. - test.skipIf(RUNTIME !== 'node')('Should include local variables when enabled', async () => { + test.runIf(supports({ runtimes: ['node'] }))('Should include local variables when enabled', async () => { await createRunner(__dirname, 'local-variables.js') .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) .start() .completed(); }); - test.skipIf(RUNTIME !== 'node')('Should include local variables when instrumenting via --import', async () => { - const instrumentPath = path.resolve(__dirname, 'local-variables-instrument.cjs'); + test.runIf(supports({ runtimes: ['node'] }))( + 'Should include local variables when instrumenting via --import', + async () => { + const instrumentPath = path.resolve(__dirname, 'local-variables-instrument.cjs'); - await createRunner(__dirname, 'local-variables-no-sentry.js') - .withFlags(`--import=${instrumentPath}`) - .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) - .start() - .completed(); - }); + await createRunner(__dirname, 'local-variables-no-sentry.js') + .withFlags(`--import=${instrumentPath}`) + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start() + .completed(); + }, + ); - test.skipIf(RUNTIME !== 'node')('Should include local variables with ESM', async () => { + test.runIf(supports({ runtimes: ['node'] }))('Should include local variables with ESM', async () => { await createRunner(__dirname, 'local-variables-caught.mjs') .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) .start() @@ -109,21 +112,27 @@ module.exports = { out_of_app_function };`, await createRunner(__dirname, 'deny-inspector.mjs').ensureNoErrorOutput().start().completed(); }); - test.skipIf(RUNTIME !== 'node')('Should retain original local variables when error is re-thrown', async () => { - await createRunner(__dirname, 'local-variables-rethrow.js') - .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) - .start() - .completed(); - }); + test.runIf(supports({ runtimes: ['node'] }))( + 'Should retain original local variables when error is re-thrown', + async () => { + await createRunner(__dirname, 'local-variables-rethrow.js') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start() + .completed(); + }, + ); - test.skipIf(RUNTIME !== 'node')('Includes local variables for caught exceptions when enabled', async () => { - await createRunner(__dirname, 'local-variables-caught.js') - .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) - .start() - .completed(); - }); + test.runIf(supports({ runtimes: ['node'] }))( + 'Includes local variables for caught exceptions when enabled', + async () => { + await createRunner(__dirname, 'local-variables-caught.js') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start() + .completed(); + }, + ); - test.skipIf(RUNTIME !== 'node')( + test.runIf(supports({ runtimes: ['node'] }))( 'Filters local variables by name via dataCollection.stackFrameVariables', async () => { await createRunner(__dirname, 'local-variables-filtered.js') @@ -156,7 +165,7 @@ module.exports = { out_of_app_function };`, .completed(); }); - test.skipIf(RUNTIME !== 'node')('Should handle different function name formats', async () => { + test.runIf(supports({ runtimes: ['node'] }))('Should handle different function name formats', async () => { await createRunner(__dirname, 'local-variables-name-matching.js') .expect({ event: { @@ -182,7 +191,7 @@ module.exports = { out_of_app_function };`, .completed(); }); - test.skipIf(RUNTIME !== 'node')( + test.runIf(supports({ runtimes: ['node'] }))( 'adds local variables to out of app frames when includeOutOfAppFrames is true', async () => { await createRunner(__dirname, 'local-variables-out-of-app.js') @@ -208,23 +217,26 @@ module.exports = { out_of_app_function };`, }, ); - test.skipIf(RUNTIME !== 'node')('does not add local variables to out of app frames by default', async () => { - await createRunner(__dirname, 'local-variables-out-of-app-default.js') - .expect({ - event: event => { - const frames = event.exception?.values?.[0]?.stacktrace?.frames || []; + test.runIf(supports({ runtimes: ['node'] }))( + 'does not add local variables to out of app frames by default', + async () => { + await createRunner(__dirname, 'local-variables-out-of-app-default.js') + .expect({ + event: event => { + const frames = event.exception?.values?.[0]?.stacktrace?.frames || []; - const inAppFrame = frames.find(frame => frame.function === 'in_app_function'); - const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function'); + const inAppFrame = frames.find(frame => frame.function === 'in_app_function'); + const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function'); - expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' }); - expect(inAppFrame?.in_app).toEqual(true); + expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' }); + expect(inAppFrame?.in_app).toEqual(true); - expect(outOfAppFrame?.vars).toBeUndefined(); - expect(outOfAppFrame?.in_app).toEqual(false); - }, - }) - .start() - .completed(); - }); + expect(outOfAppFrame?.vars).toBeUndefined(); + expect(outOfAppFrame?.in_app).toEqual(false); + }, + }) + .start() + .completed(); + }, + ); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/test.ts b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/test.ts index ef52edf7059d..4d9c312ad5ad 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/test.ts @@ -2,7 +2,7 @@ import * as childProcess from 'child_process'; import * as path from 'path'; import { describe, expect, test } from 'vitest'; import { createRunner } from '../../../utils/runner'; -import { RUNTIME } from '../../../utils'; +import { supports } from '../../../utils'; describe('OnUncaughtException integration', () => { test('should close process on uncaught error with no additional listeners registered', () => @@ -124,7 +124,7 @@ describe('OnUncaughtException integration', () => { }); // Bun and Deno: the worker thread errors are not handled as on Node. - describe.skipIf(RUNTIME !== 'node')('Worker thread error handling', () => { + describe.runIf(supports({ runtimes: ['node'] }))('Worker thread error handling', () => { test.each(['mjs', 'js'])('should not interfere with worker thread error handling ".%s"', async extension => { const runner = createRunner(__dirname, `worker-thread/caught-worker.${extension}`) .withFlags('--import', path.join(__dirname, `worker-thread/instrument.${extension}`)) diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts index 199f3dffe5aa..3f8599756958 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts @@ -3,7 +3,7 @@ import * as childProcess from 'child_process'; import * as path from 'path'; import { afterAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -import { RUNTIME } from '../../../utils'; +import { supports } from '../../../utils'; describe('onUnhandledRejectionIntegration', () => { afterAll(() => { @@ -170,32 +170,35 @@ test rejection`); }); // Bun: the rejection event's span ID differs from the already-ended span's ID. - test.skipIf(RUNTIME === 'bun')('handles unhandled rejection in spans that are ended early', async () => { - let segment: SerializedStreamedSpanContainer['items'][number] | undefined; - let errorEvent: Event | undefined; - - await createRunner(__dirname, 'scenario-with-span-ended.ts') - .unordered() - .expect({ - span: container => { - segment = container.items.find(span => span.is_segment); - expect(segment?.name).toBe('test-span'); - }, - }) - .expect({ - event: event => { - errorEvent = event; - }, - }) - .start() - .completed(); + test.runIf(supports({ runtimes: ['node', 'deno'] }))( + 'handles unhandled rejection in spans that are ended early', + async () => { + let segment: SerializedStreamedSpanContainer['items'][number] | undefined; + let errorEvent: Event | undefined; + + await createRunner(__dirname, 'scenario-with-span-ended.ts') + .unordered() + .expect({ + span: container => { + segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('test-span'); + }, + }) + .expect({ + event: event => { + errorEvent = event; + }, + }) + .start() + .completed(); - expect(segment).toBeDefined(); - expect(errorEvent).toBeDefined(); + expect(segment).toBeDefined(); + expect(errorEvent).toBeDefined(); - expect(segment!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); - expect(segment!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); - }); + expect(segment!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); + expect(segment!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); + }, + ); test('should not warn when AI_NoOutputGeneratedError or AbortError is rejected (default ignore)', () => new Promise(done => { diff --git a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts index f4cf35579ae4..5d3b6fe6cc0e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts @@ -14,8 +14,8 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, } from '@sentry/conventions/attributes'; -import { afterAll, expect } from 'vitest'; -import { conditionalTest } from '../../../utils'; +import { afterAll, describe, expect } from 'vitest'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; // `@flue/runtime` declares `engines.node >= 22.19`, so it can't live in the package's root @@ -29,7 +29,7 @@ const FLUE_DEPENDENCIES = { }, }; -conditionalTest({ min: 22 })('Flue integration', () => { +describe.runIf(supports({ min: 22 }))('Flue integration', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts index 911971abf5f9..430b0ebfda3a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts @@ -1,11 +1,11 @@ -import { afterAll, expect } from 'vitest'; -import { conditionalTest } from '../../../../utils'; +import { afterAll, describe, expect } from 'vitest'; +import { supports } from '../../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; // With `ignoreResolveSpans: false`, the channel path also subscribes `graphql:resolve` and emits a // span per non-trivial field resolver. `ignoreTrivialResolveSpans` defaults to true, so graphql's // default property resolver (the `name` field) is skipped. graphql 17 requires Node >= 22. -conditionalTest({ min: 22 })('GraphQL tracing channel Test > resolve spans', () => { +describe.runIf(supports({ min: 22 }))('GraphQL tracing channel Test > resolve spans', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts index e1ea0083877a..c41add10805e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts @@ -1,9 +1,9 @@ -import { afterAll, expect } from 'vitest'; -import { conditionalTest } from '../../../utils'; +import { afterAll, describe, expect } from 'vitest'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; // GraphQL 17 requires Node >= 22, so this suite is skipped on older Node. -conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { +describe.runIf(supports({ min: 22 }))('GraphQL tracing channel Test', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts index 1739309f1e1b..4b1cdb001c10 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts @@ -1,9 +1,9 @@ -import { afterAll, expect } from 'vitest'; -import { conditionalTest } from '../../../../utils'; +import { afterAll, describe, expect } from 'vitest'; +import { supports } from '../../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; // graphql 17 requires Node >= 22, so this suite is skipped on older Node. -conditionalTest({ min: 22 })('GraphQL tracing channel Test > useOperationNameForRootSpan', () => { +describe.runIf(supports({ min: 22 }))('GraphQL tracing channel Test > useOperationNameForRootSpan', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts index d0508d189bb3..32f8f7ff9eed 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -3,7 +3,7 @@ import { createTestServer } from '@sentry-internal/test-utils'; import { HTTP_RESPONSE_STATUS_CODE, SENTRY_OP, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import { afterAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, createRunner } from '../../../utils/runner'; -import { RUNTIME } from '../../../utils'; +import { supports } from '../../../utils'; function getCommonHttpRequestHeaders(): Record { return { @@ -194,7 +194,7 @@ describe('httpIntegration', () => { }); // Deno: the requests sometimes get a 500 response when `server.emit` is overwritten. - describe.skipIf(RUNTIME === 'deno')('custom server.emit', () => { + describe.runIf(supports({ runtimes: ['node', 'bun'] }))('custom server.emit', () => { createEsmAndCjsTests( __dirname, 'scenario-overwrite-server-emit.mjs', diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts index afc8eb2158ba..69c9874cd27f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts @@ -1,6 +1,6 @@ import { createTestServer } from '@sentry-internal/test-utils'; import { afterAll, describe, expect } from 'vitest'; -import { RUNTIME } from '../../../../utils'; +import { supports } from '../../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; describe('ignoring a continued server segment', () => { @@ -43,8 +43,9 @@ describe('ignoring a continued server segment', () => { test('propagates a negative sampling decision to outgoing fetch requests', () => testPropagation('/ignored')); // Bun 1.3.14 does not instrument outgoing `node:http` requests. // See https://github.com/getsentry/sentry-javascript/issues/23881 - test.skipIf(RUNTIME === 'bun')('propagates a negative sampling decision to outgoing node:http requests', () => - testPropagation('/ignored-http'), + test.runIf(supports({ runtimes: ['node', 'deno'] }))( + 'propagates a negative sampling decision to outgoing node:http requests', + () => testPropagation('/ignored-http'), ); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts index d26ea2281428..d1e4309ebded 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -21,8 +21,8 @@ import { URL_FULL, } from '@sentry/conventions/attributes'; import { GEN_AI_CHAT, GEN_AI_EVALUATE, GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; -import { afterAll, expect } from 'vitest'; -import { conditionalTest } from '../../../utils'; +import { afterAll, describe, expect } from 'vitest'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; // `@mastra/core` declares `engines.node >= 22.13`, so it can't live in the package's root @@ -52,7 +52,7 @@ const MASTRA_CLASSIFIER_DEPENDENCIES = { }, }; -conditionalTest({ min: 22 })('Mastra integration', () => { +describe.runIf(supports({ min: 22 }))('Mastra integration', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/neon/test.ts b/dev-packages/node-integration-tests/suites/tracing/neon/test.ts index e1310a6ff703..485e6dbfac26 100644 --- a/dev-packages/node-integration-tests/suites/tracing/neon/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/neon/test.ts @@ -14,7 +14,7 @@ import { } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { afterAll, describe, expect } from 'vitest'; -import { RUNTIME, streamedAttribute as attr } from '../../../utils'; +import { streamedAttribute as attr, supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; const ORIGIN = 'auto.db.neon'; @@ -139,7 +139,7 @@ describeWithDockerCompose('neon auto instrumentation', { workingDirectory: [__di createEsmAndCjsTests(__dirname, 'scenario-pool.mjs', 'instrument.mjs', (createTestRunner, test) => { // On Bun and Deno the pool's connect callback runs outside the segment's async context, so // the queries it issues have no parent span. - test.skipIf(RUNTIME !== 'node')('instruments `Pool` queries', { timeout: 90_000 }, async () => { + test.runIf(supports({ runtimes: ['node'] }))('instruments `Pool` queries', { timeout: 90_000 }, async () => { const runner = createTestRunner(); const spansPromise = runner.collectStreamedSpansUntilSegment('Test Span'); diff --git a/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/test.ts b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/test.ts index e0b7c43abb1d..86702f80ee20 100644 --- a/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/test.ts @@ -1,5 +1,5 @@ import { afterAll, describe, expect } from 'vitest'; -import { RUNTIME } from '../../../utils'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; describe('no_parent_span client report', () => { @@ -10,7 +10,7 @@ describe('no_parent_span client report', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { // Bun 1.3.14 does not instrument outgoing `node:http` requests. // See https://github.com/getsentry/sentry-javascript/issues/23881 - test.skipIf(RUNTIME === 'bun')( + test.runIf(supports({ runtimes: ['node', 'deno'] }))( 'records no_parent_span outcome for an outgoing http request without a local parent', async () => { const runner = createRunner() diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts index 34199f155036..d24c08130d84 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts @@ -1,5 +1,5 @@ import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { afterAll, expect } from 'vitest'; +import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, @@ -15,11 +15,11 @@ import { SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -import { conditionalTest } from '../../../../utils/index'; +import { supports } from '../../../../utils/index'; // openai 7 requires Node.js 22 — its only breaking change over v6 — so this suite is skipped on the // Node 20 CI leg rather than pinning the whole matrix to the newer runtime. -conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { +describe.runIf(supports({ min: 22 }))('OpenAI integration (V7)', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/pi-durable/test.ts b/dev-packages/node-integration-tests/suites/tracing/pi-durable/test.ts index be521b3d3d98..4333c9e0f415 100644 --- a/dev-packages/node-integration-tests/suites/tracing/pi-durable/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/pi-durable/test.ts @@ -20,9 +20,9 @@ import { SENTRY_STATUS_MESSAGE, } from '@sentry/conventions/attributes'; import { GEN_AI_CHAT, GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; -import { afterAll, expect } from 'vitest'; +import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; -import { conditionalTest, RUNTIME } from '../../../utils'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; // The pi packages declare `engines.node >= 22.19`, so they can't live in the package's root @@ -45,7 +45,7 @@ const CONTENT_ATTRIBUTES = [ GEN_AI_TOOL_CALL_RESULT, ]; -conditionalTest({ min: 22 })('pi-durable integration', () => { +describe.runIf(supports({ min: 22 }))('pi-durable integration', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -481,7 +481,7 @@ conditionalTest({ min: 22 })('pi-durable integration', () => { } // Bun does not implement `v8.setFlagsFromString`, which the scenario needs to force GC. - test.skipIf(RUNTIME === 'bun')( + test.runIf(supports({ runtimes: ['node', 'deno'] }))( 'does not keep the traces of tool calls alive for subagent conversations that never run', async () => { await createRunner() diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index 0d4fa1212fc8..4dd383254d1f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -21,7 +21,7 @@ import { } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { afterAll, describe, expect } from 'vitest'; -import { conditionalTest, EXPECTED_SDK_NAME } from '../../../utils'; +import { EXPECTED_SDK_NAME, supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; const COMMON_DB_ATTRIBUTES = { @@ -413,7 +413,7 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ // Deno: with a module load hook installed, Deno compiles a native addon (`libpq`) as JavaScript. // Bun: the `libpq` addon needs the Node symbol `node::EmitAsyncInit`, which Bun does not provide. - conditionalTest({ max: 25, skipRuntimes: ['bun', 'deno'] })('pg-native', () => { + describe.runIf(supports({ max: 25, runtimes: ['node'] }))('pg-native', () => { const EXPECTED_SPANS = { items: expect.arrayContaining([ expect.objectContaining({ name: 'Test Transaction', is_segment: true }), diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts index 9677c10f2c7b..8467dbfdaba1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts @@ -15,7 +15,7 @@ import { import { DB } from '@sentry/conventions/op'; import type { Event, SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; -import { RUNTIME } from '../../../utils'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; // On Bun, `postgres` resolves to its ESM build through the `bun` export condition, so @@ -279,7 +279,7 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: }; createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { - test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + test.runIf(mode === 'esm' || supports({ runtimes: ['node', 'deno'] }))( 'should auto-instrument `postgres` package', { timeout: 60_000 }, async () => { @@ -419,7 +419,7 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: 'scenario-requestHook.mjs', 'instrument-requestHook.mjs', (createTestRunner, test, mode) => { - test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + test.runIf(mode === 'esm' || supports({ runtimes: ['node', 'deno'] }))( 'should call requestHook when provided', { timeout: 60_000 }, async () => { @@ -506,7 +506,7 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: }; createEsmAndCjsTests(__dirname, 'scenario-url.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { - test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + test.runIf(mode === 'esm' || supports({ runtimes: ['node', 'deno'] }))( 'should instrument postgres package with URL initialization', { timeout: 90_000 }, async () => { @@ -592,7 +592,7 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: }; createEsmAndCjsTests(__dirname, 'scenario-unsafe.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { - test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + test.runIf(mode === 'esm' || supports({ runtimes: ['node', 'deno'] }))( 'should instrument sql.unsafe() queries', { timeout: 90_000 }, async () => { diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts index 0604374c83d6..e27358581b28 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts @@ -1,6 +1,6 @@ import type { SerializedStreamedSpan, SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; -import { conditionalTest } from '../../../utils'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; afterAll(() => { @@ -103,9 +103,12 @@ describe('Prisma ORM v8 Tests', () => { // The CJS scenario loads the ESM-only package via `require(esm)`, which Node's module hooks only see // from Node 22 on; on Node 20 the ORM terminals load uninstrumented. - conditionalTest({ min: 22 })('with require(esm) reaching the module hooks', testInstrumentedOperations); + describe.runIf(supports({ min: 22 }))( + 'with require(esm) reaching the module hooks', + testInstrumentedOperations, + ); - conditionalTest({ max: 21 })('with require(esm) bypassing the module hooks', () => { + describe.runIf(supports({ max: 21 }))('with require(esm) bypassing the module hooks', () => { test('should keep the pg spans on the transaction', { timeout: 75_000 }, async () => { await createRunner() .unordered() diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts index 32c7204baeb2..4443209e17aa 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts @@ -2,7 +2,7 @@ import { SENTRY_OP } from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; import { createTestServer } from '@sentry-internal/test-utils'; import { describe, expect } from 'vitest'; -import { RUNTIME } from '../../../../utils'; +import { supports } from '../../../../utils'; import { createEsmAndCjsTests } from '../../../../utils/runner'; describe('outgoing traceparent', () => { @@ -44,28 +44,31 @@ describe('outgoing traceparent', () => { createEsmAndCjsTests(__dirname, 'scenario-http.mjs', 'instrument.mjs', (createRunner, test) => { // Bun 1.3.14 does not instrument outgoing `node:http` requests. // See https://github.com/getsentry/sentry-javascript/issues/23881 - test.skipIf(RUNTIME === 'bun')('outgoing http requests should get traceparent headers', async () => { - expect.assertions(5); + test.runIf(supports({ runtimes: ['node', 'deno'] }))( + 'outgoing http requests should get traceparent headers', + async () => { + expect.assertions(5); - const [SERVER_URL, closeTestServer] = await createTestServer() - .get('/api/v1', headers => { - expect(headers['baggage']).toEqual(expect.any(String)); - expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); - expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); - expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); - }) - .start(); + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); - await createRunner() - .withEnv({ SERVER_URL }) - .expect({ - span: { - // Span contents are tested elsewhere. - }, - }) - .start() - .completed(); - closeTestServer(); - }); + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + span: { + // Span contents are tested elsewhere. + }, + }) + .start() + .completed(); + closeTestServer(); + }, + ); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts b/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts index 1d24ab0e7041..31dd20a0c712 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts @@ -10,8 +10,8 @@ import { SERVER_PORT, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { afterAll, expect } from 'vitest'; -import { conditionalTest } from '../../../utils'; +import { afterAll, describe, expect } from 'vitest'; +import { supports } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; describeWithDockerCompose('tedious auto instrumentation', { workingDirectory: [__dirname] }, () => { @@ -123,7 +123,7 @@ describeWithDockerCompose('tedious auto instrumentation', { workingDirectory: [_ }); // tedious 20 requires Node >= 22. - conditionalTest({ min: 22 })('tedious v20', () => { + describe.runIf(supports({ min: 22 }))('tedious v20', () => { createEsmAndCjsTests( __dirname, 'scenario.mjs', diff --git a/dev-packages/node-integration-tests/utils/index.ts b/dev-packages/node-integration-tests/utils/index.ts index 0849c4464bfc..4a6b9a4dde96 100644 --- a/dev-packages/node-integration-tests/utils/index.ts +++ b/dev-packages/node-integration-tests/utils/index.ts @@ -1,13 +1,11 @@ import type { EnvelopeItemType } from '@sentry/core'; import { parseSemver } from '@sentry/core'; import type * as http from 'http'; -import { describe } from 'vitest'; export const NODE_VERSION = parseSemver(process.versions.node).major || 0; /** * The runtime that runs the scenarios (`node`, `bun` or `deno`), from the `RUNTIME` env var. - * Tests use it in `test.skipIf` for behavior that a runtime does not support. */ export type Runtime = 'cloudflare' | 'node' | 'bun' | 'deno'; export const RUNTIME = (process.env.RUNTIME || 'node') as Runtime; @@ -41,33 +39,34 @@ export type DataCollectorOptions = { envelopeType?: EnvelopeItemType | EnvelopeItemType[]; }; +export type Requirements = { + /** Lowest Node major version that runs the tests. */ + min?: number; + /** Highest Node major version that runs the tests. */ + max?: number; + /** Runtimes that run the tests. All runtimes run them when this is not set. */ + runtimes?: Runtime[]; +}; + /** - * Returns`describe` or `describe.skip` depending on allowed major versions of Node and on the - * runtime that runs the scenarios. + * Whether the Node version and the runtime that runs the scenarios meet the requirements of the + * tests, for use in `test.runIf` and `describe.runIf`. * - * On Bun and Deno the version gate does not apply and the block always runs. A suite that the gate - * keeps off a Node version because it cannot run there must also be excluded for these runtimes, - * in `dev-packages/bun-integration-tests/node-suites/excludes.ts` and - * `dev-packages/deno-integration-tests/node-suites/excludes.ts`, or skipped with `skipRuntimes`. - * - * @param options.min Lowest Node major version that runs the block. - * @param options.max Highest Node major version that runs the block. - * @param options.skipRuntimes Runtimes that skip the block, for a block that cannot run there. + * On Bun and Deno the Node version requirement does not apply. A suite that cannot run on a Node + * version must also be excluded for these runtimes, in + * `dev-packages/bun-integration-tests/node-suites/excludes.ts` and + * `dev-packages/deno-integration-tests/node-suites/excludes.ts`, or limited with `runtimes`. */ -export function conditionalTest(options: { - min?: number; - max?: number; - skipRuntimes?: Runtime[]; -}): typeof describe | typeof describe.skip { - if (options.skipRuntimes?.includes(RUNTIME)) { - return describe.skip; +export function supports(requirements: Requirements): boolean { + if (requirements.runtimes && !requirements.runtimes.includes(RUNTIME)) { + return false; } // Vitest always runs on Node, so its Node version says nothing about Bun or Deno running the // scenario. Those runtimes list the suites they cannot run in their own exclude lists. if (RUNTIME !== 'node') { - return describe; + return true; } - return describe.skipIf(!matchesNodeVersion(options)); + return matchesNodeVersion(requirements); } function matchesNodeVersion({ min, max }: { min?: number; max?: number }): boolean {