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 packages/astro/src/server/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { HTTP_SERVER } from '@sentry/conventions/op';
import { _INTERNAL_getActiveClient, applySdkMetadata, debug } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { init as initNodeSdk } from '@sentry/node';
import { isDeno } from '@sentry/server-utils';
import { DEBUG_BUILD } from '../debug-build';

declare const EdgeRuntime: string | undefined;
declare const Deno: unknown;
declare const Netlify: unknown;

/**
Expand Down Expand Up @@ -49,5 +49,5 @@ export function init(options: NodeOptions): NodeClient | undefined {

function isEdgeRuntime(): boolean {
// Netlify also defines the `Netlify` global in its Node functions, so only Deno + Netlify is its edge.
return typeof EdgeRuntime === 'string' || (typeof Netlify !== 'undefined' && typeof Deno !== 'undefined');
return typeof EdgeRuntime === 'string' || (typeof Netlify !== 'undefined' && isDeno());
}
3 changes: 2 additions & 1 deletion packages/bun/src/integrations/bunHttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
HttpServerResponse,
} from '@sentry/core/server';
import { getHttpServerSubscriptions, HTTP_ON_SERVER_REQUEST } from '@sentry/core/server';
import { isBun } from '@sentry/server-utils';

const INTEGRATION_NAME = 'BunHttpServer' as const;

Expand Down Expand Up @@ -135,7 +136,7 @@ export const bunHttpServerIntegration = defineIntegration(_bunHttpServerIntegrat
*/
export function instrumentBunHttpServer(options: BunHttpServerOptions = {}): void {
// This only makes sense under Bun; on Node the diagnostics channel already handles this.
if (!process.versions.bun || hasPatched) {
if (!isBun() || hasPatched) {
return;
}

Expand Down
12 changes: 3 additions & 9 deletions packages/server-utils/src/ai/flue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
getActiveSpan,
getDefaultIsolationScope,
getIsolationScope,
GLOBAL_OBJ,
LRUMap,
startNewTrace,
startSpan,
Expand All @@ -19,6 +18,7 @@ import {
import type { GenAiOptions } from '../core/utils';
import { getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils';
import { skipPiAiProviderIntegrations } from '../pi-ai/providers';
import { isCloudflareWorkers } from '../../utils/runtime';
import { withCleanScopes } from '../../utils/withCleanScopes';
import { FLUE_INSTRUMENTATION_KEY, FLUE_OPERATION, FLUE_ORIGIN, MAX_TRACKED_FLUE_SPANS } from './constants';
import type { SpanTracker } from './utils';
Expand Down Expand Up @@ -46,6 +46,8 @@ interface Admission {
* (https://github.com/withastro/flue/issues/813).
*/
function runInAdmissionContext<T>(admission: Admission | undefined, next: () => T): T {
// Cloudflare runs each turn from a Durable Object alarm, which already starts a trace of its own, so
// there is no leaked context to undo there.
if (isCloudflareWorkers()) {
return next();
}
Expand All @@ -59,14 +61,6 @@ function runInAdmissionContext<T>(admission: Admission | undefined, next: () =>
);
}

/**
* Cloudflare runs each turn from a Durable Object alarm, which already starts a trace of its own, so
* there is no leaked context to undo there.
*/
function isCloudflareWorkers(): boolean {
return GLOBAL_OBJ.navigator?.userAgent === 'Cloudflare-Workers';
}

/**
* Build the object to hand to `instrument()` from `@flue/runtime`.
*
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { openTelemetryIntegration, getOtlpTracesEndpoint } from './opentelemetry
export * from './ai';
export { getSqlQuerySummary, sanitizeSqlQuery, getSqlQueryAttributes, getSqlSpanName } from './utils/sql';
export type { SqlDialect } from './utils/sql';
export { isBun, isCloudflareWorkers, isDeno } from './utils/runtime';
export { instrumentPostgresJsSql } from './integrations/postgresjs-wrapper';
export type { PostgresConnectionContext } from './integrations/postgresjs-wrapper';

Expand Down
14 changes: 6 additions & 8 deletions packages/server-utils/src/integrations/hono/honoIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CHANNELS } from '../../orchestrion/channels';
import { honoModuleNames } from '../../orchestrion/config/hono';
import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation';
import { bindTracingChannelToSpan, safeChannelCallback } from '../../tracing-channel';
import { isBun, isCloudflareWorkers, isDeno } from '../../utils/runtime';
import { applyPatches } from './applyPatches';
import { INTERNAL_REQUEST_ORIGIN } from './constants';
import { createHonoRequestMiddleware } from './createHonoMiddleware';
Expand All @@ -25,12 +26,9 @@ const INTEGRATION_NAME = 'Hono' as const;

export interface HonoIntegrationOptions extends SentryHonoMiddlewareOptions {}

const globalAny = globalThis as { Bun?: unknown; Deno?: unknown; navigator?: { userAgent?: string } };
const isBun = typeof globalAny.Bun !== 'undefined';
const isDeno = typeof globalAny.Deno !== 'undefined';
const isCloudflare = globalAny.navigator?.userAgent === 'Cloudflare-Workers';

const runtime: HonoRuntime = isBun ? 'bun' : isDeno ? 'deno' : isCloudflare ? 'cloudflare' : 'node';
function getHonoRuntime(): HonoRuntime {
return isBun() ? 'bun' : isDeno() ? 'deno' : isCloudflareWorkers() ? 'cloudflare' : 'node';
}

/**
* Manually instruments a Hono app and returns the Sentry request/response middleware to register
Expand All @@ -45,7 +43,7 @@ export function honoMiddleware<E extends Env>(app: Hono<E>, options: HonoIntegra
applyPatches(app);

return createHonoRequestMiddleware({
runtime,
runtime: getHonoRuntime(),
shouldHandleError: options.shouldHandleError,
});
}
Expand Down Expand Up @@ -138,7 +136,7 @@ function instrumentHono(options: HonoIntegrationOptions): void {
// The request/response middleware is stateless (per-request state lives on the request scope) and
// `options` is fixed, so build it once and reuse it across every dispatched Context.
const requestMiddleware = createHonoRequestMiddleware({
runtime,
runtime: getHonoRuntime(),
shouldHandleError: options.shouldHandleError,
});

Expand Down
3 changes: 2 additions & 1 deletion packages/server-utils/src/orchestrion/detect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { consoleSandbox, debug, GLOBAL_OBJ } from '@sentry/core';
import { isBun } from '../utils/runtime';

/**
* The module names (e.g. `mysql`, `@hapi/hapi`) orchestrion has already injected
Expand Down Expand Up @@ -36,7 +37,7 @@ export function detectOrchestrionSetup(): void {
const { runtime, bundler, channelIntegrations } = marker;

if (!runtime && !bundler) {
if (channelIntegrations && typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') {
if (channelIntegrations && isBun()) {
if (!marker.warnedBunRunNotInjected) {
marker.warnedBunRunNotInjected = true;
consoleSandbox(() => {
Expand Down
9 changes: 3 additions & 6 deletions packages/server-utils/src/orchestrion/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel';
import type { Client } from '@sentry/core';
import { addNonEnumerableProperty, debug, GLOBAL_OBJ, waitForTracingChannelBinding } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';
import { isBun, isCloudflareWorkers, isDeno } from '../utils/runtime';
import { getOrchestrionInjectedModules } from './detect';

// Marks an instrumentation callback as already run, so it subscribes at most once
Expand All @@ -13,10 +14,6 @@ const INSTRUMENTED = Symbol.for('SentryOrchestrionInstrumented');
// oxlint-disable-next-line typescript/no-explicit-any
type InstrumentationFn = ((...args: any[]) => void) & { [INSTRUMENTED]?: boolean };

const globalAny = globalThis as { Bun?: unknown; Deno?: unknown; navigator?: { userAgent?: string } };
const isBun = typeof globalAny.Bun !== 'undefined';
const isDeno = typeof globalAny.Deno !== 'undefined';

// Labels already warned about, so a client per request does not repeat the warning.
const warnedMissingCloudflarePlugin = new Set<string>();

Expand Down Expand Up @@ -104,7 +101,7 @@ export function invokeOrchestrionInstrumentation<Callback extends Instrumentatio
}
};

if (isBun || isDeno) {
if (isBun() || isDeno()) {
DEBUG_BUILD && debug.log(`[instrumentation:${label}] Bun/Deno, subscribing eagerly`);
run();
return;
Expand All @@ -122,7 +119,7 @@ export function invokeOrchestrionInstrumentation<Callback extends Instrumentatio
// from the plugin defines `bundler` at boot, so without it the channels never fire.
if (
DEBUG_BUILD &&
globalAny.navigator?.userAgent === 'Cloudflare-Workers' &&
isCloudflareWorkers() &&
!GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler &&
!warnedMissingCloudflarePlugin.has(label)
) {
Expand Down
3 changes: 2 additions & 1 deletion packages/server-utils/src/utils/diagnosticsChannel.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// oxlint-disable-next-line no-restricted-imports
import * as nodeDiagnosticsChannel from 'node:diagnostics_channel';
import { isBun } from './runtime';

// Bun garbage-collects a diagnostics channel that no code references, together with its
// subscribers, so a subscription made at `init()` can stop receiving messages after the next GC.
// Node and Deno keep a channel alive while it has subscribers. On Bun these wrappers of the
// `node:diagnostics_channel` functions keep a reference to every channel they return, so the SDK's
// subscriptions stay active. Other runtimes get the original functions.
// See https://github.com/oven-sh/bun/issues/43086
const keepChannelsReferenced = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined';
const keepChannelsReferenced = isBun();

const channelsByName = new Map<string | symbol, nodeDiagnosticsChannel.Channel>();
const tracingChannelsByName = new Map<string, nodeDiagnosticsChannel.TracingChannel>();
Expand Down
18 changes: 18 additions & 0 deletions packages/server-utils/src/utils/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { GLOBAL_OBJ } from '@sentry/core';

const globalWithRuntimes = GLOBAL_OBJ as typeof GLOBAL_OBJ & { Bun?: unknown; Deno?: unknown };

/** Whether the SDK runs in Bun. */
export function isBun(): boolean {
return typeof globalWithRuntimes.Bun !== 'undefined';
}

/** Whether the SDK runs in Deno. */
export function isDeno(): boolean {
return typeof globalWithRuntimes.Deno !== 'undefined';
}

/** Whether the SDK runs in Cloudflare Workers. */
export function isCloudflareWorkers(): boolean {
return GLOBAL_OBJ.navigator?.userAgent === 'Cloudflare-Workers';
}
32 changes: 32 additions & 0 deletions packages/server-utils/test/utils/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { isBun, isCloudflareWorkers, isDeno } from '../../src/utils/runtime';

describe('runtime detection', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it('detects none of the runtimes in Node', () => {
expect(isBun()).toBe(false);
expect(isDeno()).toBe(false);
expect(isCloudflareWorkers()).toBe(false);
});

it('detects Bun', () => {
vi.stubGlobal('Bun', {});
expect(isBun()).toBe(true);
});

it('detects Deno', () => {
vi.stubGlobal('Deno', {});
expect(isDeno()).toBe(true);
});

it('detects Cloudflare Workers by its exact user agent', () => {
vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' });
expect(isCloudflareWorkers()).toBe(true);

vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0)' });
expect(isCloudflareWorkers()).toBe(false);
});
});
Loading