Skip to content

Commit f20e973

Browse files
isaacsclaude
andauthored
fix(cloudflare): Do not add back channel integrations the user removed
The `@sentry/cloudflare/vite` plugin stores an integration factory for each instrumented package when the package first runs. At `init()`, the SDK adds the stored factories to the defaults. A package that runs after `init()`, for example a driver loaded with a dynamic `import()`, emits `orchestrion.module-injected`, and the SDK then called `client.addIntegration(factory())`. That handler did not check the user's options, so it ignored each way to opt out: an `integrations` function that filtered the integration out, `defaultIntegrations: false`, and a custom `defaultIntegrations` list. The user got the integration back. For the AI integrations, that can turn prompt and response capture back on. Whether it happened depended on module load order. The handler now applies the rules that `init()` applies to the defaults: - If the user set `defaultIntegrations`, the SDK defaults are off, so the handler adds nothing. - If `integrations` is a function, the handler passes the late integration through it with `getIntegrationsToSetup` and installs the result with the same name, if there is one. This calls the function again, once for each late module. If the function throws, the handler skips the integration, so the module load that emitted the event still works. - If `integrations` is an array, nothing changes. An array cannot remove a default, and `addIntegration` already skips a name the user configured. Fixes #25576 Fixes JS-3974 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 92f9004 commit f20e973

3 files changed

Lines changed: 115 additions & 6 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ Work in this release was contributed by @adityareddy-dev, @jpdefo, @Jxxunnn, @ph
9292

9393
These integrations need the vite plugin. Without it, they record nothing, and with `debug: true` the SDK logs a warning.
9494

95+
- **fix(cloudflare): Do not add back channel integrations the user removed**
96+
97+
When an instrumented package loaded after `init()` (for example through a dynamic `import()`), the SDK added its integration even if the user had removed it. This applied to an `integrations` function that filtered it out, `defaultIntegrations: false`, and a custom `defaultIntegrations` list. The SDK now applies the same rules to a late-loaded integration as to the defaults at `init()`. To do this, it calls an `integrations` function again, once for each package that loads after `init()`.
98+
9599
## 11.8.0
96100

97101
### Important Changes

‎packages/cloudflare/src/baseSdk.ts‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Integration } from '@sentry/core';
2-
import { _INTERNAL_isClientClosed, debug, getCurrentScope, setCurrentClient } from '@sentry/core';
2+
import { _INTERNAL_isClientClosed, debug, getCurrentScope, safeCallback, setCurrentClient } from '@sentry/core';
33
import {
44
consoleIntegration,
55
conversationIdIntegration,
@@ -107,7 +107,9 @@ export function initWithDefaultIntegrations(
107107
return cached;
108108
}
109109

110-
if (options.defaultIntegrations === undefined) {
110+
// `false` or a custom list turns off the SDK defaults, late-loaded ones included.
111+
const usesSdkDefaults = options.defaultIntegrations === undefined;
112+
if (usesSdkDefaults) {
111113
options.defaultIntegrations = getDefaultIntegrationsImpl(options);
112114
}
113115

@@ -153,12 +155,36 @@ export function initWithDefaultIntegrations(
153155
// lazily required on first use) stores its subscriber factory on the global
154156
// marker too late for the default-integrations snapshot above. Its injected
155157
// snippet emits this event right after storing the factory, so install the
156-
// integration on the live client here. `addIntegration` dedupes by
157-
// integration name, so already-installed integrations are no-ops.
158+
// integration on the live client here, with the same rules as the defaults.
159+
// `addIntegration` dedupes by integration name, so an integration the user
160+
// configured stays in place.
161+
const userIntegrations = options.integrations;
158162
client.on('orchestrion.module-injected', moduleName => {
159163
const factory = GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.integrations?.get(moduleName);
160-
if (factory) {
161-
client.addIntegration(factory());
164+
if (!usesSdkDefaults || !factory) {
165+
return;
166+
}
167+
168+
const integration = factory();
169+
if (typeof userIntegrations !== 'function') {
170+
client.addIntegration(integration);
171+
return;
172+
}
173+
174+
// Pass the late default through the user's `integrations` function, so a
175+
// filter that removed it at init removes it here too. This calls the
176+
// function again, once for each late module. If the function throws, skip
177+
// the integration, so the module load that emitted the event still works.
178+
const kept = safeCallback(
179+
DEBUG_BUILD ? 'Error in the `integrations` function, skipping a late-loaded integration:' : '',
180+
() =>
181+
getIntegrationsToSetup({ defaultIntegrations: [integration], integrations: userIntegrations }).find(
182+
({ name }) => name === integration.name,
183+
),
184+
() => undefined,
185+
);
186+
if (kept) {
187+
client.addIntegration(kept);
162188
}
163189
});
164190

‎packages/cloudflare/test/sdk.test.ts‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,85 @@ describe('getDefaultIntegrations', () => {
458458
expect(client?.getIntegrationByName('Mysql')).toBe(userInstance);
459459
});
460460

461+
describe('when a channel integration module loads after init', () => {
462+
const DSN = 'https://public@dsn.ingest.sentry.io/1337';
463+
464+
async function loadMysqlLate(client: CloudflareClient | undefined): Promise<void> {
465+
const { mysqlIntegration } = await import('@sentry/server-utils');
466+
globalThis.__SENTRY_ORCHESTRION__ = {
467+
bundler: new Set(['mysql']),
468+
integrations: new Map([['mysql', mysqlIntegration]]),
469+
};
470+
client?.emit('orchestrion.module-injected', 'mysql');
471+
}
472+
473+
test('does not add it when the integrations function filtered it out', async () => {
474+
resetSdk();
475+
const client = init({
476+
dsn: DSN,
477+
integrations: defaults => defaults.filter(integration => integration.name !== 'Mysql'),
478+
});
479+
480+
await loadMysqlLate(client);
481+
482+
expect(client?.getIntegrationByName('Mysql')).toBeUndefined();
483+
});
484+
485+
test('adds it when the integrations function keeps it', async () => {
486+
resetSdk();
487+
const client = init({ dsn: DSN, integrations: defaults => defaults });
488+
489+
await loadMysqlLate(client);
490+
491+
expect(client?.getIntegrationByName('Mysql')).toBeDefined();
492+
});
493+
494+
test('keeps the instance the integrations function configured', async () => {
495+
resetSdk();
496+
const client = init({
497+
dsn: DSN,
498+
integrations: defaults => [...defaults, exportedMysqlIntegration()],
499+
});
500+
const configured = client?.getIntegrationByName('Mysql');
501+
expect(configured).toBeDefined();
502+
503+
await loadMysqlLate(client);
504+
505+
expect(client?.getIntegrationByName('Mysql')).toBe(configured);
506+
});
507+
508+
test('does not add it and does not throw when the integrations function throws', async () => {
509+
resetSdk();
510+
let calls = 0;
511+
const client = init({
512+
dsn: DSN,
513+
integrations: defaults => {
514+
calls++;
515+
if (calls > 1) {
516+
throw new Error('integrations failed');
517+
}
518+
return defaults;
519+
},
520+
});
521+
522+
await expect(loadMysqlLate(client)).resolves.toBeUndefined();
523+
524+
expect(client?.getIntegrationByName('Mysql')).toBeUndefined();
525+
});
526+
527+
test.each([
528+
['false', false as const],
529+
['a custom list', [] as Integration[]],
530+
])('does not add it when defaultIntegrations is %s', async (_label, defaultIntegrations) => {
531+
resetSdk();
532+
const client = init({ dsn: DSN, defaultIntegrations });
533+
534+
await loadMysqlLate(client);
535+
536+
expect(client?.getIntegrationByName('Mysql')).toBeUndefined();
537+
});
538+
});
539+
461540
test('keeps a channel integration the user configured when its module loads after init', async () => {
462541
resetSdk();
463542
const { mysqlIntegration } = await import('@sentry/server-utils');

0 commit comments

Comments
 (0)