Skip to content
Open
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
@@ -0,0 +1,32 @@
import { plugin } from 'bun';
import Module from 'node:module';

// Maps `@sentry/node` to `@sentry/bun` in the Node suite files. `@sentry/bun` imports
// `@sentry/node` itself, so imports from outside the Node suite files are not touched.
const NODE_SUITE_FILE = /[\\/]node-integration-tests[\\/](suites|utils)[\\/]/;

// Bun's runtime `onResolve` does not see bare package specifiers, so ES modules are rewritten on
// load. `onLoad` output for a CommonJS file does not run, so those files keep their source.
const NODE_SUITE_ESM_FILE = /[\\/]node-integration-tests[\\/](suites|utils)[\\/].*\.(mjs|ts)$/;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to exclude the deps that get installed in these.

Suggested change
const NODE_SUITE_ESM_FILE = /[\\/]node-integration-tests[\\/](suites|utils)[\\/].*\.(mjs|ts)$/;
const NODE_SUITE_ESM_FILE = /[\\/]node-integration-tests[\\/](suites|utils)[\\/](?!\bnode_modules\b).*\.(mjs|ts)$/;

const SENTRY_NODE_SPECIFIER = /(['"])@sentry\/node\1/g;

plugin({
name: 'alias-sentry-node-to-sentry-bun',
setup(build) {
build.onLoad({ filter: NODE_SUITE_ESM_FILE }, async args => {
const source = await Bun.file(args.path).text();
return {
contents: source.replace(SENTRY_NODE_SPECIFIER, '$1@sentry/bun$1'),
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
};
});
},
});

type ResolveFilename = (request: string, parent: { filename?: string } | undefined, ...rest: unknown[]) => string;
const moduleWithResolve = Module as unknown as { _resolveFilename: ResolveFilename };
const originalResolveFilename = moduleWithResolve._resolveFilename;
moduleWithResolve._resolveFilename = function (request, parent, ...rest) {
const aliased = request === '@sentry/node' && NODE_SUITE_FILE.test(parent?.filename ?? '') ? '@sentry/bun' : request;
return originalResolveFilename.call(this, aliased, parent, ...rest);
};
1 change: 1 addition & 0 deletions dev-packages/bun-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"pg": "8.16.0"
},
"devDependencies": {
"@sentry-internal/node-integration-tests": "11.0.0",
"@sentry-internal/test-utils": "11.0.0",
"bun-types": "^1.2.9",
"vitest": "^3.2.7"
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/bun-integration-tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.json",

"include": ["suites/**/*.ts", "*.ts"],
"include": ["suites/**/*.ts", "node-suites/**/*.ts", "*.ts"],

"compilerOptions": {
"lib": ["ES2020"],
Expand Down
65 changes: 59 additions & 6 deletions dev-packages/bun-integration-tests/vite.config.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
import baseConfig from '../../vite/vite.config';

const NODE_SUITES_ROOT = fileURLToPath(new URL('../node-integration-tests', import.meta.url));

// Node suites that also run on Bun. The scenarios stay in `node-integration-tests`.
const NODE_SUITES = [
'suites/public-api/**/test.ts',
'suites/client-reports/**/test.ts',
'suites/featureFlags/**/test.ts',
];

// Single tests that fail on Bun are skipped with `test.skipIf` on `RUNTIME` in the Node suite.
const NODE_SUITES_EXCLUDE = ['**/node_modules/**'];

const nodeSuitesTest = {
root: NODE_SUITES_ROOT,
include: NODE_SUITES,
exclude: NODE_SUITES_EXCLUDE,
testTimeout: 15_000,
};

export default defineConfig({
...baseConfig,
test: {
Expand All @@ -9,7 +29,6 @@ export default defineConfig({
enabled: false,
},
isolate: false,
include: ['./suites/**/test.ts'],
testTimeout: 20_000,
...(process.env.DEBUG
? {
Expand All @@ -18,15 +37,49 @@ export default defineConfig({
}
: {}),
pool: 'threads',
poolOptions: {
threads: {
singleThread: true,
},
},
reporters: process.env.DEBUG
? ['default', { summary: false }]
: process.env.GITHUB_ACTIONS
? ['dot', 'github-actions']
: ['verbose'],
projects: [
{
extends: true,
test: {
name: 'bun',
include: ['./suites/**/test.ts'],
poolOptions: {
threads: {
singleThread: true,
},
},
},
},
{
extends: true,
test: {
...nodeSuitesTest,
name: 'node-suites',
env: { RUNTIME: 'bun' },
},
},
{
extends: true,
test: {
...nodeSuitesTest,
name: 'node-suites-sentry-bun',
exclude: [
...NODE_SUITES_EXCLUDE,
// The scenario creates a `NodeClient` itself, which sends `sentry.javascript.node`.
'suites/public-api/logs/test.ts',
],
env: {
RUNTIME: 'bun',
RUNTIME_PRELOAD: fileURLToPath(new URL('./node-suites/alias-sentry-bun.ts', import.meta.url)),
EXPECTED_SDK_NAME: 'sentry.javascript.bun',
},
},
},
],
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"imports": {
"@sentry-internal/node-integration-tests": "../../node-integration-tests/build/esm/index.js"
}
}
9 changes: 7 additions & 2 deletions dev-packages/deno-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
"install:deno": "node ./scripts/install-deno.mjs",
"lint": "oxlint . --type-aware",
"lint:fix": "oxlint . --fix --type-aware",
"test": "run-s install:deno deno-types test:unit",
"test:unit": "deno test --allow-net --allow-read --allow-run --allow-env --no-check"
"test": "run-s install:deno deno-types test:unit test:node-suites",
"test:unit": "deno test --allow-net --allow-read --allow-run --allow-env --no-check",
"test:node-suites": "vitest run"
},
"dependencies": {
"@sentry/core": "11.0.0",
"@sentry/deno": "11.0.0",
"mysql": "^2.18.1",
"pg": "^8.22.0"
},
"devDependencies": {
"@sentry-internal/node-integration-tests": "11.0.0",
"vitest": "^3.2.7"
},
"volta": {
"extends": "../../package.json"
}
Expand Down
44 changes: 44 additions & 0 deletions dev-packages/deno-integration-tests/vite.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
import baseConfig from '../../vite/vite.config';

// Runs the Node suites below on Deno. The scenarios stay in `node-integration-tests`, and the
// Deno-only suites in `suites/` run with `deno test`.
export default defineConfig({
...baseConfig,
test: {
...baseConfig.test,
root: fileURLToPath(new URL('../node-integration-tests', import.meta.url)),
coverage: {
enabled: false,
},
isolate: false,
include: [
'suites/public-api/**/test.ts',
'suites/client-reports/**/test.ts',
'suites/featureFlags/**/test.ts',
'suites/express/tracing/**/test.ts',
'suites/tracing/httpIntegration/test.ts',
'suites/tracing/httpIntegration-streamed/test.ts',
Comment on lines +20 to +22

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low/nit/question, the PR description says that it only opts into the first three, but this is more stuff?

I think it's probably best to have them in, but maybe update the description and commit message before landing.

],
// Single tests that fail on Deno are skipped with `test.skipIf` on `RUNTIME` in the Node suite.
exclude: ['**/node_modules/**'],
env: {
RUNTIME: 'deno',
DENO_IMPORT_MAP: fileURLToPath(new URL('./node-suites/import-map.json', import.meta.url)),
},
testTimeout: 15_000,
...(process.env.DEBUG
? {
disableConsoleIntercept: true,
silent: false,
}
: {}),
pool: 'threads',
reporters: process.env.DEBUG
? ['default', { summary: false }]
: process.env.GITHUB_ACTIONS
? ['dot', 'github-actions']
: ['verbose'],
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +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';

const EXPECTED_LOCAL_VARIABLES_EVENT = {
exception: {
Expand Down Expand Up @@ -79,14 +80,15 @@ module.exports = { out_of_app_function };`,
.completed();
});

test('Should include local variables when enabled', async () => {
// Bun and Deno: the error events have no local variables.
test.skipIf(RUNTIME !== 'node')('Should include local variables when enabled', async () => {
await createRunner(__dirname, 'local-variables.js')
.expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
.start()
.completed();
});

test('Should include local variables when instrumenting via --import', async () => {
test.skipIf(RUNTIME !== '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')
Expand All @@ -96,7 +98,7 @@ module.exports = { out_of_app_function };`,
.completed();
});

test('Should include local variables with ESM', async () => {
test.skipIf(RUNTIME !== 'node')('Should include local variables with ESM', async () => {
await createRunner(__dirname, 'local-variables-caught.mjs')
.expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
.start()
Expand All @@ -107,36 +109,39 @@ module.exports = { out_of_app_function };`,
await createRunner(__dirname, 'deny-inspector.mjs').ensureNoErrorOutput().start().completed();
});

test('Should retain original local variables when error is re-thrown', async () => {
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('Includes local variables for caught exceptions when enabled', async () => {
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('Filters local variables by name via dataCollection.stackFrameVariables', async () => {
await createRunner(__dirname, 'local-variables-filtered.js')
.expect({
event: event => {
const frame = event.exception?.values?.[0]?.stacktrace?.frames?.find(frame => frame.function === 'one');

expect(frame?.vars).toEqual({
name: 'some name',
keepVar: 'keep me',
secretVar: '[Filtered]',
});
},
})
.start()
.completed();
});
test.skipIf(RUNTIME !== 'node')(
'Filters local variables by name via dataCollection.stackFrameVariables',
async () => {
await createRunner(__dirname, 'local-variables-filtered.js')
.expect({
event: event => {
const frame = event.exception?.values?.[0]?.stacktrace?.frames?.find(frame => frame.function === 'one');

expect(frame?.vars).toEqual({
name: 'some name',
keepVar: 'keep me',
secretVar: '[Filtered]',
});
},
})
.start()
.completed();
},
);

test('Does not attach local variables when dataCollection.stackFrameVariables is false', async () => {
await createRunner(__dirname, 'local-variables-disabled.js')
Expand All @@ -151,7 +156,7 @@ module.exports = { out_of_app_function };`,
.completed();
});

test('Should handle different function name formats', async () => {
test.skipIf(RUNTIME !== 'node')('Should handle different function name formats', async () => {
await createRunner(__dirname, 'local-variables-name-matching.js')
.expect({
event: {
Expand All @@ -177,30 +182,33 @@ module.exports = { out_of_app_function };`,
.completed();
});

test('adds local variables to out of app frames when includeOutOfAppFrames is true', async () => {
await createRunner(__dirname, 'local-variables-out-of-app.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');

expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' });
expect(inAppFrame?.in_app).toEqual(true);

expect(outOfAppFrame?.vars).toEqual({
outOfAppVar: 'out of app value modified value',
passedArg: 'in app value modified value',
});
expect(outOfAppFrame?.in_app).toEqual(false);
},
})
.start()
.completed();
});
test.skipIf(RUNTIME !== 'node')(
'adds local variables to out of app frames when includeOutOfAppFrames is true',
async () => {
await createRunner(__dirname, 'local-variables-out-of-app.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');

expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' });
expect(inAppFrame?.in_app).toEqual(true);

expect(outOfAppFrame?.vars).toEqual({
outOfAppVar: 'out of app value modified value',
passedArg: 'in app value modified value',
});
expect(outOfAppFrame?.in_app).toEqual(false);
},
})
.start()
.completed();
},
);

test('does not add local variables to out of app frames by default', async () => {
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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +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';

describe('OnUncaughtException integration', () => {
test('should close process on uncaught error with no additional listeners registered', () =>
Expand Down Expand Up @@ -122,7 +123,8 @@ describe('OnUncaughtException integration', () => {
.completed();
});

describe('Worker thread error handling', () => {
// Bun and Deno: the worker thread errors are not handled as on Node.
describe.skipIf(RUNTIME !== '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
Loading
Loading