Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// `mysql2` >= 3.20 has a registration-only orchestrion config (its tracing
// channels are native, so orchestrion doesn't wrap it — it only splices a
// module-registration snippet). Requiring `mysql2` loads its instrumented file
// (`lib/base/connection.js`), which drives the runtime module hook to transform
// it.
//
// The registration-only transform is wired into the bundler plugins only, so
// before the runtime instrumentation set excluded these configs, the runtime
// hook threw `TypeError: transform is not a function`, which the loader turned
// into an always-on "`@sentry/server-runtime-injection` was bundled ..." warning
// on stderr. `ensureNoErrorOutput` fails the test if that warning (or anything
// else) reaches stderr.
//
// A bare `import` is service-free — it never connects to a database.
await import('mysql2');
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as path from 'path';
import { afterAll, describe, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

afterAll(() => {
cleanupChildProcesses();
});

describe('orchestrion registration-only modules at runtime', () => {
// A registration-only config (a native-channel library such as `mysql2` >= 3.20)
// carries the custom `MODULE_REGISTRATION_TRANSFORM`, which is wired into the
// bundler plugins only. The runtime loader excludes these
// (`SENTRY_RUNTIME_INSTRUMENTATIONS`), so loading such a module must not attempt
// an unavailable transform — which would surface as `TypeError: transform is
// not a function` and the always-on "`@sentry/server-runtime-injection` was
// bundled ..." warning on stderr. `ensureNoErrorOutput` fails on any stderr.
test('loads a native-channel library without the transformer-unavailable warning', async () => {
await createRunner(__dirname, 'scenario.mjs')
.withInstrument(path.join(__dirname, 'instrument.mjs'))
.ensureNoErrorOutput()
.start()
.completed();
});
});
8 changes: 4 additions & 4 deletions packages/server-runtime-injection/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
import * as Module from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { SENTRY_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config';
import { SENTRY_RUNTIME_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config';
import type { register } from 'node:module';
import ModulePatch from '@apm-js-collab/tracing-hooks';
import { initialize, load, resolve, createDiagnosticsPort } from '@apm-js-collab/tracing-hooks/hook-sync.mjs';
Expand Down Expand Up @@ -126,7 +126,7 @@ export function registerDiagnosticsChannelInjection(): void {
// incompatibility) we warn and continue without channel injection.
try {
if (typeof mod.registerHooks === 'function' && stableSyncHooks) {
initialize({ instrumentations: SENTRY_INSTRUMENTATIONS });
initialize({ instrumentations: SENTRY_RUNTIME_INSTRUMENTATIONS });
mod.registerHooks({ resolve, load });
debug.log('Registered diagnostics-channel injection via Module.registerHooks()');
} else if (typeof mod.register === 'function' && !globalAny.Bun && !globalAny.Deno) {
Expand Down Expand Up @@ -185,7 +185,7 @@ export function registerDiagnosticsChannelInjection(): void {

mod.register(hookSpecifier, {
parentURL,
data: { instrumentations: SENTRY_INSTRUMENTATIONS, diagnosticsPort },
data: { instrumentations: SENTRY_RUNTIME_INSTRUMENTATIONS, diagnosticsPort },
transferList: [diagnosticsPort],
});

Expand All @@ -194,7 +194,7 @@ export function registerDiagnosticsChannelInjection(): void {
// are resolved through the CJS machinery and never reach the ESM
// register hook, so without this patch the file we want to instrument
// loads untransformed.
new ModulePatch({ instrumentations: SENTRY_INSTRUMENTATIONS }).patch();
new ModulePatch({ instrumentations: SENTRY_RUNTIME_INSTRUMENTATIONS }).patch();
debug.log('Registered diagnostics-channel injection via Module.register()');
} else {
marker.runtimeUnavailable = true;
Expand Down
34 changes: 34 additions & 0 deletions packages/server-utils/src/orchestrion/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { vercelAiConfig } from './vercel-ai';
// Kept sorted alphabetically by module so concurrent additions insert at different
// points rather than all appending to the end (fewer merge conflicts).

import { MODULE_REGISTRATION_TRANSFORM } from './registration-only';

/**
* The orchestrion code-transform configs. Every instrumentable library is here
* so the transform is all-or-nothing: whenever orchestrion is enabled, all of
Expand Down Expand Up @@ -81,6 +83,38 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [
...vercelAiConfig,
];

/**
* The subset of {@link SENTRY_INSTRUMENTATIONS} the RUNTIME loader
* (`@sentry/server-runtime-injection`'s `register`, reached via `--import` or
* `Sentry.init()`) can actually apply.
*
* Registration-only configs (native-channel libraries such as `ai` v7,
* `ioredis`, `@redis/client`, `mysql2`, `mongoose`) carry the custom
* `MODULE_REGISTRATION_TRANSFORM` operator. That operator is wired into the
* BUNDLER plugins only (see `orchestrion/bundler/moduleInjectedTransform.ts`,
* applied via `bundler/options.ts`'s `customTransforms`); the runtime loader's
* `initialize()` receives no custom transforms. Attempting one of these at
* runtime therefore throws `TypeError: transform is not a function`, which the
* loader misreports as the always-on "`@sentry/server-runtime-injection` was
* bundled ... loads uninstrumented" warning even though nothing is wrong.
*
* Excluding them at runtime is correct, not just a way to silence the warning:
* these libraries publish their own tracing channels, and their integrations
* subscribe through `setupOnce()` / `waitForTracingChannelBinding`,
* independently of the module-injected snippet. That snippet only fires
* `orchestrion.module-injected`, which drives the `setup()` /
* `invokeOrchestrionInstrumentation` path; for a native-channel version that
* path subscribes to the injected `orchestrion:*` channels the library never
* publishes — a no-op. So running these at runtime would add no spans. The
* snippet earns its keep only on the BUNDLER path — notably bundler-only SDKs
* (e.g. `@sentry/cloudflare`) that discover a loaded module via that event to
* instantiate its integration factory. `@sentry/node` registers its
* integrations statically, so it does not need it.
*/
export const SENTRY_RUNTIME_INSTRUMENTATIONS: InstrumentationConfig[] = SENTRY_INSTRUMENTATIONS.filter(
config => config.transform !== MODULE_REGISTRATION_TRANSFORM,
);

/**
* The unique set of package names instrumented by `SENTRY_INSTRUMENTATIONS`
* merged with any caller-provided `instrumentations` (e.g. `['mysql']`).
Expand Down
44 changes: 44 additions & 0 deletions packages/server-utils/test/orchestrion/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
INSTRUMENTED_MODULE_NAMES,
instrumentedModuleNames,
SENTRY_INSTRUMENTATIONS,
SENTRY_RUNTIME_INSTRUMENTATIONS,
withoutInstrumentedExternals,
} from '../../src/orchestrion/config';
import { CHANNEL_INTEGRATION_DEFINITIONS } from '../../src/orchestrion/config/channel-integration-definitions';
import { MODULE_REGISTRATION_TRANSFORM } from '../../src/orchestrion/config/registration-only';

describe('orchestrion config — scoped @hapi/hapi module', () => {
it('includes the scoped @hapi/hapi name in INSTRUMENTED_MODULE_NAMES', () => {
Expand Down Expand Up @@ -55,3 +57,45 @@ describe('orchestrion config — custom instrumentations', () => {
expect(withoutInstrumentedExternals(external)).toEqual(['react', 'my-lib']);
});
});

describe('orchestrion config — SENTRY_RUNTIME_INSTRUMENTATIONS', () => {
// The runtime loader has no custom transforms, so a registration-only config
// (its `transform` is the bundler-only `MODULE_REGISTRATION_TRANSFORM`) throws
// `transform is not a function` there. These are excluded from the runtime set;
// the bundler keeps the full `SENTRY_INSTRUMENTATIONS`.
it('drops every registration-only config and keeps the rest in order', () => {
// The exclusion is only meaningful if there are registration-only configs to drop.
const registrationOnly = SENTRY_INSTRUMENTATIONS.filter(c => c.transform === MODULE_REGISTRATION_TRANSFORM);
expect(registrationOnly.length).toBeGreaterThan(0);

// None survive into the runtime set...
expect(SENTRY_RUNTIME_INSTRUMENTATIONS.some(c => c.transform === MODULE_REGISTRATION_TRANSFORM)).toBe(false);

// ...while every other config is preserved, unchanged and in order.
expect(SENTRY_RUNTIME_INSTRUMENTATIONS).toEqual(
SENTRY_INSTRUMENTATIONS.filter(c => c.transform !== MODULE_REGISTRATION_TRANSFORM),
);
});

it('excludes only the native-channel modules, and only via their registration-only config', () => {
const registrationOnlyModules = [
...new Set(
SENTRY_INSTRUMENTATIONS.filter(c => c.transform === MODULE_REGISTRATION_TRANSFORM).map(c => c.module.name),
),
].sort();

// These native-channel libraries are known to use a registration-only config
// today. Asserted as a subset (not the exact set) so adding another such
// library does not break this test.
expect(registrationOnlyModules).toEqual(
expect.arrayContaining(['@redis/client', 'ai', 'ioredis', 'mongoose', 'mysql2']),
);

// The exclusion is per-config, not per-module: a module with both a
// registration-only (native) config and older transform-based configs keeps
// the latter at runtime. `ai` (v7 native + v4–6 transforms) is one such case.
const runtimeAiConfigs = SENTRY_RUNTIME_INSTRUMENTATIONS.filter(c => c.module.name === 'ai');
expect(runtimeAiConfigs.length).toBeGreaterThan(0);
expect(runtimeAiConfigs.every(c => c.transform !== MODULE_REGISTRATION_TRANSFORM)).toBe(true);
});
});
Loading