Skip to content

Commit f0bb82f

Browse files
timfishJPeer264
authored andcommitted
Only inject registration code. Don't inject tracing channels.
1 parent ddde71b commit f0bb82f

8 files changed

Lines changed: 91 additions & 35 deletions

File tree

‎packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CustomTransform } from '../apmTypes';
22
import { parse } from 'meriyah';
33
import { subscriberExportForModule } from '../config/channel-integration-definitions';
4+
import { MODULE_REGISTRATION_TRANSFORM } from '../config/registration-only';
45

56
// Tracks Program nodes we already injected into, so a package with several
67
// instrumented files (or several configs pointing at one file) is injected only
@@ -75,6 +76,10 @@ function moduleInjectedSnippet(
7576
* per file. Requires `@apm-js-collab/code-transformer` >= 0.18.1, where
7677
* built-ins dispatch through the override map and expose the originals on
7778
* `state.transforms.defaults`.
79+
*
80+
* Also carries the registration-only operator, which splices the same snippet
81+
* without any channel injection — for library versions whose tracing channels
82+
* are native.
7883
*/
7984
export function moduleInjectedTransforms(
8085
// A function is read per injected file — the webpack/Turbopack loader uses it
@@ -83,15 +88,12 @@ export function moduleInjectedTransforms(
8388
// importing file's location).
8489
importSpecifier?: string | (() => string | undefined),
8590
): Record<string, CustomTransform> {
86-
const injectModuleInjected: CustomTransform = (state, program, parent, ancestry) => {
87-
const { moduleType, module, transforms } = state as {
91+
const spliceModuleInjected = (state: unknown, program: unknown): void => {
92+
const { moduleType, module } = state as {
8893
moduleType?: string;
8994
module?: { name?: string };
90-
transforms: { defaults: { tracingChannelImport: CustomTransform } };
9195
};
9296

93-
transforms.defaults.tracingChannelImport(state, program, parent, ancestry);
94-
9597
const node = program as ProgramNode;
9698
if (injectedPrograms.has(node)) {
9799
return;
@@ -116,5 +118,25 @@ export function moduleInjectedTransforms(
116118
node.body.splice(directiveIndex + 1, 0, ...statements);
117119
};
118120

119-
return { tracingChannelImport: injectModuleInjected };
121+
const injectModuleInjected: CustomTransform = (state, program, parent, ancestry) => {
122+
const { transforms } = state as {
123+
transforms: { defaults: { tracingChannelImport: CustomTransform } };
124+
};
125+
126+
transforms.defaults.tracingChannelImport(state, program, parent, ancestry);
127+
spliceModuleInjected(state, program);
128+
};
129+
130+
// Operator for registration-only configs (`transform` field, see
131+
// `config/registration-only.ts`): dispatched INSTEAD of `traceSync`, so no
132+
// channel is declared and no function is wrapped. Their `astQuery: 'Program'`
133+
// matches only the file root, so `node` is the Program itself.
134+
const injectRegistrationOnly: CustomTransform = (state, node) => {
135+
spliceModuleInjected(state, node);
136+
};
137+
138+
return {
139+
tracingChannelImport: injectModuleInjected,
140+
[MODULE_REGISTRATION_TRANSFORM]: injectRegistrationOnly,
141+
};
120142
}

‎packages/server-utils/src/orchestrion/config/ioredis.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { getModuleNames } from './module-names';
33
import { registrationOnly } from './registration-only';
44

55
export const ioredisConfig = [
6-
registrationOnly(
7-
{ name: 'ioredis', versionRange: '>=5.11.0', filePath: 'built/Redis.js' },
8-
{ className: 'Redis', methodName: 'connect' },
9-
),
6+
registrationOnly({ name: 'ioredis', versionRange: '>=5.11.0', filePath: 'built/Redis.js' }),
107
// ioredis `<5.11.0` (>=5.11.0 publishes its own `ioredis:*` diagnostics_channel)
118
...['lib/redis.js', 'built/redis.js', 'built/redis/index.js'].flatMap((filePath): InstrumentationConfig[] => [
129
{

‎packages/server-utils/src/orchestrion/config/mongoose.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ const CONTEXT_CAPTURE_QUERY_METHODS = [
3838
] as const;
3939

4040
export const mongooseConfig = [
41-
registrationOnly(
42-
{ name: 'mongoose', versionRange: '>=9.7.0', filePath: 'lib/query.js' },
43-
{ expressionName: 'estimatedDocumentCount' },
44-
),
41+
registrationOnly({ name: 'mongoose', versionRange: '>=9.7.0', filePath: 'lib/query.js' }),
4542
// Query execution
4643
// the span for most read/write operations. `op`, collection and model are
4744
// read off the `Query` at exec time.

‎packages/server-utils/src/orchestrion/config/mysql2.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ import { registrationOnly } from './registration-only';
2222
// guard) — so `Auto` would crash streamed queries. `Callback` leaves that shape untouched (a rare,
2323
// row-streaming use that consumes the emitter's events), the tradeoff being it isn't traced.
2424
export const mysql2Config = [
25-
registrationOnly(
26-
{ name: 'mysql2', versionRange: '>=3.20.0', filePath: 'lib/base/connection.js' },
27-
{ className: 'BaseConnection', methodName: 'pause' },
28-
),
25+
registrationOnly({ name: 'mysql2', versionRange: '>=3.20.0', filePath: 'lib/base/connection.js' }),
2926
{
3027
channelName: 'query',
3128
module: { name: 'mysql2', versionRange: '>=1.4.2 <3.11.5', filePath: 'lib/connection.js' },

‎packages/server-utils/src/orchestrion/config/redis.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { getModuleNames } from './module-names';
33
import { registrationOnly } from './registration-only';
44

55
export const redisConfig = [
6-
registrationOnly(
7-
{ name: '@redis/client', versionRange: '>=5.12.0', filePath: 'dist/lib/client/index.js' },
8-
{ className: 'RedisClient', methodName: 'connect' },
9-
),
6+
registrationOnly({ name: '@redis/client', versionRange: '>=5.12.0', filePath: 'dist/lib/client/index.js' }),
107
// redis `>=2.6.0 <4` (standalone `redis`). `internal_send_command` is an
118
// anonymous prototype assignment (`expressionName`); it settles via the nested
129
// `command_obj.callback`, so `kind: 'Sync'` and the subscriber wraps that callback.
Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
import type { InstrumentationConfig } from '..';
22

33
/**
4-
* Build a registration-only config for one file of a library.
5-
*
6-
* `functionQuery` takes the same shapes as a normal config (`functionName`,
7-
* `expressionName`, or `className` + `methodName`); the `kind` is fixed.
4+
* Name of the custom transform (see `bundler/moduleInjectedTransform.ts`) that
5+
* splices only the module-injected registration snippet — no channel machinery.
6+
* Prefixed so it can never shadow a built-in operator (`traceSync`,
7+
* `tracingChannelImport`, ...), which the custom-transforms map overrides by
8+
* name.
89
*/
9-
export function registrationOnly(
10-
module: { name: string; versionRange: string; filePath: string },
11-
functionQuery: { functionName?: string; expressionName?: string; className?: string; methodName?: string },
12-
): InstrumentationConfig {
10+
export const MODULE_REGISTRATION_TRANSFORM = 'sentryModuleRegistration';
11+
12+
/**
13+
* Build a registration-only config for one file of a library whose tracing
14+
* channels are native (published by the library itself): no channels are
15+
* injected, but transforming the file registers the module's channel-subscriber
16+
* integration at evaluation time. `astQuery: 'Program'` matches the file root
17+
* unconditionally, so no anchor function inside the library needs to exist.
18+
*/
19+
export function registrationOnly(module: {
20+
name: string;
21+
versionRange: string;
22+
filePath: string;
23+
}): InstrumentationConfig {
1324
return {
1425
channelName: 'module-registration',
1526
module,
16-
functionQuery: { ...functionQuery, kind: 'Sync' },
17-
} as InstrumentationConfig;
27+
astQuery: 'Program',
28+
transform: MODULE_REGISTRATION_TRANSFORM,
29+
};
1830
}

‎packages/server-utils/src/orchestrion/config/vercel-ai.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { registrationOnly } from './registration-only';
44

55
export const vercelAiConfig = [
66
...['dist/index.js', 'dist/index.mjs'].map(filePath =>
7-
registrationOnly({ name: 'ai', versionRange: '>=7.0.0', filePath }, { functionName: 'embedMany' }),
7+
registrationOnly({ name: 'ai', versionRange: '>=7.0.0', filePath }),
88
),
99
// Vercel AI v6: mirror the v7 native `ai:telemetry` channel by injecting
1010
// channels into the top-level entry points. `resolveLanguageModel` is wrapped

‎packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import { orchestrionTransformOptions } from '../../src/orchestrion/bundler/optio
1212

1313
// The code transformer reads the instrumented package's version from its
1414
// on-disk `package.json`, so each test package needs a real directory.
15-
function makePackage(root: string, name: string, version: string, type?: 'module' | 'commonjs'): void {
15+
function makePackage(root: string, name: string, version: string, type?: 'module' | 'commonjs', subdir = 'lib'): void {
1616
const dir = join(root, 'node_modules', name);
17-
mkdirSync(join(dir, 'lib'), { recursive: true });
17+
mkdirSync(join(dir, subdir), { recursive: true });
1818
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name, version, ...(type ? { type } : {}) }));
1919
}
2020

@@ -44,6 +44,8 @@ describe('module-injected transform', () => {
4444
makePackage(root, 'mysql', '2.18.1', 'commonjs');
4545
makePackage(root, 'pg', '8.11.0', 'module');
4646
makePackage(root, 'my-lib', '1.0.0', 'commonjs');
47+
makePackage(root, 'ioredis', '5.11.0', 'commonjs', 'built');
48+
makePackage(root, 'ai', '7.0.0', 'module', 'dist');
4749
});
4850

4951
afterAll(() => {
@@ -124,6 +126,38 @@ describe('module-injected transform', () => {
124126
expect(calls).toHaveLength(1);
125127
});
126128

129+
it('injects only the registration snippet for a CJS version with native channels', () => {
130+
const t = createCodeTransformer(orchestrionTransformOptions({}));
131+
// No anchor function present — registration must not depend on any library
132+
// internals existing (`astQuery: 'Program'` matches the file root).
133+
const code = "'use strict';\nclass Redis {}\nmodule.exports = Redis;\n";
134+
const result = t.transform(code, join(root, 'node_modules/ioredis/built/Redis.js'));
135+
136+
expect(result).not.toBeNull();
137+
expect(result!.code).toMatch(
138+
/const\s*\{\s*orchestrionModuleInjected,\s*redisIntegration\s*\}\s*=\s*require\(["']@sentry\/server-utils\/orchestrion["']\)/,
139+
);
140+
expect(result!.code).toContain('orchestrionModuleInjected("ioredis", redisIntegration)');
141+
// The library publishes its own channels, so nothing else is injected: no
142+
// diagnostics_channel import, no channel declaration, no function wrapper.
143+
expect(result!.code).not.toContain('diagnostics_channel');
144+
expect(result!.code).not.toContain('tr_ch_apm');
145+
expect(result!.code).toContain('class Redis');
146+
});
147+
148+
it('injects only the registration snippet for an ESM version with native channels', () => {
149+
const t = createCodeTransformer(orchestrionTransformOptions({}));
150+
const result = t.transform('export const embed = () => {};\n', join(root, 'node_modules/ai/dist/index.mjs'));
151+
152+
expect(result).not.toBeNull();
153+
expect(result!.code).toMatch(
154+
/import\s*\{\s*orchestrionModuleInjected,\s*vercelAIIntegration\s*\}\s*from\s*["']@sentry\/server-utils\/orchestrion["']/,
155+
);
156+
expect(result!.code).toContain('orchestrionModuleInjected("ai", vercelAIIntegration)');
157+
expect(result!.code).not.toContain('diagnostics_channel');
158+
expect(result!.code).not.toContain('tr_ch_apm');
159+
});
160+
127161
it('honors a custom import specifier (Turbopack passes an absolute path)', () => {
128162
const t = createCodeTransformer({
129163
...orchestrionTransformOptions({}),

0 commit comments

Comments
 (0)