-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(node): Add first-party Mastra integration #23823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import * as Sentry from '@sentry/node'; | ||
| import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
|
||
| Sentry.init({ | ||
| traceLifecycle: 'static', | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| release: '1.0', | ||
| tracesSampleRate: 1.0, | ||
| dataCollection: { genAI: { inputs: false, outputs: false } }, | ||
| integrations: [Sentry.mastraIntegration({ bootstrapObservability: false })], | ||
| transport: loggingTransport, | ||
| beforeSendTransaction: event => { | ||
| // Drop the in-process mock provider's own transactions. | ||
| return event.transaction?.includes('/v1/chat/completions') ? null : event; | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import * as Sentry from '@sentry/node'; | ||
| import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
|
||
| Sentry.init({ | ||
| traceLifecycle: 'static', | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| release: '1.0', | ||
| tracesSampleRate: 1.0, | ||
| dataCollection: { genAI: { inputs: true, outputs: true } }, | ||
| transport: loggingTransport, | ||
| beforeSendTransaction: event => { | ||
| // Drop the in-process mock provider's own transactions. | ||
| return event.transaction?.includes('/v1/chat/completions') ? null : event; | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import * as Sentry from '@sentry/node'; | ||
| import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
|
||
| Sentry.init({ | ||
| traceLifecycle: 'static', | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| release: '1.0', | ||
| tracesSampleRate: 1.0, | ||
| dataCollection: { genAI: { inputs: false, outputs: false } }, | ||
| transport: loggingTransport, | ||
| beforeSendTransaction: event => { | ||
| // Drop the in-process mock provider's own transactions. | ||
| return event.transaction?.includes('/v1/chat/completions') ? null : event; | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import express from 'express'; | ||
| import * as Sentry from '@sentry/node'; | ||
| import { Mastra } from '@mastra/core'; | ||
| import { Agent } from '@mastra/core/agent'; | ||
|
|
||
| // Inlined OpenAI-compatible mock: the ESM/CJS runner copies only this file into its temp dir. | ||
| function startMockProvider(responses) { | ||
| const app = express(); | ||
| app.use(express.json()); | ||
|
|
||
| let call = 0; | ||
| app.post('/v1/chat/completions', (req, res) => { | ||
| const response = responses[Math.min(call, responses.length - 1)]; | ||
| call++; | ||
| res.json({ | ||
| id: response.id, | ||
| object: 'chat.completion', | ||
| created: 1, | ||
| model: req.body.model, | ||
| choices: [ | ||
| { | ||
| index: 0, | ||
| finish_reason: response.toolCalls ? 'tool_calls' : 'stop', | ||
| message: { | ||
| role: 'assistant', | ||
| content: response.content ?? null, | ||
| ...(response.toolCalls ? { tool_calls: response.toolCalls } : {}), | ||
| }, | ||
| }, | ||
| ], | ||
| usage: response.usage, | ||
| }); | ||
| }); | ||
|
|
||
| const server = app.listen(0); | ||
| return { | ||
| url: `http://localhost:${server.address().port}/v1`, | ||
| close: () => server.close(), | ||
| }; | ||
| } | ||
|
|
||
| const provider = startMockProvider([ | ||
| { | ||
| id: 'chatcmpl-1', | ||
| content: 'It is 22C in Berlin.', | ||
| usage: { prompt_tokens: 12, completion_tokens: 7, total_tokens: 19 }, | ||
| }, | ||
| ]); | ||
|
|
||
| async function run() { | ||
| const agent = new Agent({ | ||
| id: 'weather_agent', | ||
| name: 'weather_agent', | ||
| instructions: 'You report the weather.', | ||
| model: { id: 'openai/gpt-4o-mini', url: provider.url, apiKey: 'test' }, | ||
| }); | ||
|
|
||
| const mastra = new Mastra({ | ||
| agents: { weather_agent: agent }, | ||
| logger: false, | ||
| }); | ||
|
|
||
| await Sentry.startSpan({ op: 'function', name: 'mastra-test' }, async () => { | ||
| await mastra.getAgent('weather_agent').generate('What is the weather in Berlin?'); | ||
| }); | ||
|
|
||
| await mastra.observability.shutdown(); | ||
| provider.close(); | ||
| } | ||
|
|
||
| run(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import express from 'express'; | ||
| import * as Sentry from '@sentry/node'; | ||
| import { Mastra } from '@mastra/core'; | ||
| import { Agent } from '@mastra/core/agent'; | ||
| import { Observability } from '@mastra/observability'; | ||
|
|
||
| // Inlined OpenAI-compatible mock: the ESM/CJS runner copies only this file into its temp dir. | ||
| function startMockProvider(responses) { | ||
| const app = express(); | ||
| app.use(express.json()); | ||
|
|
||
| let call = 0; | ||
| app.post('/v1/chat/completions', (req, res) => { | ||
| const response = responses[Math.min(call, responses.length - 1)]; | ||
| call++; | ||
| res.json({ | ||
| id: response.id, | ||
| object: 'chat.completion', | ||
| created: 1, | ||
| model: req.body.model, | ||
| choices: [ | ||
| { | ||
| index: 0, | ||
| finish_reason: response.toolCalls ? 'tool_calls' : 'stop', | ||
| message: { | ||
| role: 'assistant', | ||
| content: response.content ?? null, | ||
| ...(response.toolCalls ? { tool_calls: response.toolCalls } : {}), | ||
| }, | ||
| }, | ||
| ], | ||
| usage: response.usage, | ||
| }); | ||
| }); | ||
|
|
||
| const server = app.listen(0); | ||
| return { | ||
| url: `http://localhost:${server.address().port}/v1`, | ||
| close: () => server.close(), | ||
| }; | ||
| } | ||
|
|
||
| const provider = startMockProvider([ | ||
| { | ||
| id: 'chatcmpl-1', | ||
| content: 'It is 22C in Berlin.', | ||
| usage: { prompt_tokens: 12, completion_tokens: 7, total_tokens: 19 }, | ||
| }, | ||
| ]); | ||
|
|
||
| async function run() { | ||
| // Stub of `@mastra/sentry`: same `name: 'sentry'`, no brand. The real package calls `Sentry.init()`. | ||
| const communityExporter = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: Why do we need a stub here?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current @mastra/sentry exporter calls Sentry.init() and would replace the test client. The stub is only the name: 'sentry' signal so we hit the “community exporter already registered” path and still assert our spans go out |
||
| name: 'sentry', | ||
| async exportTracingEvent() {}, | ||
| async flush() {}, | ||
| async shutdown() {}, | ||
| }; | ||
|
|
||
| const agent = new Agent({ | ||
| id: 'weather_agent', | ||
| name: 'weather_agent', | ||
| instructions: 'You report the weather.', | ||
| model: { id: 'openai/gpt-4o-mini', url: provider.url, apiKey: 'test' }, | ||
| }); | ||
|
|
||
| const mastra = new Mastra({ | ||
| agents: { weather_agent: agent }, | ||
| logger: false, | ||
| observability: new Observability({ | ||
| configs: { | ||
| default: { serviceName: 'mastra-test', exporters: [communityExporter] }, | ||
| }, | ||
| }), | ||
| }); | ||
|
|
||
| await Sentry.startSpan({ op: 'function', name: 'mastra-test' }, async () => { | ||
| await mastra.getAgent('weather_agent').generate('What is the weather in Berlin?'); | ||
| }); | ||
|
|
||
| await mastra.observability.shutdown(); | ||
| provider.close(); | ||
| } | ||
|
|
||
| run(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import express from 'express'; | ||
| import * as Sentry from '@sentry/node'; | ||
| import { z } from 'zod'; | ||
| import { Mastra } from '@mastra/core'; | ||
| import { Agent } from '@mastra/core/agent'; | ||
| import { createTool } from '@mastra/core/tools'; | ||
| import { Observability } from '@mastra/observability'; | ||
| import { SentryMastraExporter } from '@sentry/node'; | ||
|
|
||
| // Two-step agent loop. Inlined OpenAI-compatible mock: the ESM/CJS runner copies only this file. | ||
| function startMockProvider(responses) { | ||
| const app = express(); | ||
| app.use(express.json()); | ||
|
|
||
| let call = 0; | ||
| app.post('/v1/chat/completions', (req, res) => { | ||
| const response = responses[Math.min(call, responses.length - 1)]; | ||
| call++; | ||
| res.json({ | ||
| id: response.id, | ||
| object: 'chat.completion', | ||
| created: 1, | ||
| model: req.body.model, | ||
| choices: [ | ||
| { | ||
| index: 0, | ||
| finish_reason: response.toolCalls ? 'tool_calls' : 'stop', | ||
| message: { | ||
| role: 'assistant', | ||
| content: response.content ?? null, | ||
| ...(response.toolCalls ? { tool_calls: response.toolCalls } : {}), | ||
| }, | ||
| }, | ||
| ], | ||
| usage: response.usage, | ||
| }); | ||
| }); | ||
|
|
||
| const server = app.listen(0); | ||
| return { | ||
| url: `http://localhost:${server.address().port}/v1`, | ||
| close: () => server.close(), | ||
| }; | ||
| } | ||
|
|
||
| const provider = startMockProvider([ | ||
| { | ||
| id: 'chatcmpl-tool', | ||
| toolCalls: [ | ||
| { | ||
| id: 'call_1', | ||
| type: 'function', | ||
| function: { name: 'get_weather', arguments: '{"city":"Berlin"}' }, | ||
| }, | ||
| ], | ||
| usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 }, | ||
| }, | ||
| { | ||
| id: 'chatcmpl-final', | ||
| content: 'It is 22C in Berlin.', | ||
| usage: { prompt_tokens: 30, completion_tokens: 8, total_tokens: 38 }, | ||
| }, | ||
| ]); | ||
|
|
||
| async function run() { | ||
| const agent = new Agent({ | ||
| id: 'weather_agent', | ||
| name: 'weather_agent', | ||
| instructions: 'Use the weather tool.', | ||
| model: { id: 'openai/gpt-4o-mini', url: provider.url, apiKey: 'test' }, | ||
| tools: { | ||
| get_weather: createTool({ | ||
| id: 'get_weather', | ||
| description: 'Get weather for a city', | ||
| inputSchema: z.object({ city: z.string() }), | ||
| execute: async ({ city }) => ({ temperature: 22, city }), | ||
| }), | ||
| }, | ||
| }); | ||
|
|
||
| const mastra = new Mastra({ | ||
| agents: { weather_agent: agent }, | ||
| logger: false, | ||
| observability: new Observability({ | ||
| configs: { | ||
| default: { | ||
| serviceName: 'mastra-test', | ||
| exporters: [new SentryMastraExporter()], | ||
| }, | ||
| }, | ||
| }), | ||
| }); | ||
|
|
||
| await Sentry.startSpan({ op: 'function', name: 'mastra-test' }, async () => { | ||
| await mastra.getAgent('weather_agent').generate('Weather in Berlin?', { maxSteps: 3 }); | ||
| }); | ||
|
|
||
| await mastra.observability.shutdown(); | ||
| provider.close(); | ||
| } | ||
|
|
||
| run(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import * as Sentry from '@sentry/node'; | ||
| import { z } from 'zod'; | ||
| import { Mastra } from '@mastra/core'; | ||
| import { createStep, createWorkflow } from '@mastra/core/workflows'; | ||
| import { Observability } from '@mastra/observability'; | ||
| import { SentryMastraExporter } from '@sentry/node'; | ||
|
|
||
| // `workflow_run` maps to `gen_ai.invoke_agent`; `workflow_step` has no conventional op and is dropped. | ||
| const double = createStep({ | ||
| id: 'double', | ||
| inputSchema: z.object({ n: z.number() }), | ||
| outputSchema: z.object({ n: z.number() }), | ||
| execute: async ({ inputData }) => ({ n: inputData.n * 2 }), | ||
| }); | ||
|
|
||
| const workflow = createWorkflow({ | ||
| id: 'math_workflow', | ||
| inputSchema: z.object({ n: z.number() }), | ||
| outputSchema: z.object({ n: z.number() }), | ||
| }) | ||
| .then(double) | ||
| .commit(); | ||
|
|
||
| async function run() { | ||
| const mastra = new Mastra({ | ||
| workflows: { math_workflow: workflow }, | ||
| logger: false, | ||
| observability: new Observability({ | ||
| configs: { | ||
| default: { | ||
| serviceName: 'mastra-test', | ||
| exporters: [new SentryMastraExporter()], | ||
| }, | ||
| }, | ||
| }), | ||
| }); | ||
|
|
||
| await Sentry.startSpan({ op: 'function', name: 'mastra-test' }, async () => { | ||
| const workflowRun = await mastra.getWorkflow('math_workflow').createRun(); | ||
| await workflowRun.start({ inputData: { n: 21 } }); | ||
| }); | ||
|
|
||
| await mastra.observability.shutdown(); | ||
| } | ||
|
|
||
| run(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m: Can we enable it by default? It seems Mastra 1.63.2 has an engine of 22.13+: https://github.com/mastra-ai/mastra/blob/003e75745c5fd6a7af8464ece1d2930f81dd15af/packages/core/package.json#L987
Maybe it wouldn't matter as users can't use
1.63.2anyways when they're not on the supported version and we don't import it in our integration anyways. Just wanted to double checkThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cross-link: https://github.com/getsentry/sentry-javascript/pull/23823/changes#r3913903654
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yah It's already on by default (in getTracingIntegrations(), same as the other AI integrations). we never import @mastra/core, and if registerExporter is missing we just skip