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
98 changes: 94 additions & 4 deletions packages/server-utils/src/orchestrion/bundler/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,38 @@ import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS } from '../config';
// false positive. We don't need the runtime value for typing — `UnknownPlugin`
// is sufficient — so we omit the import entirely.

export interface SentryOrchestrionPluginOptions {
/**
* Whether to register the SDK's channel-subscriber integrations at build time.
*
* When `true`, the plugin injects a generated (virtual) registration module
* into the app's server entry. That module puts the channel-subscriber
* integrations on the global orchestrion marker, where
* `getRegisteredChannelIntegrations()` picks them up. This is how the
* subscriber integrations — which SDKs deliberately do not import so bundlers
* can drop them — end up in the bundle exactly when this plugin injects the
* channels they subscribe to.
*
* The registration is SDK-agnostic: the virtual module imports from
* `@sentry/server-utils` (a transitive dependency of every SDK that uses this
* plugin), so any bundled SDK — Cloudflare today, Nuxt/Nitro, SvelteKit, Node
* SSR later — enables it the same way, with nothing to publish or wire up.
*
* Leave unset for SDKs that register integrations through the runtime
* `--import` hook instead (e.g. `@sentry/node`), which read them from a static
* import rather than the marker.
*/
registerIntegrations?: boolean;
}

/**
* Vite plugin that runs the orchestrion code transform on the bundled output.
*
* Use when bundling a Node app with Vite (e.g. Vite SSR builds, Nuxt's Nitro
* pipeline, SvelteKit). For unbundled Node processes use the runtime hook
* instead (`node --import @sentry/node/orchestrion app.js`).
*
* Returns two plugins:
* Returns the following plugins:
* 1. `sentry-orchestrion-marker` — a `renderChunk` hook that prepends a
* single-line banner to entry chunks. The banner sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` at app boot, so the
Expand All @@ -39,7 +63,11 @@ import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS } from '../config';
* Also injects every instrumented package name into `ssr.noExternal` via
* the `config` hook, since externalized deps are `require()`d at runtime
* from `node_modules` and never pass through the transform.
* 2. The upstream `@apm-js-collab/code-transformer-bundler-plugins/vite`
* 2. `sentry-orchestrion-register-integrations` (only with
* `options.registerIntegrations`) — injects a generated registration
* module into the app's server entry, see
* {@link SentryOrchestrionPluginOptions.registerIntegrations}.
* 3. The upstream `@apm-js-collab/code-transformer-bundler-plugins/vite`
* plugin, fed our central `SENTRY_INSTRUMENTATIONS` config.
*
* @example
Expand All @@ -49,12 +77,74 @@ import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS } from '../config';
* export default { plugins: [sentryOrchestrionPlugin()] };
* ```
*/
export function sentryOrchestrionPlugin(): UnknownPlugin[] {
export function sentryOrchestrionPlugin(options: SentryOrchestrionPluginOptions = {}): UnknownPlugin[] {
const codeTransformerPlugins = codeTransformer({ instrumentations: SENTRY_INSTRUMENTATIONS });
const codeTransformerArray: UnknownPlugin[] = Array.isArray(codeTransformerPlugins)
? codeTransformerPlugins
: [codeTransformerPlugins];
return [bundlerMarkerPlugin(), ...codeTransformerArray];
return [
bundlerMarkerPlugin(),
...(options.registerIntegrations ? [registerIntegrationsPlugin()] : []),
...codeTransformerArray,
];
}

// Id of the virtual registration module the plugin injects. The `\0` prefix on
// the resolved id is the Rollup/Vite convention that marks a module as
// synthetic, so no other plugin (or the file system) tries to resolve it.
const REGISTER_MODULE_ID = 'virtual:@sentry/orchestrion-register-integrations';
const RESOLVED_REGISTER_MODULE_ID = `\0${REGISTER_MODULE_ID}`;

function registerIntegrationsPlugin(): UnknownPlugin {
// The slices of Vite's environment-API / Rollup plugin context we read; typed
// structurally since we don't import `vite`/`rollup` types here (see note at
// the top of the file).
interface TransformContext {
environment?: { config?: { consumer?: string } };
getModuleInfo?: (id: string) => { isEntry?: boolean } | null;
}

return {
name: 'sentry-orchestrion-register-integrations',
resolveId(id: string): string | null {
return id === REGISTER_MODULE_ID ? RESOLVED_REGISTER_MODULE_ID : null;
},
load(id: string): { code: string; moduleSideEffects: boolean } | null {
if (id !== RESOLVED_REGISTER_MODULE_ID) return null;
// For now this registers every channel integration. `@sentry/server-utils`
// is `sideEffects: false`, so a future selective version only has to import
// the specific factories it needs here and the rest tree-shakes out of the
// bundle — without the app having to publish or wire up anything.
return {
code: [
"import { registerChannelIntegrations } from '@sentry/server-utils/orchestrion';",
'registerChannelIntegrations();',
'',
].join('\n'),
// The injected import is a bare side-effect import; keep the module so
// the `registerChannelIntegrations()` call — the whole point — survives.
moduleSideEffects: true,
};
},
transform(this: TransformContext | undefined, code: string, id: string): { code: string; map: unknown } | null {
// Client bundles must never pull in a server SDK's integrations; without
// environment info (classic non-environment-API Vite) assume server.
if (this?.environment?.config?.consumer === 'client') return null;
// Inject into the app entry only — hoisting guarantees registration runs
// before the entry body (and thus before `Sentry.init()`), whether the
// entry inits directly or re-exports a module that does. Matching every
// module that imports the SDK instead would scatter redundant imports
// across the graph (e.g. any file using `Sentry.startSpan`).
if (!this?.getModuleInfo?.(id)?.isEntry) return null;
if (code.includes(REGISTER_MODULE_ID)) return null;
const ms = new MagicString(code);
// Appending keeps existing sourcemap lines intact; ESM hoisting makes the
// import's position irrelevant, and registration only has to happen by
// the time `init()` runs, not before the entry body evaluates.
ms.append(`\nimport ${JSON.stringify(REGISTER_MODULE_ID)};\n`);
return { code: ms.toString(), map: ms.generateMap({ hires: true }) };
},
};
}

function bundlerMarkerPlugin(): UnknownPlugin {
Expand Down
20 changes: 19 additions & 1 deletion packages/server-utils/src/orchestrion/detect.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { Integration } from '@sentry/core';
import { debug } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';

declare global {
// eslint-disable-next-line no-var
var __SENTRY_ORCHESTRION__: { runtime?: boolean; bundler?: boolean } | undefined;
var __SENTRY_ORCHESTRION__:
| { runtime?: boolean; bundler?: boolean; integrations?: Array<() => Integration> }
| undefined;
}

/**
Expand All @@ -20,6 +23,21 @@ export function isOrchestrionInjected(): boolean {
return !!(marker?.runtime || marker?.bundler);
}

/**
* Returns fresh instances of every channel-subscriber integration an injector
* registered on the global marker (e.g. the registration module that the
* `@sentry/cloudflare/vite` plugin injects into the worker bundle).
*
* SDKs that can't afford to ship the subscriber code unconditionally read the
* registry through this function instead of importing the integrations: no
* static import means bundlers drop the integration code entirely unless the
* injector put its registration module — and with it the integrations — into
* the bundle.
*/
export function getRegisteredChannelIntegrations(): Integration[] {
return (globalThis.__SENTRY_ORCHESTRION__?.integrations || []).map(factory => factory());
}

/**
* Verifies that the diagnostics channels have been injected either by the
* runtime `--import` hook (or init-time registration), a bundler plugin, or
Expand Down
17 changes: 16 additions & 1 deletion packages/server-utils/src/orchestrion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { postgresChannelIntegration } from '../integrations/tracing-channel/post
import { postgresJsChannelIntegration } from '../integrations/tracing-channel/postgres-js';
import { vercelAiChannelIntegration } from '../integrations/tracing-channel/vercel-ai';

export { detectOrchestrionSetup, isOrchestrionInjected } from './detect';
export { detectOrchestrionSetup, getRegisteredChannelIntegrations, isOrchestrionInjected } from './detect';
export {
anthropicChannelIntegration,
googleGenAIChannelIntegration,
Expand Down Expand Up @@ -49,3 +49,18 @@ export const channelIntegrations = {
vercelAiIntegration: vercelAiChannelIntegration,
hapiIntegration: hapiChannelIntegration,
} as const;

/**
* Puts the factories of all channel integrations onto the global orchestrion
* marker, where `getRegisteredChannelIntegrations()` picks them up.
*
* Only meant to be called from a bundler-injected registration module (e.g.
* `@sentry/cloudflare/orchestrion`, injected by the `@sentry/cloudflare/vite`
* plugin) — calling it statically from an SDK would defeat the whole point of
* the registry, which is keeping the integration code out of bundles that the
* injecting plugin never touched.
*/
export function registerChannelIntegrations(): void {
const marker = (globalThis.__SENTRY_ORCHESTRION__ = globalThis.__SENTRY_ORCHESTRION__ || {});
marker.integrations = Object.values(channelIntegrations);
}
5 changes: 4 additions & 1 deletion packages/server-utils/src/orchestrion/runtime/register.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Integration } from '@sentry/core';
import { debug } from '@sentry/core';
import { createRequire } from 'node:module';
import * as Module from 'node:module';
Expand All @@ -7,7 +8,9 @@ import { SENTRY_INSTRUMENTATIONS } from '../config';

declare global {
// eslint-disable-next-line no-var
var __SENTRY_ORCHESTRION__: { runtime?: boolean; bundler?: boolean } | undefined;
var __SENTRY_ORCHESTRION__:
| { runtime?: boolean; bundler?: boolean; integrations?: Array<() => Integration> }
| undefined;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getRegisteredChannelIntegrations } from '../../src/orchestrion/detect';
import { channelIntegrations, registerChannelIntegrations } from '../../src/orchestrion/index';

describe('channel-integration registry', () => {
beforeEach(() => {
delete globalThis.__SENTRY_ORCHESTRION__;
});

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

describe('getRegisteredChannelIntegrations', () => {
it('returns an empty array when no marker exists', () => {
expect(getRegisteredChannelIntegrations()).toEqual([]);
});

it('returns an empty array when the marker has no integrations', () => {
globalThis.__SENTRY_ORCHESTRION__ = { bundler: true };
expect(getRegisteredChannelIntegrations()).toEqual([]);
});

it('instantiates each registered factory', () => {
globalThis.__SENTRY_ORCHESTRION__ = {
integrations: [() => ({ name: 'FirstIntegration' }), () => ({ name: 'SecondIntegration' })],
};

expect(getRegisteredChannelIntegrations().map(i => i.name)).toEqual(['FirstIntegration', 'SecondIntegration']);
});

it('returns fresh instances on every call', () => {
globalThis.__SENTRY_ORCHESTRION__ = { integrations: [() => ({ name: 'FirstIntegration' })] };

const [first] = getRegisteredChannelIntegrations();
const [second] = getRegisteredChannelIntegrations();

expect(first).not.toBe(second);
expect(first?.name).toBe(second?.name);
});
});

describe('registerChannelIntegrations', () => {
it('registers a factory for every canonical channel integration', () => {
registerChannelIntegrations();

const registered = getRegisteredChannelIntegrations();
expect(registered).toHaveLength(Object.keys(channelIntegrations).length);
expect(registered.every(i => typeof i.name === 'string' && i.name.length > 0)).toBe(true);
});

it('creates the marker when none exists', () => {
registerChannelIntegrations();

expect(globalThis.__SENTRY_ORCHESTRION__?.integrations).toBeDefined();
});

it('preserves existing marker fields', () => {
globalThis.__SENTRY_ORCHESTRION__ = { bundler: true, runtime: true };

registerChannelIntegrations();

expect(globalThis.__SENTRY_ORCHESTRION__?.bundler).toBe(true);
expect(globalThis.__SENTRY_ORCHESTRION__?.runtime).toBe(true);
expect(getRegisteredChannelIntegrations().length).toBeGreaterThan(0);
});

it('registers factories, not eagerly-built instances', () => {
registerChannelIntegrations();

expect(globalThis.__SENTRY_ORCHESTRION__?.integrations?.every(factory => typeof factory === 'function')).toBe(
true,
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, expect, it } from 'vitest';
import { sentryOrchestrionPlugin } from '../../src/orchestrion/bundler/vite';

const REGISTER_MODULE_ID = 'virtual:@sentry/orchestrion-register-integrations';
const RESOLVED_REGISTER_MODULE_ID = `\0${REGISTER_MODULE_ID}`;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getRegisterPlugin(plugins: any[]): any {
return plugins.find(p => p.name === 'sentry-orchestrion-register-integrations');
}

// A Vite/Rollup plugin `this` context that reports `id` as an entry (or not),
// optionally in a given environment consumer ('client' | 'server').
function ctx({ isEntry = true, consumer }: { isEntry?: boolean; consumer?: string } = {}): unknown {
return {
getModuleInfo: () => ({ isEntry }),
...(consumer ? { environment: { config: { consumer } } } : {}),
};
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function runTransform(plugin: any, code: string, context: unknown = ctx()): { code: string; map: unknown } | null {
return plugin.transform.call(context, code, 'entry.js');
}

describe('sentryOrchestrionPlugin — registerIntegrations', () => {
it('omits the register plugin by default', () => {
expect(getRegisterPlugin(sentryOrchestrionPlugin())).toBeUndefined();
});

it('omits the register plugin when registerIntegrations is false', () => {
expect(getRegisterPlugin(sentryOrchestrionPlugin({ registerIntegrations: false }))).toBeUndefined();
});

it('includes the register plugin when registerIntegrations is true', () => {
expect(getRegisterPlugin(sentryOrchestrionPlugin({ registerIntegrations: true }))).toBeDefined();
});

describe('virtual registration module', () => {
const plugin = getRegisterPlugin(sentryOrchestrionPlugin({ registerIntegrations: true }));

it('resolves the virtual id to the synthetic (\\0-prefixed) id', () => {
expect(plugin.resolveId(REGISTER_MODULE_ID)).toBe(RESOLVED_REGISTER_MODULE_ID);
});

it('does not resolve unrelated ids', () => {
expect(plugin.resolveId('some-other-module')).toBeNull();
});

it('loads a module that registers the channel integrations, kept as a side effect', () => {
const result = plugin.load(RESOLVED_REGISTER_MODULE_ID);

expect(result?.code).toContain("from '@sentry/server-utils/orchestrion'");
expect(result?.code).toContain('registerChannelIntegrations()');
expect(result?.moduleSideEffects).toBe(true);
});

it('does not reference any specific SDK package (SDK-agnostic)', () => {
expect(plugin.load(RESOLVED_REGISTER_MODULE_ID)?.code).not.toContain('@sentry/cloudflare');
});

it('does not load unrelated ids', () => {
expect(plugin.load('some-other-module')).toBeNull();
});
});

describe('transform', () => {
const plugin = getRegisterPlugin(sentryOrchestrionPlugin({ registerIntegrations: true }));

it('injects the virtual registration import into the entry module', () => {
const result = runTransform(plugin, 'export default {};\n', ctx({ isEntry: true }));

expect(result?.code).toContain(`import "${REGISTER_MODULE_ID}";`);
expect(result?.map).toBeTruthy();
});

it('injects into a re-export entry that never names the SDK', () => {
const result = runTransform(plugin, `export { default } from './worker';\n`, ctx({ isEntry: true }));

expect(result?.code).toContain(`import "${REGISTER_MODULE_ID}";`);
});

it('does not inject into non-entry modules', () => {
const code = `import * as Sentry from '@sentry/cloudflare';\nSentry.startSpan({}, () => {});\n`;
expect(runTransform(plugin, code, ctx({ isEntry: false }))).toBeNull();
});

it('does not double-inject when the virtual module is already imported', () => {
const code = `export default {};\nimport "${REGISTER_MODULE_ID}";\n`;
expect(runTransform(plugin, code, ctx({ isEntry: true }))).toBeNull();
});

it('does not inject into client-environment entries', () => {
expect(runTransform(plugin, 'export default {};\n', ctx({ isEntry: true, consumer: 'client' }))).toBeNull();
});

it('injects into server-environment entries', () => {
const result = runTransform(plugin, 'export default {};\n', ctx({ isEntry: true, consumer: 'server' }));

expect(result?.code).toContain(`import "${REGISTER_MODULE_ID}";`);
});

it('assumes server when no environment info is available (classic Vite)', () => {
const noEnvCtx = { getModuleInfo: () => ({ isEntry: true }) };
const result = runTransform(plugin, 'export default {};\n', noEnvCtx);

expect(result?.code).toContain(`import "${REGISTER_MODULE_ID}";`);
});
});
});
Loading