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
71 changes: 8 additions & 63 deletions packages/cloudflare/src/vite/flueRuntime.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import MagicString from 'magic-string';

// Namespace binding the injected provider import uses; read back by the integration
// off the global marker.
const PROVIDER_IDENTIFIER = '__SENTRY_FLUE_RUNTIME__';

const FLUE_MODULE = '@flue/runtime';

// The bundled `@sentry/server-utils` Flue integration module (ESM build — the only one a
// worker loads). It reads `@flue/runtime` off the global marker this provider populates,
// because `instrument()` registers into module-scope state no channel payload can carry.
const FLUE_INTEGRATION_ID = /@sentry\/server-utils\/build\/esm\/integrations\/flue\.js$/;

/** Whether `id` is the Sentry Flue integration module the provider injects into. */
export function isFlueIntegrationModuleId(id: string): boolean {
const normalizedId = id.replace(/\\/g, '/').replace(/[?#].*$/, '');
return FLUE_INTEGRATION_ID.test(normalizedId);
}
import type { ProvidedModulePlugin } from './providedModulePlugin';
import { createProvidedModulePlugin } from './providedModulePlugin';

/**
* Splices a static `import * as … from '@flue/runtime'` into Sentry's own Flue integration module
Expand All @@ -28,48 +10,11 @@ export function isFlueIntegrationModuleId(id: string): boolean {
* user passes it by calling `instrument()` themselves; a bundled worker has no `node_modules` to
* resolve from, so it is supplied at build time instead.
*/
export function sentryFlueRuntimeProviderPlugin(): {
name: string;
configResolved(config: { root: string }): void;
transform(code: string, id: string): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined;
} {
let providerSnippet: string | undefined;

return {
export function sentryFlueRuntimeProviderPlugin(): ProvidedModulePlugin {
return createProvidedModulePlugin({
name: 'sentry-cloudflare-flue-runtime-provider',

configResolved(config: { root: string }): void {
// Build-time only; never ships to the worker. Probed with CJS resolution, which an ESM-only
// `@flue/runtime` fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` — so only a module-not-found
// counts as absent, and any other failure still injects and lets Vite report it. Not
// `import.meta.resolve`: `parentURL` is ignored without a flag, and it is absent from the
// CJS build.
try {
createRequire(resolve(config.root, 'noop.js')).resolve(FLUE_MODULE);
} catch (error) {
const code = (error as NodeJS.ErrnoException | undefined)?.code;
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND') {
return;
}
}
// A getter where Mastra assigns: the bundler may evaluate Sentry's module before
// `@flue/runtime` is initialized, and assigning there would store `undefined`.
providerSnippet =
`import * as ${PROVIDER_IDENTIFIER} from '${FLUE_MODULE}';\n` +
'(globalThis.__SENTRY_ORCHESTRION__ = globalThis.__SENTRY_ORCHESTRION__ || {});\n' +
'(globalThis.__SENTRY_ORCHESTRION__.providedModules = globalThis.__SENTRY_ORCHESTRION__.providedModules || {});\n' +
`Object.defineProperty(globalThis.__SENTRY_ORCHESTRION__.providedModules, '${FLUE_MODULE}', ` +
`{ configurable: true, enumerable: true, get() { return ${PROVIDER_IDENTIFIER}; } });\n`;
},

transform(code: string, id: string): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined {
// `code.includes` keeps this idempotent: a second pass over already-injected output would
// otherwise emit a duplicate `import * as` binding, which is a syntax error.
if (!providerSnippet || !isFlueIntegrationModuleId(id) || code.includes(PROVIDER_IDENTIFIER)) return undefined;

const ms = new MagicString(code);
ms.prepend(providerSnippet);
return { code: ms.toString(), map: ms.generateMap({ hires: true }) };
},
};
moduleName: '@flue/runtime',
identifier: '__SENTRY_FLUE_RUNTIME__',
integrationModule: 'flue',
});
}
74 changes: 14 additions & 60 deletions packages/cloudflare/src/vite/mastraObservability.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,20 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import MagicString from 'magic-string';

// Namespace binding the injected provider import uses; read back by the integration
// off the global marker.
const PROVIDER_IDENTIFIER = '__SENTRY_MASTRA_OBSERVABILITY__';

// The bundled `@sentry/server-utils` Mastra integration module (ESM build — the only
// one a worker loads). Its `loadMastraObservability` reads `@mastra/observability` off
// the global marker this provider populates, instead of `createRequire`, which cannot
// resolve a package inside a bundled worker.
const MASTRA_INTEGRATION_ID = /@sentry\/server-utils\/build\/esm\/integrations\/mastra\.js$/;

/** Whether `id` is the Sentry Mastra integration module the provider injects into. */
export function isMastraIntegrationModuleId(id: string): boolean {
const normalizedId = id.replace(/\\/g, '/').replace(/[?#].*$/, '');
return MASTRA_INTEGRATION_ID.test(normalizedId);
}
import type { ProvidedModulePlugin } from './providedModulePlugin';
import { createProvidedModulePlugin } from './providedModulePlugin';

/**
* Splices a static `import * as … from '@mastra/observability'` into Sentry's own
* Mastra integration module and stashes the namespace on the global orchestrion
* marker.
* Splices a static `import * as … from '@mastra/observability'` into Sentry's own Mastra
* integration module and stashes the namespace on the global orchestrion marker.
*
* On Cloudflare the integration cannot `createRequire('@mastra/observability')` to
* bootstrap Mastra's observability pipeline — there is no on-disk `node_modules` in
* workerd — so without this the user has to construct and wire up an `Observability`
* themselves. The import is static (statically analyzable, no lazy `import()`), lands
* in Sentry's module rather than the user's code, and is only emitted when the package
* actually resolves; if it is absent, the integration keeps its Node `createRequire`
* fallback and the marker stays empty.
* On Cloudflare the integration cannot `createRequire('@mastra/observability')` to bootstrap
* Mastra's observability pipeline — there is no on-disk `node_modules` in workerd — so without
* this the user has to construct and wire up an `Observability` themselves. If the package is
* absent, the integration keeps its Node `createRequire` fallback and the marker stays empty.
*/
export function sentryMastraObservabilityProviderPlugin(): {
name: string;
configResolved(config: { root: string }): void;
transform(code: string, id: string): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined;
} {
let providerSnippet: string | undefined;

return {
export function sentryMastraObservabilityProviderPlugin(): ProvidedModulePlugin {
return createProvidedModulePlugin({
name: 'sentry-cloudflare-mastra-observability-provider',

configResolved(config: { root: string }): void {
// Resolved at build time (Node), so this `createRequire` never ships to the worker.
try {
createRequire(resolve(config.root, 'noop.js')).resolve('@mastra/observability');
} catch {
return;
}
providerSnippet =
`import * as ${PROVIDER_IDENTIFIER} from '@mastra/observability';\n` +
'(globalThis.__SENTRY_ORCHESTRION__ = globalThis.__SENTRY_ORCHESTRION__ || {});\n' +
'(globalThis.__SENTRY_ORCHESTRION__.providedModules = globalThis.__SENTRY_ORCHESTRION__.providedModules || {})' +
`['@mastra/observability'] = ${PROVIDER_IDENTIFIER};\n`;
},

transform(code: string, id: string): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined {
if (!providerSnippet || !isMastraIntegrationModuleId(id)) return undefined;

const ms = new MagicString(code);
ms.prepend(providerSnippet);
return { code: ms.toString(), map: ms.generateMap({ hires: true }) };
},
};
moduleName: '@mastra/observability',
identifier: '__SENTRY_MASTRA_OBSERVABILITY__',
integrationModule: 'mastra',
});
}
137 changes: 137 additions & 0 deletions packages/cloudflare/src/vite/providedModulePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { resolve } from 'node:path';
import MagicString from 'magic-string';

/**
* The slice of the Rollup plugin context the probe needs. Declared here rather than imported so
* this file carries no Rollup or Vite type dependency.
*/
interface ResolveContext {
resolve(
source: string,
importer?: string,
options?: { skipSelf?: boolean },
): Promise<{ id: string; external?: boolean | string } | null>;
warn(message: string): void;
}

/** The plugin shape `sentryCloudflareVitePlugin` composes. */
export interface ProvidedModulePlugin {
name: string;
applyToEnvironment(environment: { config: { consumer: string } }): boolean;
configResolved(config: { root: string }): void;
buildStart(this: ResolveContext): Promise<void>;
transform(code: string, id: string): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined;
}

export interface ProvidedModulePluginOptions {
/** Vite plugin name, e.g. `sentry-cloudflare-flue-runtime-provider`. */
name: string;
/** Bare specifier of the package to provide, e.g. `@flue/runtime`. */
moduleName: string;
/** Namespace binding the injected import uses, e.g. `__SENTRY_FLUE_RUNTIME__`. */
identifier: string;
/** Basename of the `@sentry/server-utils` integration module to inject into, e.g. `flue`. */
integrationModule: string;
}

/**
* Build the matcher for one `@sentry/server-utils` integration module.
*
* Plain `endsWith`, not a `RegExp`: nothing here needs pattern matching, and building one from
* a caller-supplied string would need escaping, which is the only reason this file would have to
* import from `@sentry/core`. A build-time plugin should not drag the SDK into the build.
*/
export function createIntegrationModuleMatcher(integrationModule: string): (id: string) => boolean {
// The ESM build only: a worker never loads the CJS one.
const suffix = `@sentry/server-utils/build/esm/integrations/${integrationModule}.js`;

return (id: string): boolean =>
id
.replace(/\\/g, '/')
.replace(/[?#].*$/, '')
.endsWith(suffix);
}

function buildProviderSnippet({ moduleName, identifier }: ProvidedModulePluginOptions): string {
const marker = 'globalThis.__SENTRY_ORCHESTRION__';

// A getter, not an assignment: assigning reads the binding at injection time, so it stores
// `undefined` whenever the bundler evaluates Sentry's module before the provided package
// finished initializing. Enumerable so the entry shows up in `Object.keys` and a spread.
return (
`import * as ${identifier} from '${moduleName}';\n` +
`(${marker} = ${marker} || {});\n` +
`(${marker}.providedModules = ${marker}.providedModules || {});\n` +
`Object.defineProperty(${marker}.providedModules, '${moduleName}', ` +
`{ configurable: true, enumerable: true, get() { return ${identifier}; } });\n`
);
}

/**
* Build a Vite plugin that splices a static `import * as … from '<moduleName>'` into one of
* Sentry's own integration modules and exposes the namespace on the global orchestrion marker.
*
* Some packages are instrumented by registration rather than by patching, so instrumenting them
* needs a reference to that module's own binding and no channel payload carries one. On Node the
* integration resolves it itself; a bundled worker has no `node_modules` to resolve from, so the
* binding is supplied at build time instead. The import is static, lands in Sentry's module rather
* than the user's code, and is only emitted when the package actually resolves.
*/
export function createProvidedModulePlugin(options: ProvidedModulePluginOptions): ProvidedModulePlugin {
const isIntegrationModuleId = createIntegrationModuleMatcher(options.integrationModule);

let root = process.cwd();
let providerSnippet: string | undefined;

return {
name: options.name,

applyToEnvironment(environment: { config: { consumer: string } }): boolean {
// Server environments only. `buildStart` runs per environment against one shared plugin
// instance, so without this a `client` build resolves first, under browser conditions, and
// answers on the worker's behalf. That defeats the point of probing with `this.resolve`.
// Same gate the orchestrion plugin uses.
return environment.config.consumer === 'server';
},

configResolved(config: { root: string }): void {
root = config.root;
},

async buildStart(this: ResolveContext): Promise<void> {
// Already answered by an earlier server environment. A build with several worker
// environments shares the answer: they resolve under the same conditions.
if (providerSnippet) return;

try {
// The environment's own resolver, so the probe uses the conditions the injected import
// will. That is what a `require.resolve` probe cannot do: an ESM-only package has no
// `require` condition and reads as missing. Resolved from the app root, not from Sentry's
// own install.
const resolved = await this.resolve(options.moduleName, resolve(root, 'noop.js'));
if (!resolved) return;
} catch (error) {
// Present but unresolvable for some other reason. Inject anyway so the build fails loudly
// rather than silently shipping a worker with no instrumentation, and surface the original
// cause: the import error Vite raises next says nothing about why resolution broke.
this.warn(
`[Sentry] could not resolve ${options.moduleName} while probing for it; injecting the provider anyway. ${
(error as Error | undefined)?.message ?? error
}`,
);
}

providerSnippet = buildProviderSnippet(options);
},

transform(code: string, id: string): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined {
// `code.includes` keeps this idempotent: a second pass over already-injected output would
// otherwise emit a duplicate `import * as` binding, which is a syntax error.
if (!providerSnippet || !isIntegrationModuleId(id) || code.includes(options.identifier)) return undefined;

const ms = new MagicString(code);
ms.prepend(providerSnippet);
return { code: ms.toString(), map: ms.generateMap({ hires: true }) };
},
};
}
Loading
Loading