Closed
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 numberDiff line numberDiff line change
Expand Up@@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All@@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand DownExpand Up@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
24 changes: 9 additions & 15 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand DownExpand Up@@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand DownExpand Up@@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand DownExpand Up@@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All@@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
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 numberDiff line numberDiff line change
Expand Up@@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All@@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand DownExpand Up@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
24 changes: 9 additions & 15 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand DownExpand Up@@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand DownExpand Up@@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand DownExpand Up@@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All@@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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 numberDiff line numberDiff line change
Expand Up@@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All@@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand DownExpand Up@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
24 changes: 9 additions & 15 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand DownExpand Up@@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand DownExpand Up@@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand DownExpand Up@@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All@@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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 numberDiff line numberDiff line change
Expand Up@@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All@@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand DownExpand Up@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
24 changes: 9 additions & 15 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand DownExpand Up@@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand DownExpand Up@@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand DownExpand Up@@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All@@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
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 numberDiff line numberDiff line change
Expand Up@@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All@@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand DownExpand Up@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
24 changes: 9 additions & 15 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand DownExpand Up@@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand DownExpand Up@@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand DownExpand Up@@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All@@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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 numberDiff line numberDiff line change
Expand Up@@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All@@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand DownExpand Up@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
24 changes: 9 additions & 15 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand DownExpand Up@@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand DownExpand Up@@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand DownExpand Up@@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All@@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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 numberDiff line numberDiff line change
Expand Up@@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All@@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand DownExpand Up@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
24 changes: 9 additions & 15 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand DownExpand Up@@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand DownExpand Up@@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand DownExpand Up@@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All@@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
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 numberDiff line numberDiff line change
Expand Up@@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All@@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand DownExpand Up@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
24 changes: 9 additions & 15 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand DownExpand Up@@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand DownExpand Up@@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand DownExpand Up@@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All@@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading