Skip to content
Draft
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
72 changes: 72 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,72 @@
// 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'];

// 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/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/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,
...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 @@ -2,7 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core';
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes';
import { afterAll, describe, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { conditionalTest, RUNTIME } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner';

// Query-span origin depends on which instrumentation is active. Blocks driving the SDK's default
Expand Down 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.
(RUNTIME === 'deno' ? describe.skip : conditionalTest({ max: 25 }))('pg-native', () => {
createEsmAndCjsTests(
__dirname,
'scenario-native.mjs',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterAll, describe, expect } from 'vitest';
import { conditionalTest } from '../../../utils';
import { conditionalTest, RUNTIME } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner';

describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [__dirname] }, () => {
Expand Down 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.
(RUNTIME === 'deno' ? describe.skip : conditionalTest({ max: 25 }))('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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { existsSync } from 'fs';
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 NODE_MAJOR = Number(process.versions.node.split('.')[0]);
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 @@ export function createRunner(...paths: string[]) {
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 @@ export function createRunner(...paths: string[]) {
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 @@ export function createRunner(...paths: string[]) {
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 @@ export function createRunner(...paths: string[]) {
});

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

Expand All @@ -504,6 +534,7 @@ export function createRunner(...paths: string[]) {

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

if (ensureNoErrorOutput) {
complete();
Expand Down Expand Up @@ -562,6 +593,7 @@ export function createRunner(...paths: string[]) {

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