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
2 changes: 0 additions & 2 deletions dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@
"@langchain/core": "^0.3.80",
"@langchain/langgraph": "^0.2.32",
"@langchain/openai": "^0.5.0",
"@mastra/core": "1.63.2",
"@mastra/observability": "1.17.4",
"@modelcontextprotocol/client": "^2.0.0",
"@modelcontextprotocol/server": "^2.0.0",
"@nestjs/common": "^11",
Expand Down
405 changes: 228 additions & 177 deletions dev-packages/node-integration-tests/suites/tracing/mastra/test.ts

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ export type InternalGlobal = {
__SENTRY_ORCHESTRION__?: {
/** Empty array signifies runtime hooked */
runtime?: string[];
/**
* The resolved `file:` URL of a transformed file for each runtime-injected
* module, keyed by module name (e.g. `@mastra/core`). Lets an integration
* anchor `createRequire` on the app's actual copy of a dependency without
* relying on `process.cwd()` or the CJS `require.cache` — the latter is
* always empty for ESM-loaded modules. Only the runtime `--import`/hook path
* populates this; the bundler path inlines modules and records none.
*/
runtimeFiles?: Record<string, string>;
/**
* Module names recorded as each bundler-transformed module loads (the
* injected snippet calls `orchestrionModuleInjected`). The bundler plugin's
Expand Down
5 changes: 4 additions & 1 deletion packages/server-runtime-injection/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function registerDiagnosticsChannelInjection(): void {
// we build against, hence the cast.
const mod = Module as NodeModule;

setDiagnosticsHook(({ moduleName, error }): void => {
setDiagnosticsHook(({ url, moduleName, error }): void => {
if (error) {
// A stripped transformer surfaces as a `TypeError` (`parse`/`generate` are `undefined`) and
// costs the user this module's instrumentation, so it is worth an always-on warning. Every
Expand All @@ -110,6 +110,9 @@ export function registerDiagnosticsChannelInjection(): void {
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ || {};
GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtime = GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtime || [];
GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtime.push(moduleName);
// Record the module's resolved file so integrations can anchor dependency resolution on the
// app's actual copy, even under ESM (where the CJS `require.cache` never sees the module).
(GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtimeFiles ??= {})[moduleName] = url;
// Tell channel integrations their module just loaded, so they subscribe
// now. They hold off at `init()` to avoid claiming channel slots for
// modules that never load, because Node caps channels in use at 1024.
Expand Down
31 changes: 28 additions & 3 deletions packages/server-utils/src/integrations/mastra.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as diagnosticsChannel from 'node:diagnostics_channel';
import { createRequire } from 'node:module';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { IntegrationFn } from '@sentry/core';
import { consoleSandbox, debug, defineIntegration } from '@sentry/core';
import { consoleSandbox, debug, defineIntegration, GLOBAL_OBJ } from '@sentry/core';
import {
COMMUNITY_MASTRA_SENTRY_EXPORTER_NAME,
MASTRA_EXPORTER_BRAND,
Expand Down Expand Up @@ -169,9 +170,28 @@ function appRequire(): ReturnType<typeof createRequire> {
return createRequire(cwdRequireParent());
}

/**
* The runtime injection hook records the resolved file of each instrumented module as it loads.
* Unlike the CJS `require.cache`, this is populated for ESM-loaded modules too, so it is the
* reliable anchor for finding the app's `@mastra/observability` next to its `@mastra/core`.
*/
function findInjectedMastraCoreFilename(): string | undefined {
const url = GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.runtimeFiles?.['@mastra/core'];
if (!url) {
return undefined;
}

try {
return url.startsWith('file:') ? fileURLToPath(url) : url;
} catch {
return undefined;
}
}

/**
* `@mastra/core` is already evaluated (we are in its constructor). Prefer that file so a
* serverless/test cwd that is not the app still finds the app's `@mastra/observability`.
* Only sees CJS-loaded modules; ESM apps rely on {@link findInjectedMastraCoreFilename}.
*/
function findLoadedMastraCoreFilename(): string | undefined {
const cache = appRequire().cache;
Expand All @@ -197,11 +217,16 @@ function tryRequireObservability(parent: string): Record<string, unknown> | unde
}

/**
* Prefer the already-loaded `@mastra/core` file, then cwd-resolved core, then cwd itself.
* A cache hit can still fail under pnpm if that copy cannot see `@mastra/observability`.
* Prefer the runtime-injected `@mastra/core` file (works under ESM and CJS), then the CJS-cached
* copy, then cwd-resolved core, then cwd itself. A hit can still fail under pnpm if that copy
* cannot see `@mastra/observability`, hence the fallbacks.
*/
function loadMastraObservability(): Record<string, unknown> {
const parents = new Set<string>();
const injectedCore = findInjectedMastraCoreFilename();
if (injectedCore) {
parents.add(injectedCore);
}
const loadedCore = findLoadedMastraCoreFilename();
if (loadedCore) {
parents.add(loadedCore);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { CHANNELS } from '../../../src/orchestrion/channels';
const observabilityFrom = vi.hoisted(() => ({
parent: undefined as string | undefined,
cache: {} as NodeJS.Dict<NodeModule>,
injectedCoreUrl: 'file:///app/node_modules/@mastra/core/dist/mastra-abc123.js',
injectedCore: '/app/node_modules/@mastra/core/dist/mastra-abc123.js',
loadedCore: '/app/node_modules/@mastra/core/dist/index.js',
cwdCore: '/cwd/node_modules/@mastra/core/index.js',
cwd: `${process.cwd()}/noop.js`,
Expand Down Expand Up @@ -59,6 +61,39 @@ describe('mastraIntegration observability resolve', () => {
delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__;
});

it('loads @mastra/observability from the runtime-injected @mastra/core file (ESM path)', () => {
// No CJS cache entry — mirrors an ESM app, where the loaded module never reaches `require.cache`.
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = {
runtime: ['@mastra/core'],
runtimeFiles: { '@mastra/core': observabilityFrom.injectedCoreUrl },
};
const registerExporter = vi.fn();

tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({
self: { registerExporter },
arguments: [],
});

expect(observabilityFrom.parent).toBe(observabilityFrom.injectedCore);
expect(registerExporter).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything());
});

it('prefers the runtime-injected file over the CJS-cached copy', () => {
observabilityFrom.cache[observabilityFrom.loadedCore] = {} as NodeModule;
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = {
runtime: ['@mastra/core'],
runtimeFiles: { '@mastra/core': observabilityFrom.injectedCoreUrl },
};
const registerExporter = vi.fn();

tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({
self: { registerExporter },
arguments: [],
});

expect(observabilityFrom.parent).toBe(observabilityFrom.injectedCore);
});

it('loads @mastra/observability from a loaded @mastra/core file, not cwd', () => {
observabilityFrom.cache[observabilityFrom.loadedCore] = {} as NodeModule;
const registerExporter = vi.fn();
Expand Down
Loading
Loading