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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -164,7 +164,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 @@ -223,7 +223,7 @@ export function registerDiagnosticsChannelInjection(): void {

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

Expand All @@ -232,7 +232,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
32 changes: 32 additions & 0 deletions packages/server-utils/src/orchestrion/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,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,
);

/**
* The unique set of package names instrumented by `SENTRY_INSTRUMENTATIONS`
* merged with any caller-provided `instrumentations` (e.g. `['mysql']`).
Expand Down
50 changes: 50 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,51 @@ 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('only keeps configs the transform-less runtime loader can actually apply', () => {
// The filter is only meaningful if some configs carry a custom transform to drop.
expect(SENTRY_INSTRUMENTATIONS.some(c => c.transform)).toBe(true);

// The invariant: the runtime loader registers no custom transforms, so any config
// carrying one (not just MODULE_REGISTRATION_TRANSFORM) throws there. Every runtime
// config must therefore be transform-less — this is what we need to hold even if a
// future feature adds a different named transform.
expect(SENTRY_RUNTIME_INSTRUMENTATIONS.every(c => !c.transform)).toBe(true);
});

it('keeps every transform-less config, dropping only the ones with a custom transform', () => {
// Nothing the runtime can apply is lost: each transform-less config from the full
// set survives (by reference), and the runtime set adds nothing extra.
for (const config of SENTRY_INSTRUMENTATIONS.filter(c => !c.transform)) {
expect(SENTRY_RUNTIME_INSTRUMENTATIONS).toContain(config);
}
expect(SENTRY_RUNTIME_INSTRUMENTATIONS).toHaveLength(SENTRY_INSTRUMENTATIONS.filter(c => !c.transform).length);
});

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)).toBe(true);
});
});
Loading