Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Work in this release was contributed by @psh4607, @thijsw, @trinitiwowka, @nehap
- `DenoMongoose` => `Mongoose`
- `DenoMysql` => `Mysql`
- `DenoPostgres` => `Postgres`
- 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')`.

Copy link
Copy Markdown
Member

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.2 anyways when they're not on the supported version and we don't import it in our integration anyways. Just wanted to double check

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

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

- **feat(browser): Add `bfcacheMetricsIntegration` to track back/forward cache health**

The new opt-in `bfcacheMetricsIntegration` emits metrics about browser back/forward cache (bfcache) navigations, so you can
Expand Down
6 changes: 4 additions & 2 deletions dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@
"@koa/router": "^12.0.1",
"@langchain/anthropic": "^0.3.10",
"@langchain/core": "^0.3.80",
"@langchain/openai": "^0.5.0",
"@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 All @@ -53,9 +55,9 @@
"@prisma/client": "6.15.0",
"@sentry/aws-serverless": "10.67.0",
"@sentry/core": "10.67.0",
"@sentry/server-utils": "10.67.0",
"@sentry/hono": "10.67.0",
"@sentry/node": "10.67.0",
"@sentry/server-utils": "10.67.0",
"@types/mongodb": "^3.6.20",
"@types/mysql": "^2.15.21",
"@types/pg": "^8.6.5",
Expand Down
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 = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Why do we need a stub here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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();
Loading
Loading