Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
Original file line number Diff line number Diff line change
@@ -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'];
Expand Down
4 changes: 2 additions & 2 deletions dev-packages/node-integration-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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();
},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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()
Expand All @@ -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')
Expand Down Expand Up @@ -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: {
Expand All @@ -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')
Expand All @@ -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();
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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', () =>
Expand Down Expand Up @@ -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}`))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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<void>(done => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +29,7 @@ const FLUE_DEPENDENCIES = {
},
};

conditionalTest({ min: 22 })('Flue integration', () => {
describe.runIf(supports({ min: 22 }))('Flue integration', () => {
afterAll(() => {
cleanupChildProcesses();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> {
return {
Expand Down Expand Up @@ -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',
Expand Down
Loading
Loading