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
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ try {
// ignore
}

const marker = (globalThis as { __SENTRY_ORCHESTRION__?: { runtime?: string[]; bundler?: string[] } })
const marker = (globalThis as { __SENTRY_ORCHESTRION__?: { runtime?: string[]; bundler?: Set<string> } })
.__SENTRY_ORCHESTRION__;
// `bundler` is a `Set`, which `JSON.stringify` renders as `{}`. Spread it into
// an array so the test can read the recorded module names.
const reportedMarker = marker ? { ...marker, bundler: marker.bundler ? [...marker.bundler] : undefined } : null;

setTimeout(() => {
// eslint-disable-next-line no-console
console.log(`SCENARIO events=${events.join(',')} statement=${statement} marker=${JSON.stringify(marker ?? null)}`);
console.log(`SCENARIO events=${events.join(',')} statement=${statement} marker=${JSON.stringify(reportedMarker)}`);
process.exit(0);
}, 200);
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,14 @@ try {
// `start` has already published synchronously by this point.
}

const marker = (globalThis as { __SENTRY_ORCHESTRION__?: { runtime?: string[]; bundler?: string[] } })
const marker = (globalThis as { __SENTRY_ORCHESTRION__?: { runtime?: string[]; bundler?: Set<string> } })
.__SENTRY_ORCHESTRION__;
// `bundler` is a `Set`, which `JSON.stringify` renders as `{}`. Spread it into
// an array so the test can read the recorded module names.
const reportedMarker = marker ? { ...marker, bundler: marker.bundler ? [...marker.bundler] : undefined } : null;

setTimeout(() => {
// eslint-disable-next-line no-console
console.log(`SCENARIO events=${events.join(',')} statement=${statement} marker=${JSON.stringify(marker ?? null)}`);
console.log(`SCENARIO events=${events.join(',')} statement=${statement} marker=${JSON.stringify(reportedMarker)}`);
process.exit(0);
}, 200);
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ test.describe('Orchestrion build-time injection', () => {
});

test('does not inject diagnostics-channel publishers into the client build', () => {
// The client build may carry the inert `__SENTRY_ORCHESTRION__.bundler = []` detection marker
// The client build may carry the inert `__SENTRY_ORCHESTRION__.bundler = new Set()` detection marker
// (it's environment-agnostic and harmless in a browser), but the actual `tracingChannel` publishers
// and their channel names must never leak — `applyToEnvironment` keeps the transform server-only, so
// a browser never hits a `diagnostics_channel` call that would throw.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ assert.equal(
// real build this runs when the bundled module is first evaluated.
orchestrionModuleInjected('generic-pool');

assert.ok(marker.bundler?.includes('generic-pool'), 'expected the module to be recorded as bundler-injected');
assert.ok(marker.bundler?.has('generic-pool'), 'expected the module to be recorded as bundler-injected');

// The helper emitted `orchestrion.module-injected`, so the GenericPool
// integration must have subscribed, even though generic-pool was never loaded
Expand Down
6 changes: 3 additions & 3 deletions packages/bun/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ interface BunPluginBuilder {
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them — plus, via the module-injected transform, the
* snippet that records each module on `globalThis.__SENTRY_ORCHESTRION__` when
* it is evaluated — and injects the marker banner so `bundler` is set (to `[]`)
* from boot, which is what gates the SDK's channel-integration setup at
* `init()`.
* it is evaluated — and injects the marker banner so `bundler` is set (to an
* empty `Set`) from boot, which is what gates the SDK's channel-integration
* setup at `init()`.
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
6 changes: 3 additions & 3 deletions packages/cloudflare/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('getDefaultIntegrations', () => {

test('does not add orchestrion channel integrations when only the bundler marker is set', () => {
// The plugin's entry banner ran, but no instrumented module has loaded yet.
globalThis.__SENTRY_ORCHESTRION__ = { bundler: [] };
globalThis.__SENTRY_ORCHESTRION__ = { bundler: new Set() };

const names = getDefaultIntegrations({}).map(i => i.name);

Expand All @@ -115,7 +115,7 @@ describe('getDefaultIntegrations', () => {
const { mysqlIntegration, postgresIntegration, lruMemoizerIntegration } =
await import('@sentry/server-utils/orchestrion');
globalThis.__SENTRY_ORCHESTRION__ = {
bundler: ['mysql', 'pg', 'lru-memoizer'],
bundler: new Set(['mysql', 'pg', 'lru-memoizer']),
integrations: new Map([
['mysql', mysqlIntegration],
['pg', postgresIntegration],
Expand All @@ -138,7 +138,7 @@ describe('getDefaultIntegrations', () => {
// Mirror `orchestrionModuleInjected` for a driver that first evaluates
// after init: store the factory on the marker, then emit the event.
globalThis.__SENTRY_ORCHESTRION__ = {
bundler: ['mysql'],
bundler: new Set(['mysql']),
integrations: new Map([['mysql', mysqlIntegration]]),
};
client?.emit('orchestrion.module-injected', 'mysql');
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/utils/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ export type InternalGlobal = {
/**
* Module names recorded as each bundler-transformed module loads (the
* injected snippet calls `orchestrionModuleInjected`). The bundler plugin's
* entry banner ensures `[]` at boot, so a defined array — even empty —
* signifies the plugin ran.
* entry banner ensures an empty `Set` at boot, so a defined set — even
* empty — signifies the plugin ran.
*/
bundler?: string[];
bundler?: Set<string>;
/**
* Channel-subscriber integration factories stored by the snippet the
* bundler transform splices into each instrumented module, keyed by module
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ const DEFAULT_IMPORT_SPECIFIER = '@sentry/server-utils/orchestrion';

/**
* Entry-chunk banner that marks "the bundler plugin ran" for
* `detectOrchestrionSetup()`. Merge-only (`g.bundler = g.bundler || []`) so it
* can never clobber module names already recorded by an injected snippet that
* happened to run first; the names themselves arrive per module, when each
* `detectOrchestrionSetup()`. Merge-only (`g.bundler = g.bundler || new Set()`)
* so it can never clobber module names already recorded by an injected snippet
* that happened to run first; the names themselves arrive per module, when each
* transformed module is evaluated and its snippet calls
* `orchestrionModuleInjected`.
*/
export const ORCHESTRION_BUNDLER_MARKER_BANNER =
';(function(){var g=globalThis.__SENTRY_ORCHESTRION__=globalThis.__SENTRY_ORCHESTRION__||{};g.bundler=g.bundler||[];})();';
';(function(){var g=globalThis.__SENTRY_ORCHESTRION__=globalThis.__SENTRY_ORCHESTRION__||{};g.bundler=g.bundler||new Set();})();';

/**
* Snippet injected into each instrumented module. It imports the
Expand Down
16 changes: 9 additions & 7 deletions packages/server-utils/src/orchestrion/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ export function isOrchestrionInjected(): boolean {
* Channel-based integrations use it to decide whether to subscribe now (their
* module is already loaded) or wait for the module-injected event.
*
* The `Array.isArray` guard is runtime safety, not typing: a banner from
* another SDK copy or version may have written a non-array flag here.
* The `instanceof Set` guard is runtime safety, not typing: a banner from
* another SDK copy or version may have written a non-Set flag here.
*/
export function getOrchestrionInjectedModules(): string[] {
const { runtime, bundler } = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ?? {};
return [...(runtime ?? []), ...(Array.isArray(bundler) ? bundler : [])];
return [...(runtime ?? []), ...(bundler instanceof Set ? bundler : [])];
}

/**
* Verifies that the diagnostics channels have been injected either by the
* runtime `--import` hook (or init-time registration), a bundler plugin, or
* both, and warns if not. When at least one injector is active, logs for each
* mechanism whether it hooked (a defined array, even empty, means it did) and
* mechanism whether it hooked (a defined list, even empty, means it did) and
* which libraries it injected. For the bundler path, the entry banner ensures
* `[]` at boot; module names arrive as each transformed module is evaluated,
* so an empty list can also just mean none has loaded yet.
* an empty `Set` at boot; module names arrive as each transformed module is
* evaluated, so an empty set can also just mean none has loaded yet.
*
* Both injectors being active at once is fine: they operate on disjoint module
* sets (a module is either loaded through Node's loader and transformed by the
Expand Down Expand Up @@ -64,7 +64,9 @@ export function detectOrchestrionSetup(): void {
);
debug.log(
bundler
? `[Sentry] Bundler plugin ran, injected libraries=${JSON.stringify(bundler)}`
? `[Sentry] Bundler plugin ran, injected libraries=${JSON.stringify(
bundler instanceof Set ? [...bundler] : bundler,
)}`
: '[Sentry] Bundler plugin did not run',
);
}
9 changes: 3 additions & 6 deletions packages/server-utils/src/orchestrion/moduleInjected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,9 @@ export function orchestrionModuleInjected(moduleName: string, integrationFn?: ()
const marker = (GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ??= {});

// Runtime guard, not just type narrowing: a banner from another SDK copy or
// version may have written a non-array flag here; leave that untouched.
if (marker.bundler === undefined || Array.isArray(marker.bundler)) {
const bundler = (marker.bundler ??= []);
if (!bundler.includes(moduleName)) {
bundler.push(moduleName);
}
// version may have written a non-Set flag here; leave that untouched.
if (marker.bundler === undefined || marker.bundler instanceof Set) {
(marker.bundler ??= new Set()).add(moduleName);
}

if (integrationFn) {
Expand Down
18 changes: 14 additions & 4 deletions packages/server-utils/test/orchestrion/bundler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,20 +347,30 @@ describe('orchestrionTransformOptions', () => {
new Function('globalThis', banner as string)(global);
}

it('marks the plugin as ran with an empty module list', () => {
it('marks the plugin as ran with an empty module set', () => {
const global: Record<string, unknown> = {};

runBanner(global);

expect((global.__SENTRY_ORCHESTRION__ as { bundler?: string[] }).bundler).toEqual([]);
expect((global.__SENTRY_ORCHESTRION__ as { bundler?: Set<string> }).bundler).toEqual(new Set());
});

it('never clobbers module names an injected snippet already recorded', () => {
const global: Record<string, unknown> = { __SENTRY_ORCHESTRION__: { bundler: ['mysql'] } };
const global: Record<string, unknown> = { __SENTRY_ORCHESTRION__: { bundler: new Set(['mysql']) } };

runBanner(global);

expect((global.__SENTRY_ORCHESTRION__ as { bundler?: string[] }).bundler).toEqual(['mysql']);
expect((global.__SENTRY_ORCHESTRION__ as { bundler?: Set<string> }).bundler).toEqual(new Set(['mysql']));
});

it('runs twice without resetting the recorded modules', () => {
const global: Record<string, unknown> = {};

runBanner(global);
(global.__SENTRY_ORCHESTRION__ as { bundler: Set<string> }).bundler.add('mysql');
runBanner(global);

expect((global.__SENTRY_ORCHESTRION__ as { bundler?: Set<string> }).bundler).toEqual(new Set(['mysql']));
});
});
});
29 changes: 27 additions & 2 deletions packages/server-utils/test/orchestrion/detect.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GLOBAL_OBJ } from '@sentry/core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { isOrchestrionInjected } from '../../src/orchestrion/detect';
import { getOrchestrionInjectedModules, isOrchestrionInjected } from '../../src/orchestrion/detect';

describe('isOrchestrionInjected', () => {
beforeEach(() => {
Expand All @@ -17,7 +17,8 @@ describe('isOrchestrionInjected', () => {

it.each([
['runtime', { runtime: [] }],
['bundler array', { bundler: ['mysql'] }],
['bundler set', { bundler: new Set(['mysql']) }],
['bundler empty set', { bundler: new Set() }],
['bundler true', { bundler: true }],
['integrations', { integrations: new Map() }],
] as const)('is true when %s injection is present', (_label, marker) => {
Expand All @@ -27,3 +28,27 @@ describe('isOrchestrionInjected', () => {
expect(isOrchestrionInjected()).toBe(true);
});
});

describe('getOrchestrionInjectedModules', () => {
beforeEach(() => {
delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__;
});

afterEach(() => {
delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__;
});

it('is empty when no marker exists', () => {
expect(getOrchestrionInjectedModules()).toEqual([]);
});

it('merges the runtime list and the bundler set', () => {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { runtime: ['pg'], bundler: new Set(['mysql']) };
expect(getOrchestrionInjectedModules()).toEqual(['pg', 'mysql']);
});

it('ignores a foreign non-Set bundler flag', () => {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { runtime: ['pg'], bundler: true as unknown as Set<string> };
expect(getOrchestrionInjectedModules()).toEqual(['pg']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ describe('invokeOrchestrionInstrumentation', () => {
});

it('treats bundler-recorded modules as injected', () => {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: ['mysql'] };
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: new Set(['mysql']) };
const client = makeClient();
const callback = vi.fn();

Expand All @@ -179,8 +179,8 @@ describe('invokeOrchestrionInstrumentation', () => {
expect(callback).toHaveBeenCalledTimes(1);
});

it('ignores a non-array bundler flag (Bun sets `true`)', () => {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: true as unknown as string[] };
it('ignores a non-Set bundler flag written by a foreign SDK copy', () => {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: true as unknown as Set<string> };
const client = makeClient();
const callback = vi.fn();

Expand Down
20 changes: 14 additions & 6 deletions packages/server-utils/test/orchestrion/moduleInjected.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ describe('orchestrionModuleInjected', () => {

it('records the module name as bundler-injected', () => {
orchestrionModuleInjected('mysql');
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(['mysql']);
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(new Set(['mysql']));
});

it('deduplicates the recorded module across repeated calls', () => {
orchestrionModuleInjected('mysql');
orchestrionModuleInjected('mysql');
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(['mysql']);
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(new Set(['mysql']));
});

it('adds to the set the banner already created', () => {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: new Set(['pg']) };

orchestrionModuleInjected('mysql');

expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(new Set(['pg', 'mysql']));
});

it('stores the factory on the global marker keyed by module name', () => {
Expand All @@ -41,7 +49,7 @@ describe('orchestrionModuleInjected', () => {
it('emits the module-injected event on the current client, after recording', () => {
const emit = vi.fn(() => {
// Listeners react by reading the marker, so it must be recorded by now.
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(['mysql']);
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(new Set(['mysql']));
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.integrations?.has('mysql')).toBe(true);
});
getCurrentScope().setClient({ emit } as unknown as Client);
Expand All @@ -54,7 +62,7 @@ describe('orchestrionModuleInjected', () => {
it('does not throw when no client is set yet', () => {
expect(() => orchestrionModuleInjected('mysql', factory('Mysql'))).not.toThrow();
// still recorded for the next init() to pick up
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(['mysql']);
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.bundler).toEqual(new Set(['mysql']));
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.integrations?.has('mysql')).toBe(true);
});

Expand All @@ -69,8 +77,8 @@ describe('orchestrionModuleInjected', () => {
expect(emit).toHaveBeenCalledWith('orchestrion.module-injected', 'mysql');
});

it('leaves a foreign non-array bundler flag untouched but still stores and emits', () => {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: true as unknown as string[] };
it('leaves a foreign non-Set bundler flag untouched but still stores and emits', () => {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: true as unknown as Set<string> };
const emit = vi.fn();
getCurrentScope().setClient({ emit } as unknown as Client);

Expand Down
Loading