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
13 changes: 12 additions & 1 deletion packages/bun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
"types": "./build/types/index.d.ts",
"default": "./build/cjs/index.js"
}
},
"./light": {
"import": {
"types": "./build/types/light/index.d.ts",
"default": "./build/esm/light/index.js"
},
"require": {
"types": "./build/types/light/index.d.ts",
"default": "./build/cjs/light/index.js"
}
}
},
"typesVersions": {
Expand All @@ -40,7 +50,8 @@
},
"dependencies": {
"@sentry/core": "10.51.0",
"@sentry/node": "10.51.0"
"@sentry/node": "10.51.0",
"@sentry/node-core": "10.51.0"
},
"devDependencies": {
"bun-types": "^1.2.9"
Expand Down
17 changes: 16 additions & 1 deletion packages/bun/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';

export default makeNPMConfigVariants(makeBaseNPMConfig());
const baseConfig = makeBaseNPMConfig({
entrypoints: ['src/index.ts', 'src/light/index.ts'],
packageSpecificConfig: {
output: {
exports: 'named',
preserveModules: true,
},
},
});

// Convert the external array to a function so that subpath imports
// (e.g. `@sentry/node-core/light`) are also treated as external.
const externalArray = baseConfig.external || [];
baseConfig.external = id => externalArray.some(dep => id === dep || id.startsWith(`${dep}/`));

export default makeNPMConfigVariants(baseConfig);
8 changes: 8 additions & 0 deletions packages/bun/src/light/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Bun light-specific exports (override node-core/light equivalents)
export { getDefaultIntegrations, init, initWithoutDefaultIntegrations } from './sdk';
export { makeFetchTransport } from '../transports';

// Re-export everything from @sentry/node-core/light.
// Note: explicit exports above take precedence over the wildcard re-export below,
// so our Bun-specific init/getDefaultIntegrations override the node-core ones.
export * from '@sentry/node-core/light';
210 changes: 210 additions & 0 deletions packages/bun/src/light/sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import * as os from 'node:os';
import type { Integration, Options } from '@sentry/core';
import {
applySdkMetadata,
debug,
envToBool,
eventFiltersIntegration,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
linkedErrorsIntegration,
propagationContextFromHeaders,
requestDataIntegration,
spanStreamingIntegration,
stackParserFromStackParserOptions,
} from '@sentry/core';
import {
childProcessIntegration,
consoleIntegration,
contextLinesIntegration,
defaultStackParser,
getSentryRelease,
localVariablesIntegration,
modulesIntegration,
nodeContextIntegration,
onUncaughtExceptionIntegration,
onUnhandledRejectionIntegration,
processSessionIntegration,
spotlightIntegration,
systemErrorIntegration,
} from '@sentry/node-core';
import {
httpIntegration,
LightNodeClient,
nativeNodeFetchIntegration,
setAsyncLocalStorageAsyncContextStrategy,
} from '@sentry/node-core/light';
import { makeFetchTransport } from '../transports';
import type { BunOptions } from '../types';

const SPOTLIGHT_INTEGRATION_NAME = 'Spotlight';

/**
* Get default integrations for the Bun Light SDK.
*/
export function getDefaultIntegrations(): Integration[] {
return [
// Common
eventFiltersIntegration(),
functionToStringIntegration(),
linkedErrorsIntegration(),
requestDataIntegration(),
systemErrorIntegration(),
// Native Wrappers
consoleIntegration(),
httpIntegration(),
nativeNodeFetchIntegration(),
// Global Handlers
onUncaughtExceptionIntegration(),
onUnhandledRejectionIntegration(),
// Event Info
contextLinesIntegration(),
localVariablesIntegration(),
nodeContextIntegration(),
modulesIntegration(),
childProcessIntegration(),
processSessionIntegration(),
];
}

/**
* Initialize Sentry for Bun in light mode (without OpenTelemetry).
*
* This is a lightweight alternative to the default @sentry/bun entry point.
* It does not load OpenTelemetry or any auto-instrumentation modules, making it
* suitable for CLI tools and other non-server Bun applications.
*
* @example
* import * as Sentry from '@sentry/bun/light';
*
* Sentry.init({ dsn: '__DSN__' });
*/
export function init(userOptions: BunOptions = {}): LightNodeClient | undefined {
return _init(userOptions, getDefaultIntegrations);
}

/**
* Initialize Sentry for Bun in light mode, without any integrations added by default.
*/
export function initWithoutDefaultIntegrations(userOptions: BunOptions = {}): LightNodeClient {
return _init(userOptions, () => []);
}

function _init(_options: BunOptions, getDefaultIntegrationsImpl: (options: Options) => Integration[]): LightNodeClient {
const options = getClientOptions(_options, getDefaultIntegrationsImpl);

if (options.debug === true) {
debug.enable();
}

// Use AsyncLocalStorage-based context strategy instead of OpenTelemetry
setAsyncLocalStorageAsyncContextStrategy();

const scope = getCurrentScope();
scope.update(options.initialScope);

if (options.spotlight && !options.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) {
options.integrations.push(
spotlightIntegration({
sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined,
}),
);
}

applySdkMetadata(options, 'bun', ['bun', 'node-core']);

// LightNodeClient expects NodeClientOptions; our merged options are structurally compatible
const client: LightNodeClient = new LightNodeClient(options as ConstructorParameters<typeof LightNodeClient>[0]);
getCurrentScope().setClient(client);

client.init();

client.startClientReportTracking();

updateScopeFromEnvVariables();

if (process.env.VERCEL) {
process.on('SIGTERM', async () => {
await client.flush(200);
});
}

return client;
}

function getClientOptions(
options: BunOptions,
getDefaultIntegrationsImpl: (options: Options) => Integration[],
): BunOptions & { integrations: Integration[] } {
const release = getRelease(options.release);
const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate);

const mergedOptions = {
...options,
dsn: options.dsn ?? process.env.SENTRY_DSN,
environment: options.environment ?? process.env.SENTRY_ENVIRONMENT,
sendClientReports: options.sendClientReports ?? true,
transport: options.transport ?? makeFetchTransport,
stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser),
platform: 'javascript',
runtime: { name: 'bun', version: typeof Bun !== 'undefined' ? Bun.version : 'unknown' },
serverName: options.serverName || global.process.env.SENTRY_NAME || os.hostname(),
release,
tracesSampleRate,
debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG),
};

const integrations = options.integrations;
const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(mergedOptions);

const resolvedIntegrations = getIntegrationsToSetup({
defaultIntegrations,
integrations,
});

if (mergedOptions.traceLifecycle === 'stream' && !resolvedIntegrations.some(i => i.name === 'SpanStreaming')) {
resolvedIntegrations.push(spanStreamingIntegration());
}

return {
...mergedOptions,
integrations: resolvedIntegrations,
};
}

function getRelease(release: BunOptions['release']): string | undefined {
if (release !== undefined) {
return release;
}

const detectedRelease = getSentryRelease();
if (detectedRelease !== undefined) {
return detectedRelease;
}

return undefined;
}

function getTracesSampleRate(tracesSampleRate: BunOptions['tracesSampleRate']): number | undefined {
if (tracesSampleRate !== undefined) {
return tracesSampleRate;
}

const sampleRateFromEnv = process.env.SENTRY_TRACES_SAMPLE_RATE;
if (!sampleRateFromEnv) {
return undefined;
}

const parsed = parseFloat(sampleRateFromEnv);
return isFinite(parsed) ? parsed : undefined;
}

function updateScopeFromEnvVariables(): void {
if (envToBool(process.env.SENTRY_USE_ENVIRONMENT) !== false) {
const sentryTraceEnv = process.env.SENTRY_TRACE;
const baggageEnv = process.env.SENTRY_BAGGAGE;
const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv);
getCurrentScope().setPropagationContext(propagationContext);
}
}
68 changes: 68 additions & 0 deletions packages/bun/test/light.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { BaseTransportOptions, Envelope, Event, Transport, TransportMakeRequestResponse } from '@sentry/core';
import { describe, expect, test } from 'bun:test';
import type { LightNodeClient } from '../src/light/index';
import { init, makeFetchTransport } from '../src/light/index';

const envelopes: Envelope[] = [];

function testTransport(_options: BaseTransportOptions): Transport {
return {
send(request: Envelope): Promise<TransportMakeRequestResponse> {
envelopes.push(request);
return Promise.resolve({ statusCode: 200 });
},
flush(): PromiseLike<boolean> {
return new Promise(resolve => setTimeout(() => resolve(true), 100));
},
};
}

describe('Bun Light SDK', () => {
const initOptions = {
dsn: 'https://[email protected]/0000000',
tracesSampleRate: 1,
transport: testTransport,
};

test('SDK works as expected', async () => {
let client: LightNodeClient | undefined;
expect(() => {
client = init(initOptions);
}).not.toThrow();

expect(client).not.toBeUndefined();

client?.captureException(new Error('test'));
client?.flush();

await new Promise(resolve => setTimeout(resolve, 1000));

const errorEnvelope = envelopes.find(envelope => envelope?.[1][0]?.[0]?.type === 'event');
expect(errorEnvelope).toBeDefined();

const event = errorEnvelope?.[1][0][1] as Event;

expect(event.sdk?.name).toBe('sentry.javascript.bun');

expect(event.exception?.values?.[0]?.type).toBe('Error');
expect(event.exception?.values?.[0]?.value).toBe('test');
});

test('SDK sets bun runtime metadata', () => {
const client = init(initOptions);

expect(client).not.toBeUndefined();

const options = client?.getOptions();
expect(options?.runtime?.name).toBe('bun');
});

test('SDK uses makeFetchTransport by default', () => {
const client = init({ dsn: initOptions.dsn });

expect(client).not.toBeUndefined();

const options = client?.getOptions();
expect(options?.transport).toBe(makeFetchTransport);
});
});
4 changes: 3 additions & 1 deletion packages/bun/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

"compilerOptions": {
// package-specific options
"types": ["bun-types"]
"types": ["bun-types"],
"module": "Node16",
"moduleResolution": "Node16"
}
}
4 changes: 2 additions & 2 deletions packages/node-core/src/light/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export class LightNodeClient extends ServerRuntimeClient<NodeClientOptions> {

const clientOptions: ServerRuntimeClientOptions = {
...options,
platform: 'node',
runtime: { name: 'node', version: global.process.version },
platform: (options as ServerRuntimeClientOptions).platform || 'node',
runtime: (options as ServerRuntimeClientOptions).runtime || { name: 'node', version: global.process.version },
serverName,
};

Expand Down
Loading