Skip to content

Commit 98330de

Browse files
RulaKhaledclaude
andauthored
feat(node): Add first-party Mastra integration (#23823)
Mastra agent traces now come from the SDK: `invoke_agent`, `chat`, and `execute_tool` spans, on by default for Node-family SDKs. Ref JS-3455 (dup JSSDK-21). **How it attaches** - Hooks the `Mastra` constructor and registers our exporter via `registerExporter()`. - If the app already has observability, we attach to it. - If it doesn’t, we bootstrap a pipeline from `@mastra/observability` when that package is already installed. We never add it ourselves. - You can also register the exporter by hand. **What we emit** - Only ops, names, and attributes from [sentry-conventions](https://getsentry.github.io/sentry-conventions/ops/#gen_ai). - Span types with no conventional op (`workflow_step`, `memory_operation`, `processor_run`, `scorer_*`) are dropped; children re-parent onto the nearest exported ancestor. - `model_inference` is dropped too. Mastra nests `model_generation > model_step > model_inference`, and the inference span repeats the generation’s model/usage — exporting both produced a duplicate nested `chat` per step. `model_generation` is the `gen_ai.chat` span. Note to reviewer: 🗯️ This diff is large mostly because of Node integration tests and the @mastra/* yarn.lock bump, not because the SDK surface is huge. The product decisions are in three files: - packages/server-utils/src/ai/mastra/utils.ts — span names and gen_ai.* attributes - packages/server-utils/src/ai/mastra/index.ts — exporter (drop unmapped types, re-parent children) - packages/server-utils/src/integrations/mastra.ts — constructor attach, bootstrap, skip duplicate provider wrapping If you are checking that auto-instrumentation actually hooks Mastra, also glance at packages/server-utils/src/orchestrion/config/mastra.ts --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 37118dd commit 98330de

36 files changed

Lines changed: 2640 additions & 21 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Work in this release was contributed by @psh4607, @thijsw, @trinitiwowka, @nehap
1414
- `DenoMongoose` => `Mongoose`
1515
- `DenoMysql` => `Mysql`
1616
- `DenoPostgres` => `Postgres`
17+
- feat(node): Add first-party Mastra integration ([#23823](https://github.com/getsentry/sentry-javascript/pull/23823)). Enabled by default; disable with `defaultIntegrations: integrations => integrations.filter(i => i.name !== 'Mastra')`.
1718
- **feat(browser): Add `bfcacheMetricsIntegration` to track back/forward cache health**
1819

1920
The new opt-in `bfcacheMetricsIntegration` emits metrics about browser back/forward cache (bfcache) navigations, so you can

‎dev-packages/node-integration-tests/package.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
"@koa/router": "^12.0.1",
4343
"@langchain/anthropic": "^0.3.10",
4444
"@langchain/core": "^0.3.80",
45-
"@langchain/openai": "^0.5.0",
4645
"@langchain/langgraph": "^0.2.32",
46+
"@langchain/openai": "^0.5.0",
47+
"@mastra/core": "1.63.2",
48+
"@mastra/observability": "1.17.4",
4749
"@modelcontextprotocol/client": "^2.0.0",
4850
"@modelcontextprotocol/server": "^2.0.0",
4951
"@nestjs/common": "^11",
@@ -53,9 +55,9 @@
5355
"@prisma/client": "6.15.0",
5456
"@sentry/aws-serverless": "10.67.0",
5557
"@sentry/core": "10.67.0",
56-
"@sentry/server-utils": "10.67.0",
5758
"@sentry/hono": "10.67.0",
5859
"@sentry/node": "10.67.0",
60+
"@sentry/server-utils": "10.67.0",
5961
"@types/mongodb": "^3.6.20",
6062
"@types/mysql": "^2.15.21",
6163
"@types/pg": "^8.6.5",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
traceLifecycle: 'static',
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
release: '1.0',
8+
tracesSampleRate: 1.0,
9+
dataCollection: { genAI: { inputs: false, outputs: false } },
10+
integrations: [Sentry.mastraIntegration({ bootstrapObservability: false })],
11+
transport: loggingTransport,
12+
beforeSendTransaction: event => {
13+
// Drop the in-process mock provider's own transactions.
14+
return event.transaction?.includes('/v1/chat/completions') ? null : event;
15+
},
16+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
traceLifecycle: 'static',
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
release: '1.0',
8+
tracesSampleRate: 1.0,
9+
dataCollection: { genAI: { inputs: true, outputs: true } },
10+
transport: loggingTransport,
11+
beforeSendTransaction: event => {
12+
// Drop the in-process mock provider's own transactions.
13+
return event.transaction?.includes('/v1/chat/completions') ? null : event;
14+
},
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
traceLifecycle: 'static',
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
release: '1.0',
8+
tracesSampleRate: 1.0,
9+
dataCollection: { genAI: { inputs: false, outputs: false } },
10+
transport: loggingTransport,
11+
beforeSendTransaction: event => {
12+
// Drop the in-process mock provider's own transactions.
13+
return event.transaction?.includes('/v1/chat/completions') ? null : event;
14+
},
15+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import express from 'express';
2+
import * as Sentry from '@sentry/node';
3+
import { Mastra } from '@mastra/core';
4+
import { Agent } from '@mastra/core/agent';
5+
6+
// Inlined OpenAI-compatible mock: the ESM/CJS runner copies only this file into its temp dir.
7+
function startMockProvider(responses) {
8+
const app = express();
9+
app.use(express.json());
10+
11+
let call = 0;
12+
app.post('/v1/chat/completions', (req, res) => {
13+
const response = responses[Math.min(call, responses.length - 1)];
14+
call++;
15+
res.json({
16+
id: response.id,
17+
object: 'chat.completion',
18+
created: 1,
19+
model: req.body.model,
20+
choices: [
21+
{
22+
index: 0,
23+
finish_reason: response.toolCalls ? 'tool_calls' : 'stop',
24+
message: {
25+
role: 'assistant',
26+
content: response.content ?? null,
27+
...(response.toolCalls ? { tool_calls: response.toolCalls } : {}),
28+
},
29+
},
30+
],
31+
usage: response.usage,
32+
});
33+
});
34+
35+
const server = app.listen(0);
36+
return {
37+
url: `http://localhost:${server.address().port}/v1`,
38+
close: () => server.close(),
39+
};
40+
}
41+
42+
const provider = startMockProvider([
43+
{
44+
id: 'chatcmpl-1',
45+
content: 'It is 22C in Berlin.',
46+
usage: { prompt_tokens: 12, completion_tokens: 7, total_tokens: 19 },
47+
},
48+
]);
49+
50+
async function run() {
51+
const agent = new Agent({
52+
id: 'weather_agent',
53+
name: 'weather_agent',
54+
instructions: 'You report the weather.',
55+
model: { id: 'openai/gpt-4o-mini', url: provider.url, apiKey: 'test' },
56+
});
57+
58+
const mastra = new Mastra({
59+
agents: { weather_agent: agent },
60+
logger: false,
61+
});
62+
63+
await Sentry.startSpan({ op: 'function', name: 'mastra-test' }, async () => {
64+
await mastra.getAgent('weather_agent').generate('What is the weather in Berlin?');
65+
});
66+
67+
await mastra.observability.shutdown();
68+
provider.close();
69+
}
70+
71+
run();
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import express from 'express';
2+
import * as Sentry from '@sentry/node';
3+
import { Mastra } from '@mastra/core';
4+
import { Agent } from '@mastra/core/agent';
5+
import { Observability } from '@mastra/observability';
6+
7+
// Inlined OpenAI-compatible mock: the ESM/CJS runner copies only this file into its temp dir.
8+
function startMockProvider(responses) {
9+
const app = express();
10+
app.use(express.json());
11+
12+
let call = 0;
13+
app.post('/v1/chat/completions', (req, res) => {
14+
const response = responses[Math.min(call, responses.length - 1)];
15+
call++;
16+
res.json({
17+
id: response.id,
18+
object: 'chat.completion',
19+
created: 1,
20+
model: req.body.model,
21+
choices: [
22+
{
23+
index: 0,
24+
finish_reason: response.toolCalls ? 'tool_calls' : 'stop',
25+
message: {
26+
role: 'assistant',
27+
content: response.content ?? null,
28+
...(response.toolCalls ? { tool_calls: response.toolCalls } : {}),
29+
},
30+
},
31+
],
32+
usage: response.usage,
33+
});
34+
});
35+
36+
const server = app.listen(0);
37+
return {
38+
url: `http://localhost:${server.address().port}/v1`,
39+
close: () => server.close(),
40+
};
41+
}
42+
43+
const provider = startMockProvider([
44+
{
45+
id: 'chatcmpl-1',
46+
content: 'It is 22C in Berlin.',
47+
usage: { prompt_tokens: 12, completion_tokens: 7, total_tokens: 19 },
48+
},
49+
]);
50+
51+
async function run() {
52+
// Stub of `@mastra/sentry`: same `name: 'sentry'`, no brand. The real package calls `Sentry.init()`.
53+
const communityExporter = {
54+
name: 'sentry',
55+
async exportTracingEvent() {},
56+
async flush() {},
57+
async shutdown() {},
58+
};
59+
60+
const agent = new Agent({
61+
id: 'weather_agent',
62+
name: 'weather_agent',
63+
instructions: 'You report the weather.',
64+
model: { id: 'openai/gpt-4o-mini', url: provider.url, apiKey: 'test' },
65+
});
66+
67+
const mastra = new Mastra({
68+
agents: { weather_agent: agent },
69+
logger: false,
70+
observability: new Observability({
71+
configs: {
72+
default: { serviceName: 'mastra-test', exporters: [communityExporter] },
73+
},
74+
}),
75+
});
76+
77+
await Sentry.startSpan({ op: 'function', name: 'mastra-test' }, async () => {
78+
await mastra.getAgent('weather_agent').generate('What is the weather in Berlin?');
79+
});
80+
81+
await mastra.observability.shutdown();
82+
provider.close();
83+
}
84+
85+
run();
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import express from 'express';
2+
import * as Sentry from '@sentry/node';
3+
import { z } from 'zod';
4+
import { Mastra } from '@mastra/core';
5+
import { Agent } from '@mastra/core/agent';
6+
import { createTool } from '@mastra/core/tools';
7+
import { Observability } from '@mastra/observability';
8+
import { SentryMastraExporter } from '@sentry/node';
9+
10+
// Two-step agent loop. Inlined OpenAI-compatible mock: the ESM/CJS runner copies only this file.
11+
function startMockProvider(responses) {
12+
const app = express();
13+
app.use(express.json());
14+
15+
let call = 0;
16+
app.post('/v1/chat/completions', (req, res) => {
17+
const response = responses[Math.min(call, responses.length - 1)];
18+
call++;
19+
res.json({
20+
id: response.id,
21+
object: 'chat.completion',
22+
created: 1,
23+
model: req.body.model,
24+
choices: [
25+
{
26+
index: 0,
27+
finish_reason: response.toolCalls ? 'tool_calls' : 'stop',
28+
message: {
29+
role: 'assistant',
30+
content: response.content ?? null,
31+
...(response.toolCalls ? { tool_calls: response.toolCalls } : {}),
32+
},
33+
},
34+
],
35+
usage: response.usage,
36+
});
37+
});
38+
39+
const server = app.listen(0);
40+
return {
41+
url: `http://localhost:${server.address().port}/v1`,
42+
close: () => server.close(),
43+
};
44+
}
45+
46+
const provider = startMockProvider([
47+
{
48+
id: 'chatcmpl-tool',
49+
toolCalls: [
50+
{
51+
id: 'call_1',
52+
type: 'function',
53+
function: { name: 'get_weather', arguments: '{"city":"Berlin"}' },
54+
},
55+
],
56+
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 },
57+
},
58+
{
59+
id: 'chatcmpl-final',
60+
content: 'It is 22C in Berlin.',
61+
usage: { prompt_tokens: 30, completion_tokens: 8, total_tokens: 38 },
62+
},
63+
]);
64+
65+
async function run() {
66+
const agent = new Agent({
67+
id: 'weather_agent',
68+
name: 'weather_agent',
69+
instructions: 'Use the weather tool.',
70+
model: { id: 'openai/gpt-4o-mini', url: provider.url, apiKey: 'test' },
71+
tools: {
72+
get_weather: createTool({
73+
id: 'get_weather',
74+
description: 'Get weather for a city',
75+
inputSchema: z.object({ city: z.string() }),
76+
execute: async ({ city }) => ({ temperature: 22, city }),
77+
}),
78+
},
79+
});
80+
81+
const mastra = new Mastra({
82+
agents: { weather_agent: agent },
83+
logger: false,
84+
observability: new Observability({
85+
configs: {
86+
default: {
87+
serviceName: 'mastra-test',
88+
exporters: [new SentryMastraExporter()],
89+
},
90+
},
91+
}),
92+
});
93+
94+
await Sentry.startSpan({ op: 'function', name: 'mastra-test' }, async () => {
95+
await mastra.getAgent('weather_agent').generate('Weather in Berlin?', { maxSteps: 3 });
96+
});
97+
98+
await mastra.observability.shutdown();
99+
provider.close();
100+
}
101+
102+
run();
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as Sentry from '@sentry/node';
2+
import { z } from 'zod';
3+
import { Mastra } from '@mastra/core';
4+
import { createStep, createWorkflow } from '@mastra/core/workflows';
5+
import { Observability } from '@mastra/observability';
6+
import { SentryMastraExporter } from '@sentry/node';
7+
8+
// `workflow_run` maps to `gen_ai.invoke_agent`; `workflow_step` has no conventional op and is dropped.
9+
const double = createStep({
10+
id: 'double',
11+
inputSchema: z.object({ n: z.number() }),
12+
outputSchema: z.object({ n: z.number() }),
13+
execute: async ({ inputData }) => ({ n: inputData.n * 2 }),
14+
});
15+
16+
const workflow = createWorkflow({
17+
id: 'math_workflow',
18+
inputSchema: z.object({ n: z.number() }),
19+
outputSchema: z.object({ n: z.number() }),
20+
})
21+
.then(double)
22+
.commit();
23+
24+
async function run() {
25+
const mastra = new Mastra({
26+
workflows: { math_workflow: workflow },
27+
logger: false,
28+
observability: new Observability({
29+
configs: {
30+
default: {
31+
serviceName: 'mastra-test',
32+
exporters: [new SentryMastraExporter()],
33+
},
34+
},
35+
}),
36+
});
37+
38+
await Sentry.startSpan({ op: 'function', name: 'mastra-test' }, async () => {
39+
const workflowRun = await mastra.getWorkflow('math_workflow').createRun();
40+
await workflowRun.start({ inputData: { n: 21 } });
41+
});
42+
43+
await mastra.observability.shutdown();
44+
}
45+
46+
run();

0 commit comments

Comments
 (0)