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
79 changes: 79 additions & 0 deletions dev-packages/deno-integration-tests/node-suites/excludes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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.

// 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'];

// `@sentry/node` instruments `fetch` through undici's diagnostics channels, which Deno's `fetch`
// does not publish. `@sentry/deno` has its own `fetchIntegration` for this. These suites check
// spans, breadcrumbs or headers of outgoing `fetch` requests.
const NO_FETCH_INSTRUMENTATION = [
'suites/tracing/double-baggage/**',
'suites/tracing/http-client-span-streamed/test.ts',
'suites/tracing/http-client-spans/fetch-basic-streamed/test.ts',
'suites/tracing/http-client-spans/fetch-basic/test.ts',
'suites/tracing/http-client-spans/fetch-error/test.ts',
'suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts',
'suites/tracing/http-client-spans/fetch-headers-to-span-attributes/test.ts',
'suites/tracing/http-client-spans/fetch-strip-query/test.ts',
'suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts',
'suites/tracing/ignoreSpans-streamed/continued-trace-http-client/test.ts',
'suites/tracing/ignoreSpans-streamed/continued-trace-segment/test.ts',
'suites/tracing/no-parent-span-client-report/test.ts',
'suites/tracing/requests/fetch-breadcrumbs/test.ts',
'suites/tracing/requests/fetch-no-trace-propagation/test.ts',
'suites/tracing/requests/fetch-no-tracing-no-spans/test.ts',
'suites/tracing/requests/fetch-no-tracing/test.ts',
'suites/tracing/requests/fetch-sampled-no-active-span/test.ts',
'suites/tracing/requests/fetch-unsampled/test.ts',
'suites/tracing/requests/traceparent/test.ts',
'suites/tracing/sample-rand-propagation/test.ts',
'suites/tracing/sample-rate-propagation/**',
];

// In the ESM tests Deno cannot find `PrismaClient`, a CommonJS export of `@prisma/client`.
const PRISMA_ESM_INTEROP = ['suites/tracing/prisma-orm-v5/test.ts', 'suites/tracing/prisma-orm-v6/test.ts'];

// In the CJS tests Deno cannot `require()` a dependency that ships only ES modules: `graphql` 17,
// and `escape-string-regexp` under `mastra`. The ESM tests of `mastra` also check `fetch` spans.
const REQUIRE_OF_ESM_ONLY_DEPENDENCY = ['suites/tracing/graphql-tracing-channel/**', 'suites/tracing/mastra/test.ts'];

// Some or all tests fail on Deno, cause not investigated yet. In most AI suites the span
// streaming test fails. `apollo-graphql` (CJS tests only) and `mongodb` fail on Deno 2.8.3 (the CI
// version) and pass on Deno 2.9.0.
const NOT_TRIAGED = [
'suites/tracing/anthropic/test.ts',
'suites/tracing/apollo-graphql/**',
'suites/tracing/fastify/test.ts',
'suites/tracing/flue/test.ts',
'suites/tracing/google-genai/test.ts',
'suites/tracing/groq/test.ts',
'suites/tracing/http-client-spans/http-strip-query/test.ts',
'suites/tracing/ioredis-dc/test.ts',
'suites/tracing/koa/test.ts',
'suites/tracing/langchain/test.ts',
'suites/tracing/langgraph/test.ts',
'suites/tracing/mistral/test.ts',
'suites/tracing/mongodb/test.ts',
'suites/tracing/mongoose-v5/test.ts',
'suites/tracing/mysql/test.ts',
'suites/tracing/openai/test.ts',
'suites/tracing/openai/v7/test.ts',
'suites/tracing/orchestrion-lazy-registration/test.ts',
'suites/tracing/prisma-orm-v7/test.ts',
'suites/tracing/together-ai/test.ts',
'suites/tracing/vercelai/v6_v7/test.ts',
];

// Passes on Deno when run alone, but failed in about 1 of 3 full runs of this package.
const FLAKY = ['suites/tracing/tracePropagationTargets/**'];

export const NODE_SUITES_EXCLUDE = [
'**/node_modules/**',
...NODE_ONLY,
...NO_FETCH_INSTRUMENTATION,
...PRISMA_ESM_INTEROP,
...REQUIRE_OF_ESM_ONLY_DEPENDENCY,
...NOT_TRIAGED,
...FLAKY,
];
4 changes: 2 additions & 2 deletions dev-packages/deno-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"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:node-suites",
"test:unit": "deno test --allow-net --allow-read --allow-run --allow-env --no-check",
"test": "node ./scripts/run-tests.mjs",
"test:unit": "deno test --allow-net --allow-read --allow-run --allow-env --no-check suites",
"test:node-suites": "vitest run"
},
"dependencies": {
Expand Down
27 changes: 27 additions & 0 deletions dev-packages/deno-integration-tests/scripts/run-tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// `yarn test` runs the Deno-only suites (`deno test`) and then the shared Node suites (vitest),
// like the other integration test packages.
//
// `yarn test <filter>` runs only the shared Node suites with that filter, e.g. `yarn test express`,
// because `deno test` does not take vitest filters. To filter the Deno-only suites by test name,
// run `yarn test:unit --filter <name>`.
import { spawnSync } from 'node:child_process';

const filters = process.argv.slice(2);

function run(script, args = []) {
const result = spawnSync('yarn', ['--silent', script, ...args], {
stdio: 'inherit',
shell: process.platform === 'win32',
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

if (filters.length === 0) {
run('install:deno');
run('deno-types');
run('test:unit');
}

run('test:node-suites', filters);
15 changes: 4 additions & 11 deletions dev-packages/deno-integration-tests/vite.config.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
import baseConfig from '../../vite/vite.config';
import { NODE_SUITES_EXCLUDE } from './node-suites/excludes';

// Runs the Node suites below on Deno. The scenarios stay in `node-integration-tests`, and the
// Runs all Node suites on Deno. The scenarios stay in `node-integration-tests`, and the
// Deno-only suites in `suites/` run with `deno test`.
export default defineConfig({
...baseConfig,
Expand All @@ -13,16 +14,8 @@ export default defineConfig({
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',
],
// Single tests that fail on Deno are skipped with `test.skipIf` on `RUNTIME` in the Node suite.
exclude: ['**/node_modules/**'],
include: ['suites/**/test.ts'],
exclude: NODE_SUITES_EXCLUDE,
env: {
RUNTIME: 'deno',
DENO_IMPORT_MAP: fileURLToPath(new URL('./node-suites/import-map.json', import.meta.url)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createTestServer } from '@sentry-internal/test-utils';
import { 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';

function getCommonHttpRequestHeaders(): Record<string, unknown> {
return {
Expand Down Expand Up @@ -173,7 +174,8 @@ describe('httpIntegration', () => {
});
});

describe('custom server.emit', () => {
// Deno: the requests sometimes get a 500 response when `server.emit` is overwritten.
describe.skipIf(RUNTIME === 'deno')('custom server.emit', () => {
createEsmAndCjsTests(
__dirname,
'scenario-overwrite-server-emit.mjs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ describeWithDockerCompose('postgres auto instrumentation (streamed)', { workingD
});
});

conditionalTest({ max: 25 })('pg-native', () => {
// Deno: with a module load hook installed, Deno compiles a native addon (`libpq`) as JavaScript.
conditionalTest({ max: 25, skipRuntimes: ['deno'] })('pg-native', () => {
createEsmAndCjsTests(
__dirname,
'scenario-native.mjs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [
});
});

conditionalTest({ max: 25 })('pg-native', () => {
// Deno: with a module load hook installed, Deno compiles a native addon (`libpq`) as JavaScript.
conditionalTest({ max: 25, skipRuntimes: ['deno'] })('pg-native', () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,9 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory:

createEsmAndCjsTests(__dirname, 'scenario-unsafe.mjs', 'instrument.mjs', (createTestRunner, test) => {
test('should instrument sql.unsafe() queries', { timeout: 90_000 }, async () => {
await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
// The last query fails on purpose, and its unhandled rejection also sends an error event, which can
// arrive before the transaction.
await createTestRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ describe('tracer.startActiveSpan errors', () => {
// `catch` inside the callback.
test('does NOT mark span errored when uncaught error escapes raw tracer.startActiveSpan callback', async () => {
await createRunner()
// The rejection is captured as an error event, which can reach the transport before the
// transaction.
.ignore('event')
.expect({
transaction: {
transaction: 'test span name',
Expand Down
15 changes: 11 additions & 4 deletions dev-packages/node-integration-tests/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,27 @@ export type DataCollectorOptions = {
};

/**
* Returns`describe` or `describe.skip` depending on allowed major versions of Node.
* Returns`describe` or `describe.skip` depending on allowed major versions of Node and on the
* runtime that runs the scenarios.
*
* @param {{ min?: number; max?: number }} allowedVersion
* @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.
*/
export function conditionalTest(allowedVersion: {
export function conditionalTest(options: {
min?: number;
max?: number;
skipRuntimes?: Runtime[];
}): typeof describe | typeof describe.skip {
if (options.skipRuntimes?.includes(RUNTIME)) {
return describe.skip;
}
// 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 describe.skipIf(!matchesNodeVersion(allowedVersion));
return describe.skipIf(!matchesNodeVersion(options));
}

function matchesNodeVersion({ min, max }: { min?: number; max?: number }): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import { tmpdir } from 'os';
import { join } from 'path';
import { inspect } from 'util';
import { onTestFailed } from 'vitest';
import type { DeepPartial } from './../assertions';
import {
assertEnvelopeHeader,
Expand Down Expand Up @@ -108,6 +109,10 @@
const COMPILE_CACHE_ENV: Record<string, string> =
NODE_MAJOR >= 22 ? { NODE_COMPILE_CACHE: join(tmpdir(), 'sentry-node-it-compile-cache') } : {};

// The Bun and Deno packages run these suites from their own folder. Scenarios read the working
// directory (e.g. `modulesIntegration` reads its `package.json`), so it is always this package.
const PACKAGE_ROOT = join(__dirname, '..', '..');

/** Node flags that preload a module before the entry point. */
const PRELOAD_FLAGS = ['--import', '--require', '-r'];

Expand Down Expand Up @@ -277,14 +282,26 @@
let envelopeCount = 0;
let scenarioServerPort: number | undefined;
let hasExited = false;
let exitStatus: string | undefined;
let child: ReturnType<typeof spawn> | undefined;
let spawnedAt: number | undefined;
let lastOutputAt: number | undefined;
let logsDumped = false;

// Resolved the moment `complete()` runs, so `completed()` can await the result directly
// instead of polling — see the comment on `waitForEvent`.
const completedDeferred = createDeferred();
// Resolved once the scenario reports its server port, so `makeRequest` can await it directly.
const portReady = createDeferred();

// Vitest stops a test at its own timeout before `completed()` gives up, so print the child
// output then too. `completed()` prints it for the failures it reports itself.
onTestFailed(() => {
if (!isComplete) {
dumpCapturedLogs();
}
});

function complete(error?: Error): void {
if (isComplete) {
return;
Expand Down Expand Up @@ -322,12 +339,23 @@
function dumpCapturedLogs(): void {
// Skip when the failure is expected (`test.fails` variants) — the output would just be noise.
// In debug mode the same lines are already streamed live, so skip then too.
if (process.env.DEBUG || suppressErrorLogs) {
if (process.env.DEBUG || suppressErrorLogs || logsDumped) {
return;
}

logsDumped = true;
const now = Date.now();
const state = [
`runtime=${getRuntime()}`,
`pid=${child?.pid ?? 'none'}`,
hasExited ? `exited (${exitStatus})` : 'running',
`envelopes=${envelopeCount}/${expectedEnvelopeCount}`,
`ms since spawn=${spawnedAt ? now - spawnedAt : 'not spawned'}`,
`ms since last output=${lastOutputAt ? now - lastOutputAt : 'no output'}`,
].join(', ');

// eslint-disable-next-line no-console
console.log(`\n--- Captured child process output for ${testPath} ---`);
console.log(`\n--- Captured child process output for ${testPath} (${state}) ---`);
if (logs.length === 0) {
// eslint-disable-next-line no-console
console.log('(no output captured)');
Expand Down Expand Up @@ -474,7 +502,8 @@
const runtime = getRuntime();
const childFlags = wantsAutoFlush ? [...buildAutoFlushFlags(flags, testPath, runtime), ...flags] : flags;

child = spawn(runtime, buildRuntimeArgs(runtime, childFlags, testPath), { env });
child = spawn(runtime, buildRuntimeArgs(runtime, childFlags, testPath), { env, cwd: PACKAGE_ROOT });
spawnedAt = Date.now();

child.on('error', e => {
// eslint-disable-next-line no-console
Expand All @@ -491,6 +520,7 @@
});

child.stderr?.on('data', (data: Buffer) => {
lastOutputAt = Date.now();
const output = data.toString();
logs.push(output.trim());

Expand All @@ -504,6 +534,7 @@

child.on('close', (code, signal) => {
hasExited = true;
exitStatus = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`;

if (ensureNoErrorOutput) {
complete();
Expand All @@ -521,7 +552,7 @@
if (!isComplete && expectedEnvelopeCount > 0) {
const how = signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`;
complete(
new Error(

Check failure on line 555 in dev-packages/node-integration-tests/utils/runner/createRunner.ts

View workflow job for this annotation

GitHub Actions / Deno Integration Tests

suites/tracing/prisma-orm-v8/test.ts > Prisma ORM v8 Tests > Prisma ORM v8 > with require(esm) bypassing the module hooks > should keep the pg spans on the transaction [cjs]

Error: Scenario exited (code 1) after 0/1 expected envelope(s), before the test completed. ❯ ChildProcess.<anonymous> utils/runner/createRunner.ts:555:17

Check failure on line 555 in dev-packages/node-integration-tests/utils/runner/createRunner.ts

View workflow job for this annotation

GitHub Actions / Deno Integration Tests

suites/tracing/prisma-orm-v8/test.ts > Prisma ORM v8 Tests > Prisma ORM v8 > with require(esm) reaching the module hooks > should instrument Prisma ORM operations and nest their queries [cjs]

Error: Scenario exited (code 1) after 0/1 expected envelope(s), before the test completed. ❯ ChildProcess.<anonymous> utils/runner/createRunner.ts:555:17

Check failure on line 555 in dev-packages/node-integration-tests/utils/runner/createRunner.ts

View workflow job for this annotation

GitHub Actions / Deno Integration Tests

suites/tracing/prisma-orm-v8/test.ts > Prisma ORM v8 Tests > Prisma ORM v8 > should instrument Prisma ORM operations and nest their queries [esm]

Error: Scenario exited (code 1) after 0/1 expected envelope(s), before the test completed. ❯ ChildProcess.<anonymous> utils/runner/createRunner.ts:555:17
`Scenario exited (${how}) after ${envelopeCount}/${expectedEnvelopeCount} expected ` +
'envelope(s), before the test completed.',
),
Expand Down Expand Up @@ -562,6 +593,7 @@

let buffer = Buffer.alloc(0);
child.stdout?.on('data', (data: Buffer) => {
lastOutputAt = Date.now();
// This is horribly memory inefficient but it's only for tests
buffer = Buffer.concat([buffer, data]);

Expand Down
Loading