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
4 changes: 2 additions & 2 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -430,7 +430,7 @@ module.exports = [
path: 'packages/node/build/esm/index.js',
import: createImport('initWithoutDefaultIntegrations', 'getDefaultIntegrationsWithoutPerformance'),
gzip: true,
limit: '87 KB',
limit: '92 KB',
disablePlugins: ['@size-limit/esbuild'],
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
modifyWebpackConfig: function (config) {
Expand All@@ -454,7 +454,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '97 KB',
limit: '99 KB',
disablePlugins: ['@size-limit/esbuild'],
},
// Cloudflare SDK (ESM) - compressed, minified to match `wrangler deploy --dry-run --minify` output
Expand Down
55 changes: 39 additions & 16 deletions dev-packages/deno-integration-tests/src/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { TransactionEvent } from '@sentry/core';
import type { Event, TransactionEvent } from '@sentry/core';
import { getAsyncContextStrategy, getMainCarrier, setAsyncContextStrategy } from '@sentry/core';

/**
Expand All@@ -16,41 +16,64 @@ export function resetGlobals(): void {
setAsyncContextStrategy(acs);
}

export interface TransactionSink {
beforeSendTransaction: (event: TransactionEvent) => null;
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
interface EventSink<T> {
beforeSend: (event: T) => null;
waitFor: (predicate: (event: T) => boolean) => Promise<T>;
}

/**
* A `beforeSendTransaction` hook that records every transaction and lets a test
* `await` the first one matching a predicate. `waitFor` resolves immediately if
* a match already arrived, so there is no ordering race with the hook.
*/
export function transactionSink(): TransactionSink {
const transactions: TransactionEvent[] = [];
const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
Comment thread
cursor[bot] marked this conversation as resolved.
function eventSink<T>(): EventSink<T> {
const events: T[] = [];
const waiters: { predicate: (e: T) => boolean; resolve: (e: T) => void }[] = [];
return {
beforeSendTransaction(event) {
transactions.push(event);
beforeSend(event) {
events.push(event);

for (let i = waiters.length - 1; i >= 0; i--) {
const w = waiters[i]!;
if (w.predicate(event)) {
waiters.splice(i, 1);
w.resolve(event);
}
}

return null;
},
waitFor(predicate) {
const already = transactions.find(predicate);
const already = events.find(predicate);
if (already) return Promise.resolve(already);
return new Promise<TransactionEvent>(resolve => {
return new Promise<T>(resolve => {
waiters.push({ predicate, resolve });
});
},
};
}

/**
* A `beforeSend` hook that records every transaction event and lets a test
* `await` the first one matching a predicate. `waitFor` resolves immediately if
* a match already arrived, so there is no ordering race with the hook.
*/
export function transactionSink(): {
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
beforeSendTransaction: (event: TransactionEvent) => null;
} {
const sink = eventSink<TransactionEvent>();

return {
waitFor: sink.waitFor,
beforeSendTransaction: sink.beforeSend,
};
}

/**
* A `beforeSend` hook that records every error and lets a test
* `await` the first one matching a predicate. `waitFor` resolves immediately if
* a match already arrived, so there is no ordering race with the hook.
*/
export function errorSink(): EventSink<Event> {
return eventSink<Event>();
}

/** Reject with a descriptive message if `p` does not settle within `ms`. */
export function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
// <reference lib="deno.ns" />

import { channel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
import { init } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { errorSink, resetGlobals, withTimeout } from '../../src/index.ts';

Deno.test('fastify instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({ traceLifecycle: 'static', dsn: 'https://username@domain/123' }) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('Fastify'), `Fastify should be in defaults, got ${names.join(', ')}`);
});

Deno.test('fastify instrumentation: tracing:fastify.request.handler:error channel captures the error', async () => {
resetGlobals();
const sink = errorSink();
init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
beforeSend: sink.beforeSend,
});

const error = new Error('fastify boom');

// Fastify v5 publishes this native diagnostics channel when a request handler errors; the
// integration subscribes to it directly (no orchestrion injection needed). A 5xx reply passes the
// default `shouldHandleError`, so the error is captured.
channel('tracing:fastify.request.handler:error').publish({
error,
request: { method: 'GET', routeOptions: { url: '/boom' } },
reply: { statusCode: 500 },
});

const event = await withTimeout(
sink.waitFor(e => e.exception?.values?.[0]?.value === 'fastify boom'),
5000,
"the captured 'fastify boom' error",
);

assertExists(event.exception?.values?.[0]);
assertEquals(event.exception?.values?.[0]?.mechanism?.type, 'auto.function.fastify');
assertEquals(event.exception?.values?.[0]?.mechanism?.handled, false);
});
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
const Sentry = require('@sentry/node');
const { waitForDebuggerReady } = require('@sentry-internal/test-utils');

setTimeout(() => {
process.exit();
Expand DownExpand Up@@ -52,7 +53,9 @@ setTimeout(() => {
setTimeout(() => {
anr.startWorker();

setTimeout(() => {
// Wait for the restarted worker's debugger session to reconnect before blocking the event
// loop, otherwise on slow CI the worker isn't ready to sample and the ANR is missed entirely.
waitForDebuggerReady(() => {
Comment thread
cursor[bot] marked this conversation as resolved.
longWork();
});
}, 2000);
Expand Down
7 changes: 5 additions & 2 deletions packages/bun/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,6 @@ import type { NodeClient } from '@sentry/node';
import {
consoleIntegration,
contextLinesIntegration,
getAutoPerformanceIntegrations,
httpIntegration,
init as initNode,
modulesIntegration,
Expand All@@ -26,6 +25,7 @@ import { fetchIntegration } from './integrations/fetch';
import { makeFetchTransport } from './transports';
import type { BunOptions } from './types';
import { bunHttpServerIntegration } from './integrations/bunHttpServer';
import { getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils';

/**
* The performance integrations for bun: the OTel auto-performance set, but with
Expand All@@ -40,7 +40,7 @@ function getPerformanceIntegrations(options: Options): Integration[] {
return [];
}

return getAutoPerformanceIntegrations();
return getTracingIntegrations();
}

/** Get the default integrations for the Bun SDK, excluding performance integrations. */
Expand All@@ -64,6 +64,9 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] {
nodeContextIntegration(),
modulesIntegration(),
processSessionIntegration(),
// Framework-level integrations. These are not performance-only: they also handle error capture, so
// they are added by default rather than gated behind tracing
...getErrorIntegrations(),
// Bun Specific
bunServerIntegration(),
bunHttpServerIntegration(),
Expand Down
25 changes: 12 additions & 13 deletions packages/bun/test/init.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { type Integration } from '@sentry/core';
import * as sentryNode from '@sentry/node';
import * as sentryServerUtils from '@sentry/server-utils';
import type { Mock } from 'bun:test';
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import {
Expand All@@ -22,15 +22,14 @@ class MockIntegration implements Integration {
}

describe('init()', () => {
let mockAutoPerformanceIntegrations: Mock<() => Integration[]>;
let mockGetTracingIntegrations: Mock<() => Integration[]>;

beforeEach(() => {
// @ts-expect-error weird
mockAutoPerformanceIntegrations = spyOn(sentryNode, 'getAutoPerformanceIntegrations');
mockGetTracingIntegrations = spyOn(sentryServerUtils, 'getTracingIntegrations');
});

afterEach(() => {
mockAutoPerformanceIntegrations.mockRestore();
mockGetTracingIntegrations.mockRestore();
});

describe('integrations', () => {
Expand All@@ -41,7 +40,7 @@ describe('init()', () => {

expect(client?.getOptions().integrations).toEqual([]);

expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
});

it('enables spotlight with default URL from config `true`', () => {
Expand DownExpand Up@@ -75,7 +74,7 @@ describe('init()', () => {
expect(mockDefaultIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1);
expect(mockIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1);
expect(mockIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1);
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
});

it('installs integrations returned from a callback function', () => {
Expand All@@ -99,12 +98,12 @@ describe('init()', () => {
expect(mockDefaultIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1);
expect(mockDefaultIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(0);
expect(newIntegration.setupOnce).toHaveBeenCalledTimes(1);
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
});

it('installs performance default instrumentations if tracing is enabled', () => {
const autoPerformanceIntegrations = [new MockIntegration('Performance integration')];
mockAutoPerformanceIntegrations.mockImplementation(() => autoPerformanceIntegrations);
mockGetTracingIntegrations.mockImplementation(() => autoPerformanceIntegrations);

const mockIntegrations = [
new MockIntegration('Some mock integration 4.1'),
Expand All@@ -120,7 +119,7 @@ describe('init()', () => {
expect(mockIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1);
expect(mockIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1);
expect(autoPerformanceIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1);
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(1);
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(1);

const integrations = getClient()?.getOptions().integrations;
expect(integrations).toBeArray();
Expand All@@ -137,7 +136,7 @@ describe('init()', () => {
const client = getClient();

expect(client?.getOptions().integrations).toEqual([]);
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
});

it('still installs user-provided integrations', () => {
Expand All@@ -162,12 +161,12 @@ describe('init()', () => {
const full = getDefaultIntegrations({}).map(({ name }) => name);

expect(withoutPerformance).toEqual(full);
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
});

it('omits the performance integrations that the full set adds when tracing is enabled', () => {
const performanceIntegration = new MockIntegration('Performance integration');
mockAutoPerformanceIntegrations.mockImplementation(() => [performanceIntegration]);
mockGetTracingIntegrations.mockImplementation(() => [performanceIntegration]);

const withoutPerformance = getDefaultIntegrationsWithoutPerformance().map(({ name }) => name);
const full = getDefaultIntegrations({ tracesSampleRate: 1 }).map(({ name }) => name);
Expand Down
60 changes: 4 additions & 56 deletions packages/deno/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,32 +11,7 @@ import {
requestDataIntegration,
stackParserFromStackParserOptions,
} from '@sentry/core';
import {
amqplibIntegration,
anthropicAIIntegration,
awsIntegration,
expressIntegration,
firebaseIntegration,
genericPoolIntegration,
googleGenAIIntegration,
graphqlIntegration,
hapiIntegration,
kafkaIntegration,
koaIntegration,
langChainIntegration,
langGraphIntegration,
lruMemoizerIntegration,
mongoIntegration,
mongooseIntegration,
mysqlIntegration,
mysql2Integration,
openAIIntegration,
postgresIntegration,
postgresJsIntegration,
tediousIntegration,
vercelAIIntegration,
redisIntegration,
} from '@sentry/server-utils';
import { getTracingIntegrations, getErrorIntegrations } from '@sentry/server-utils';
import { DenoClient } from './client';
import { breadcrumbsIntegration } from './integrations/breadcrumbs';
import { denoContextIntegration } from './integrations/context';
Expand DownExpand Up@@ -64,39 +39,12 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
denoContextIntegration(),
denoServeIntegration(),
denoHttpIntegration(),
redisIntegration(),
graphqlIntegration(),
vercelAIIntegration(),
// orchestrion-based instrumentations. We add a deliberate list here rather
// than every channel integration: each one needs a Deno test proving it
// records spans.
//
// The orchestrion channels may be injected after (or while) the SDK loads.
// If they never load, these are no-ops.
amqplibIntegration(),
anthropicAIIntegration(),
awsIntegration(),
expressIntegration(),
firebaseIntegration(),
genericPoolIntegration(),
googleGenAIIntegration(),
hapiIntegration(),
kafkaIntegration(),
koaIntegration(),
langChainIntegration(),
langGraphIntegration(),
lruMemoizerIntegration(),
mongoIntegration(),
mongooseIntegration(),
mysqlIntegration(),
mysql2Integration(),
openAIIntegration(),
postgresIntegration(),
postgresJsIntegration(),
tediousIntegration(),
contextLinesIntegration(),
normalizePathsIntegration(),
globalHandlersIntegration(),
// server-utils integrations
...getErrorIntegrations(),
...getTracingIntegrations(),
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
];
}

Expand Down
Loading
Loading