From a45a5dbd5557eeabc16ae7c616385f85e52c32bc Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Tue, 1 Sep 2026 23:27:35 +0200 Subject: [PATCH] feat(node): Add first-party Mastra integration Mastra is exporter-based, so we hook the constructor, register a Sentry exporter, and bootstrap an observability pipeline when the app has not configured one. --- CHANGELOG.md | 1 + .../vite-autoinstrument/workflow/test.ts | 1 + .../node-integration-tests/package.json | 6 +- .../mastra/instrument-no-bootstrap.mjs | 16 + .../tracing/mastra/instrument-with-pii.mjs | 15 + .../suites/tracing/mastra/instrument.mjs | 15 + .../suites/tracing/mastra/scenario-auto.mjs | 72 ++++ .../mastra/scenario-community-exporter.mjs | 86 ++++ .../suites/tracing/mastra/scenario-tools.mjs | 103 +++++ .../tracing/mastra/scenario-workflow.mjs | 47 ++ .../suites/tracing/mastra/scenario.mjs | 82 ++++ .../suites/tracing/mastra/test.ts | 210 +++++++++ packages/astro/src/index.server.ts | 2 + packages/aws-serverless/src/index.ts | 2 + packages/bun/src/index.ts | 2 + packages/deno/src/index.ts | 2 + .../deno/test/__snapshots__/mod.test.ts.snap | 3 + packages/elysia/src/index.ts | 2 + packages/google-cloud-serverless/src/index.ts | 2 + packages/node/src/index.ts | 2 + packages/server-utils/src/ai/index.ts | 1 + .../server-utils/src/ai/mastra/constants.ts | 51 +++ packages/server-utils/src/ai/mastra/index.ts | 244 +++++++++++ packages/server-utils/src/ai/mastra/types.ts | 100 +++++ packages/server-utils/src/ai/mastra/utils.ts | 253 +++++++++++ packages/server-utils/src/index.ts | 2 + .../server-utils/src/integrations/index.ts | 2 + .../server-utils/src/integrations/mastra.ts | 261 ++++++++++++ .../server-utils/src/orchestrion/channels.ts | 2 + .../config/channel-integration-definitions.ts | 1 + .../src/orchestrion/config/index.ts | 2 + .../src/orchestrion/config/mastra.ts | 27 ++ .../ai/lib/tracing/mastra-exporter.test.ts | 402 ++++++++++++++++++ .../mastra/bootstrap-resolve.test.ts | 135 ++++++ .../mastra/skip-providers.test.ts | 61 +++ .../test/orchestrion/webpack-loader.test.ts | 35 ++ yarn.lock | 352 ++++++++++++++- 37 files changed, 2581 insertions(+), 21 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/instrument-no-bootstrap.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/instrument-with-pii.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/scenario-auto.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/scenario-community-exporter.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/scenario-tools.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/scenario-workflow.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mastra/test.ts create mode 100644 packages/server-utils/src/ai/mastra/constants.ts create mode 100644 packages/server-utils/src/ai/mastra/index.ts create mode 100644 packages/server-utils/src/ai/mastra/types.ts create mode 100644 packages/server-utils/src/ai/mastra/utils.ts create mode 100644 packages/server-utils/src/integrations/mastra.ts create mode 100644 packages/server-utils/src/orchestrion/config/mastra.ts create mode 100644 packages/server-utils/test/ai/lib/tracing/mastra-exporter.test.ts create mode 100644 packages/server-utils/test/integrations/mastra/bootstrap-resolve.test.ts create mode 100644 packages/server-utils/test/integrations/mastra/skip-providers.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 43662f8399e5..9700a653ba18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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')`. - **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 diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/test.ts index c942d5376132..3823a98470b4 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/test.ts @@ -8,6 +8,7 @@ import { createRunner } from '../../../runner'; // transform wrapped `MyWorkflow` with `instrumentWorkflowWithSentry`. it('auto-instruments a Workflow class', async ({ signal }) => { const runner = createRunner(__dirname) + .unordered() .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; expect(transactionEvent.transaction).toBe('step-one'); diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 89891b61c083..f6aadf26e95a 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -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", @@ -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", diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/instrument-no-bootstrap.mjs b/dev-packages/node-integration-tests/suites/tracing/mastra/instrument-no-bootstrap.mjs new file mode 100644 index 000000000000..f4ebcd4a74cc --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/instrument-no-bootstrap.mjs @@ -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; + }, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/mastra/instrument-with-pii.mjs new file mode 100644 index 000000000000..6cd8bfaa55bc --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/instrument-with-pii.mjs @@ -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; + }, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mastra/instrument.mjs new file mode 100644 index 000000000000..ec49598a39c9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/instrument.mjs @@ -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; + }, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-auto.mjs b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-auto.mjs new file mode 100644 index 000000000000..41e227443855 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-auto.mjs @@ -0,0 +1,72 @@ +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(); + await Sentry.flush(2000); + provider.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-community-exporter.mjs b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-community-exporter.mjs new file mode 100644 index 000000000000..8826576e8efe --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-community-exporter.mjs @@ -0,0 +1,86 @@ +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 = { + 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(); + await Sentry.flush(2000); + provider.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-tools.mjs b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-tools.mjs new file mode 100644 index 000000000000..520e0d6eeb26 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-tools.mjs @@ -0,0 +1,103 @@ +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(); + await Sentry.flush(2000); + provider.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-workflow.mjs b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-workflow.mjs new file mode 100644 index 000000000000..7f9d3c72e911 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario-workflow.mjs @@ -0,0 +1,47 @@ +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(); + await Sentry.flush(2000); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario.mjs new file mode 100644 index 000000000000..c417d48c19c9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/scenario.mjs @@ -0,0 +1,82 @@ +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'; +import { SentryMastraExporter } from '@sentry/node'; + +// 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, + 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('What is the weather in Berlin?'); + }); + + await mastra.observability.shutdown(); + await Sentry.flush(2000); + provider.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts new file mode 100644 index 000000000000..9e8c7438de03 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -0,0 +1,210 @@ +import { + GEN_AI_AGENT_NAME, + GEN_AI_INPUT_MESSAGES, + GEN_AI_OPERATION_NAME, + GEN_AI_OUTPUT_MESSAGES, + GEN_AI_PIPELINE_NAME, + GEN_AI_PROVIDER_NAME, + GEN_AI_REQUEST_MODEL, + GEN_AI_RESPONSE_MODEL, + GEN_AI_SYSTEM_INSTRUCTIONS, + GEN_AI_TOOL_CALL_ARGUMENTS, + GEN_AI_TOOL_CALL_RESULT, + GEN_AI_TOOL_DEFINITIONS, + GEN_AI_TOOL_NAME, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { afterAll, expect } from 'vitest'; +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; + +// `@mastra/core` declares `engines.node >= 22.13`, and the CI matrix still includes 20.19. +conditionalTest({ min: 22 })('Mastra integration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('creates invoke_agent and chat spans following the gen_ai conventions', async () => { + await createRunner() + .expect({ transaction: { transaction: 'mastra-test' } }) + .expect({ + span: container => { + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gpt-4o-mini', + 'invoke_agent weather_agent', + ]); + + const agentSpan = container.items.find(span => span.name === 'invoke_agent weather_agent')!; + expect(agentSpan.status).toBe('ok'); + expect(agentSpan.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(agentSpan.attributes['sentry.origin'].value).toBe('auto.ai.mastra'); + expect(agentSpan.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent'); + expect(agentSpan.attributes[GEN_AI_AGENT_NAME].value).toBe('weather_agent'); + expect(agentSpan.attributes[GEN_AI_PIPELINE_NAME].value).toBe('weather_agent'); + expect(agentSpan.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(12); + expect(agentSpan.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(7); + expect(agentSpan.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(19); + expect(agentSpan.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4o-mini'); + + const chatSpan = container.items.find(span => span.name === 'chat gpt-4o-mini')!; + expect(chatSpan.status).toBe('ok'); + expect(chatSpan.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(chatSpan.attributes['sentry.origin'].value).toBe('auto.ai.mastra'); + expect(chatSpan.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); + expect(chatSpan.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gpt-4o-mini'); + expect(chatSpan.attributes[GEN_AI_PROVIDER_NAME].value).toBe('openai'); + expect(chatSpan.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(19); + + expect(chatSpan.parent_span_id).toBe(agentSpan.span_id); + }, + }) + .start() + .completed(); + }); + + test('omits prompts and responses when genAI recording is off', async () => { + await createRunner() + .expect({ transaction: { transaction: 'mastra-test' } }) + .expect({ + span: container => { + for (const span of container.items) { + expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); + expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); + expect(span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]).toBeUndefined(); + } + }, + }) + .start() + .completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('records prompts and responses when genAI recording is on', async () => { + await createRunner() + .expect({ transaction: { transaction: 'mastra-test' } }) + .expect({ + span: container => { + const agentSpan = container.items.find(span => span.name === 'invoke_agent weather_agent')!; + expect(agentSpan.attributes[GEN_AI_SYSTEM_INSTRUCTIONS].value).toBe('You report the weather.'); + expect(agentSpan.attributes[GEN_AI_INPUT_MESSAGES].value).toContain('What is the weather in Berlin?'); + expect(agentSpan.attributes[GEN_AI_OUTPUT_MESSAGES].value).toContain('It is 22C in Berlin.'); + + const chatSpan = container.items.find(span => span.name === 'chat gpt-4o-mini')!; + expect(chatSpan.attributes[GEN_AI_INPUT_MESSAGES].value).toContain('What is the weather in Berlin?'); + expect(chatSpan.attributes[GEN_AI_OUTPUT_MESSAGES].value).toContain('It is 22C in Berlin.'); + }, + }) + .start() + .completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('creates an execute_tool span parented onto the generation', async () => { + await createRunner() + .expect({ transaction: { transaction: 'mastra-test' } }) + .expect({ + span: container => { + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gpt-4o-mini', + 'execute_tool get_weather', + 'invoke_agent weather_agent', + ]); + + const toolSpan = container.items.find(span => span.name === 'execute_tool get_weather')!; + expect(toolSpan.status).toBe('ok'); + expect(toolSpan.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolSpan.attributes['sentry.origin'].value).toBe('auto.ai.mastra'); + expect(toolSpan.attributes[GEN_AI_OPERATION_NAME].value).toBe('execute_tool'); + expect(toolSpan.attributes[GEN_AI_TOOL_NAME].value).toBe('get_weather'); + expect(toolSpan.attributes[GEN_AI_TOOL_CALL_ARGUMENTS].value).toContain('Berlin'); + expect(toolSpan.attributes[GEN_AI_TOOL_CALL_RESULT].value).toContain('22'); + + const chatSpan = container.items.find(span => span.name === 'chat gpt-4o-mini')!; + expect(toolSpan.parent_span_id).toBe(chatSpan.span_id); + + const agentSpan = container.items.find(span => span.name === 'invoke_agent weather_agent')!; + expect(agentSpan.attributes[GEN_AI_TOOL_DEFINITIONS].value).toContain('get_weather'); + expect(agentSpan.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(50); + expect(agentSpan.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(13); + expect(agentSpan.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(63); + }, + }) + .start() + .completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-workflow.mjs', 'instrument.mjs', (createRunner, test) => { + test('exports the workflow run but drops workflow steps, which have no conventional op', async () => { + await createRunner() + .expect({ transaction: { transaction: 'mastra-test' } }) + .expect({ + span: container => { + expect(container.items.map(span => span.name)).toEqual(['invoke_agent math_workflow']); + + const workflowSpan = container.items[0]!; + expect(workflowSpan.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(workflowSpan.attributes[GEN_AI_PIPELINE_NAME].value).toBe('math_workflow'); + }, + }) + .start() + .completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-auto.mjs', 'instrument.mjs', (createRunner, test) => { + test('auto-instruments a Mastra app that configured no observability at all', async () => { + await createRunner() + .expect({ transaction: { transaction: 'mastra-test' } }) + .expect({ + span: container => { + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gpt-4o-mini', + 'invoke_agent weather_agent', + ]); + for (const span of container.items) { + expect(span.attributes['sentry.origin'].value).toBe('auto.ai.mastra'); + } + }, + }) + .start() + .completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-community-exporter.mjs', 'instrument.mjs', (createRunner, test) => { + test('still exports its own spans when the community @mastra/sentry exporter is present', async () => { + await createRunner() + .expect({ transaction: { transaction: 'mastra-test' } }) + .expect({ + span: container => { + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gpt-4o-mini', + 'invoke_agent weather_agent', + ]); + }, + }) + .start() + .completed(); + }); + }); + createEsmAndCjsTests(__dirname, 'scenario-auto.mjs', 'instrument-no-bootstrap.mjs', (createRunner, test) => { + test('leaves an unconfigured app alone when bootstrapObservability is false', async () => { + await createRunner() + .expect({ + transaction: event => { + expect(event.transaction).toBe('mastra-test'); + const genAiSpans = (event.spans ?? []).filter(span => span.op?.startsWith('gen_ai.')); + expect(genAiSpans).toEqual([]); + }, + }) + .start() + .completed(); + }); + }); +}); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 7e34a1727574..c65d5f937be9 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -94,6 +94,8 @@ export { openAIIntegration, langChainIntegration, langGraphIntegration, + mastraIntegration, + SentryMastraExporter, parameterize, pinoIntegration, postgresIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index d48be039ba2f..959f9e6672e8 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -62,6 +62,8 @@ export { openAIIntegration, langChainIntegration, langGraphIntegration, + mastraIntegration, + SentryMastraExporter, modulesIntegration, nodeRuntimeMetricsIntegration, type NodeRuntimeMetricsOptions, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 5bae62b97c69..5e1e373560b8 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -84,6 +84,8 @@ export { openAIIntegration, langChainIntegration, langGraphIntegration, + mastraIntegration, + SentryMastraExporter, modulesIntegration, contextLinesIntegration, nodeContextIntegration, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index aa3fa1babba8..4c3766c6ee04 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -132,6 +132,8 @@ export { koaIntegration, langChainIntegration, langGraphIntegration, + mastraIntegration, + SentryMastraExporter, lruMemoizerIntegration, mongoIntegration, mongooseIntegration, diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 4ce1aea47cd0..9c0b54cd65da 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -79,6 +79,7 @@ snapshot[`captureMessage 1`] = ` "Aws", "LangChain", "LangGraph", + "Mastra", "VercelAI", "OpenAI", "Anthropic_AI", @@ -186,6 +187,7 @@ snapshot[`captureMessage twice 1`] = ` "Aws", "LangChain", "LangGraph", + "Mastra", "VercelAI", "OpenAI", "Anthropic_AI", @@ -300,6 +302,7 @@ snapshot[`captureMessage twice 2`] = ` "Aws", "LangChain", "LangGraph", + "Mastra", "VercelAI", "OpenAI", "Anthropic_AI", diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index dcdb8b70c135..904b707b1a37 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -63,6 +63,8 @@ export { openAIIntegration, langChainIntegration, langGraphIntegration, + mastraIntegration, + SentryMastraExporter, modulesIntegration, contextLinesIntegration, nodeContextIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 49f441e653d1..d5e15c366af0 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -62,6 +62,8 @@ export { openAIIntegration, langChainIntegration, langGraphIntegration, + mastraIntegration, + SentryMastraExporter, modulesIntegration, nodeRuntimeMetricsIntegration, type NodeRuntimeMetricsOptions, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 3b18ac957f07..3b908df5e1db 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -20,6 +20,8 @@ export { langChainIntegration, langGraphIntegration, lruMemoizerIntegration, + mastraIntegration, + SentryMastraExporter, mongoIntegration, mongooseIntegration, mysqlIntegration, diff --git a/packages/server-utils/src/ai/index.ts b/packages/server-utils/src/ai/index.ts index 769ab826e01d..9fd995466027 100644 --- a/packages/server-utils/src/ai/index.ts +++ b/packages/server-utils/src/ai/index.ts @@ -10,3 +10,4 @@ export { instrumentGoogleGenAIClient } from './google-genai'; export { instrumentWorkersAiClient } from './workers-ai'; export { createLangChainCallbackHandler, instrumentLangChainEmbeddings } from './langchain'; export { instrumentStateGraph, instrumentStateGraphCompile, instrumentCreateReactAgent } from './langgraph'; +export { SentryMastraExporter } from './mastra'; diff --git a/packages/server-utils/src/ai/mastra/constants.ts b/packages/server-utils/src/ai/mastra/constants.ts new file mode 100644 index 000000000000..3b47f8c06338 --- /dev/null +++ b/packages/server-utils/src/ai/mastra/constants.ts @@ -0,0 +1,51 @@ +import { GEN_AI_CHAT, GEN_AI_EMBEDDINGS, GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; +import type { MastraSpanType } from './types'; + +export const MASTRA_INTEGRATION_NAME = 'Mastra' as const; + +export const MASTRA_ORIGIN = 'auto.ai.mastra'; + +/** Not `sentry` — that is the community `@mastra/sentry` exporter name. */ +export const MASTRA_EXPORTER_NAME = 'sentry-sdk'; + +/** Name used by the community `@mastra/sentry` package. */ +export const COMMUNITY_MASTRA_SENTRY_EXPORTER_NAME = 'sentry'; + +/** + * Distinguishes our exporter from the community one. `Symbol.for` so the check still matches if + * two copies of `@sentry/server-utils` are loaded (`instanceof` would not). + */ +export const MASTRA_EXPORTER_BRAND = Symbol.for('sentry.mastra.exporter'); + +/** + * Only conventional `gen_ai` ops. Unmapped Mastra types (workflow steps, processors, scorers, …) + * are dropped; their children re-parent onto the nearest mapped ancestor. + * + * `op` comes from `@sentry/conventions/op`. `operationName` is a `gen_ai.operation.name` literal — + * the conventions package has no constants for those. + */ +export const SPAN_TYPE_OPS: Readonly> = { + agent_run: { op: GEN_AI_INVOKE_AGENT, operationName: 'invoke_agent' }, + workflow_run: { op: GEN_AI_INVOKE_AGENT, operationName: 'invoke_agent' }, + model_generation: { op: GEN_AI_CHAT, operationName: 'chat' }, + tool_call: { op: GEN_AI_EXECUTE_TOOL, operationName: 'execute_tool' }, + mcp_tool_call: { op: GEN_AI_EXECUTE_TOOL, operationName: 'execute_tool' }, + provider_tool_call: { op: GEN_AI_EXECUTE_TOOL, operationName: 'execute_tool' }, + client_tool_call: { op: GEN_AI_EXECUTE_TOOL, operationName: 'execute_tool' }, + rag_embedding: { op: GEN_AI_EMBEDDINGS, operationName: 'embeddings' }, +}; + +export const TOOL_SPAN_TYPES: ReadonlySet = new Set([ + 'tool_call', + 'mcp_tool_call', + 'provider_tool_call', + 'client_tool_call', +]); + +/** + * `model_inference` is omitted: Mastra nests `model_generation > model_step > model_inference`, + * and the inference span repeats the generation — mapping both to `gen_ai.chat` duplicates it. + */ +export const MODEL_SPAN_TYPES: ReadonlySet = new Set(['model_generation']); + +export const AGENT_SPAN_TYPES: ReadonlySet = new Set(['agent_run', 'workflow_run']); diff --git a/packages/server-utils/src/ai/mastra/index.ts b/packages/server-utils/src/ai/mastra/index.ts new file mode 100644 index 000000000000..ae49c7fbd379 --- /dev/null +++ b/packages/server-utils/src/ai/mastra/index.ts @@ -0,0 +1,244 @@ +import type { Span } from '@sentry/core'; +import { + _INTERNAL_skipAiProviderWrapping, + debug, + flush as sentryFlush, + getActiveSpan, + getClient, + LRUMap, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_STATUS_ERROR, + startInactiveSpan, +} from '@sentry/core'; +import { GEN_AI_RESPONSE_MODEL } from '@sentry/conventions/attributes'; +import { DEBUG_BUILD } from '../../debug-build'; +import { ANTHROPIC_AI_INTEGRATION_NAME } from '../anthropic-ai/constants'; +import type { GenAiOptions } from '../core/utils'; +import { resolveAIRecordingOptions } from '../core/utils'; +import { GOOGLE_GENAI_INTEGRATION_NAME } from '../google-genai/constants'; +import { OPENAI_INTEGRATION_NAME } from '../openai/constants'; +import { + getOperation, + getSpanAttributes, + getSpanName, + getUsageAttributes, + isExportedSpanType, + mergeUsageAttributes, + type SpanAttributes, +} from './utils'; +import { + AGENT_SPAN_TYPES, + MASTRA_EXPORTER_BRAND, + MASTRA_EXPORTER_NAME, + MASTRA_ORIGIN, + MODEL_SPAN_TYPES, +} from './constants'; +import type { MastraExportedSpan, MastraObservabilityExporter, MastraSpanType, MastraTracingEvent } from './types'; + +export type MastraExporterOptions = GenAiOptions; + +interface TrackedSpan { + span: Span; + spanType: MastraSpanType; + usage: SpanAttributes; +} + +const FLUSH_TIMEOUT_MS = 2000; + +/** Bound on the skipped-span parent walk; a cyclic chain from Mastra would otherwise hang the process. */ +const MAX_PARENT_WALK_DEPTH = 100; + +/** Cap on tracked spans, matching `MAX_TRACKED_PRISMA_SPANS`. Spans that never end would otherwise leak. */ +const MAX_TRACKED_MASTRA_SPANS = 1000; + +/** Stored in `_skipped` for a dropped span with no parent, so `get` can distinguish it from absent. */ +const NO_PARENT = ''; + +// Same list and timing idea as LangChain: skip on the first exported event, not at +// `new Mastra()`, so a raw provider call before any Mastra run still gets its own span. +const SKIPPED_PROVIDERS = [OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAME, GOOGLE_GENAI_INTEGRATION_NAME]; + +/** + * Mastra `ObservabilityExporter` that turns tracing events into Sentry spans. + * + * Never calls `Sentry.init()` / `Sentry.close()` — the community `@mastra/sentry` package does both + * and replaces the app's client. No `@mastra/*` or `node:` imports. + */ +export class SentryMastraExporter implements MastraObservabilityExporter { + public name = MASTRA_EXPORTER_NAME; + + public readonly [MASTRA_EXPORTER_BRAND] = true; + + private readonly _spans = new LRUMap(MAX_TRACKED_MASTRA_SPANS); + /** Dropped span id -> parent id, for re-parenting descendants. */ + private readonly _skipped = new LRUMap(MAX_TRACKED_MASTRA_SPANS); + private readonly _options: MastraExporterOptions; + + public constructor(options: MastraExporterOptions = {}) { + this._options = options; + } + + /** Mastra's interface is async; the work is synchronous. */ + public async exportTracingEvent(event: MastraTracingEvent): Promise { + if (!getClient()) { + return; + } + + _INTERNAL_skipAiProviderWrapping(SKIPPED_PROVIDERS); + + try { + this._handleEvent(event); + } catch (error) { + DEBUG_BUILD && debug.error('[Mastra] failed to export tracing event', error); + } + } + + /** Flush without closing the client — the app still owns it. */ + public async flush(): Promise { + await sentryFlush(FLUSH_TIMEOUT_MS); + } + + /** End open spans, then flush. Does not close the Sentry client. */ + public async shutdown(): Promise { + for (const { span } of this._spans.values()) { + span.end(); + } + this._spans.clear(); + this._skipped.clear(); + await this.flush(); + } + + private _handleEvent(event: MastraTracingEvent): void { + const span = event.exportedSpan; + + if (span.isEvent) { + return; + } + + // Unmapped types are recorded so children re-parent onto a surviving ancestor. + if (!isExportedSpanType(span.type)) { + if (event.type === 'span_started') { + this._skipped.set(span.id, span.parentSpanId ?? NO_PARENT); + } else if (event.type === 'span_ended') { + this._skipped.remove(span.id); + } + return; + } + + switch (event.type) { + case 'span_started': + this._onSpanStarted(span); + break; + case 'span_updated': + this._onSpanUpdated(span); + break; + case 'span_ended': + this._onSpanEnded(span); + break; + } + } + + private _onSpanStarted(span: MastraExportedSpan): void { + const parentId = this._resolveParentId(span.parentSpanId); + const parentSpan = parentId ? this._spans.get(parentId)?.span : undefined; + const activeSpan = getActiveSpan(); + + const sentrySpan = startInactiveSpan({ + name: getSpanName(span), + startTime: span.startTime, + // Prefer the Mastra parent so the tree stays together; else the active request span. + parentSpan: parentSpan ?? activeSpan, + attributes: { + ...this._attributesFor(span), + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getOperation(span.type)?.op, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MASTRA_ORIGIN, + }, + }); + + this._trackSpan(span.id, { span: sentrySpan, spanType: span.type, usage: {} }); + } + + /** End the oldest Sentry span before `LRUMap` drops it without calling `end()`. */ + private _trackSpan(id: string, tracked: TrackedSpan): void { + const keys = this._spans.keys(); + if (!keys.includes(id) && keys.length >= MAX_TRACKED_MASTRA_SPANS) { + const oldestId = keys[0]; + if (oldestId !== undefined) { + this._spans.remove(oldestId)?.span.end(); + } + } + this._spans.set(id, tracked); + } + + private _onSpanUpdated(span: MastraExportedSpan): void { + const tracked = this._spans.get(span.id); + if (tracked) { + tracked.span.setAttributes(this._attributesFor(span)); + } + } + + private _onSpanEnded(span: MastraExportedSpan): void { + const tracked = this._spans.get(span.id); + if (!tracked) { + DEBUG_BUILD && debug.warn(`[Mastra] no Sentry span open for ended span ${span.id} (${span.name})`); + return; + } + + const { span: sentrySpan } = tracked; + sentrySpan.setAttributes(this._attributesFor(span)); + sentrySpan.updateName(getSpanName(span)); + + if (MODEL_SPAN_TYPES.has(span.type)) { + this._rollUpToParentAgent(span); + } + + if (span.errorInfo) { + // Status only — capturing here would duplicate the app's own report with a worse stack. + sentrySpan.setStatus({ code: SPAN_STATUS_ERROR, message: span.errorInfo.message }); + } + + sentrySpan.end(span.endTime); + this._spans.remove(span.id); + } + + /** Copy model/usage onto the parent agent so the AI Agents view shows totals. */ + private _rollUpToParentAgent(span: MastraExportedSpan): void { + const parentId = this._resolveParentId(span.parentSpanId); + const parent = parentId ? this._spans.get(parentId) : undefined; + if (!parent || !AGENT_SPAN_TYPES.has(parent.spanType)) { + return; + } + + const attrs = span.attributes ?? {}; + const rolled = mergeUsageAttributes(parent.usage, getUsageAttributes(attrs.usage)); + parent.usage = rolled; + const model = attrs.responseModel ?? attrs.model; + if (model !== undefined) { + rolled[GEN_AI_RESPONSE_MODEL] = model; + } + parent.span.setAttributes(rolled); + } + + /** Nearest exported ancestor of a dropped span. */ + private _resolveParentId(parentSpanId: string | undefined): string | undefined { + let current = parentSpanId; + for (let depth = 0; current; depth++) { + const parentOfSkipped = this._skipped.get(current); + if (parentOfSkipped === undefined) { + return current; + } + if (depth >= MAX_PARENT_WALK_DEPTH) { + DEBUG_BUILD && debug.warn('[Mastra] gave up walking a parent chain that is too deep or cyclic'); + return undefined; + } + current = parentOfSkipped === NO_PARENT ? undefined : parentOfSkipped; + } + return undefined; + } + + private _attributesFor(span: MastraExportedSpan): ReturnType { + // Per event: the exporter can be constructed before `Sentry.init()`, when `dataCollection.genAI` does not exist yet. + return getSpanAttributes(span, resolveAIRecordingOptions(this._options)); + } +} diff --git a/packages/server-utils/src/ai/mastra/types.ts b/packages/server-utils/src/ai/mastra/types.ts new file mode 100644 index 000000000000..35d125644346 --- /dev/null +++ b/packages/server-utils/src/ai/mastra/types.ts @@ -0,0 +1,100 @@ +/** + * Duck-typed copies of the `@mastra/core/observability` shapes we read. No `@mastra/*` dependency. + */ + +/** + * Span types this exporter maps onto a `gen_ai` op. Everything else — present or future — arrives + * as the open `string` arm and is dropped. + */ +export type MastraSpanType = + | 'agent_run' + | 'workflow_run' + | 'model_generation' + | 'tool_call' + | 'mcp_tool_call' + | 'provider_tool_call' + | 'client_tool_call' + | 'rag_embedding' + | (string & {}); + +export type MastraTracingEventType = 'span_started' | 'span_updated' | 'span_ended'; + +export interface MastraUsageStats { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + inputDetails?: { + cacheRead?: number; + cacheWrite?: number; + }; + outputDetails?: { + reasoning?: number; + }; +} + +export interface MastraErrorInfo { + id?: string; + name?: string; + message: string; +} + +/** Fields that map onto a Sentry convention attribute. Everything else Mastra sets is ignored. */ +export interface MastraSpanAttributes { + // model_generation / rag_embedding + model?: string; + provider?: string; + responseModel?: string; + responseId?: string; + finishReason?: string; + streaming?: boolean; + usage?: MastraUsageStats; + parameters?: { + temperature?: number; + maxOutputTokens?: number; + topP?: number; + topK?: number; + presencePenalty?: number; + frequencyPenalty?: number; + stopSequences?: string[]; + seed?: number; + }; + // tool_call / mcp_tool_call / provider_tool_call / client_tool_call + toolDescription?: string; + // agent_run / workflow_run + instructions?: string; + prompt?: string; + conversationId?: string; + availableTools?: unknown; +} + +export interface MastraExportedSpan { + id: string; + parentSpanId?: string; + /** Mastra's own name; unused for Sentry span names. */ + name: string; + type: MastraSpanType; + startTime: Date; + endTime?: Date; + /** Point-in-time marker with no duration; dropped. */ + isEvent?: boolean; + entityId?: string; + entityName?: string; + input?: unknown; + output?: unknown; + attributes?: MastraSpanAttributes; + metadata?: { threadId?: string; runId?: string }; + errorInfo?: MastraErrorInfo; +} + +export interface MastraTracingEvent { + type: MastraTracingEventType; + exportedSpan: MastraExportedSpan; +} + +/** Subset of Mastra's `ObservabilityExporter`. The rest of that interface is optional. */ +export interface MastraObservabilityExporter { + name: string; + exportTracingEvent(event: MastraTracingEvent): Promise; + flush(): Promise; + shutdown(): Promise; +} diff --git a/packages/server-utils/src/ai/mastra/utils.ts b/packages/server-utils/src/ai/mastra/utils.ts new file mode 100644 index 000000000000..5d389ce781e2 --- /dev/null +++ b/packages/server-utils/src/ai/mastra/utils.ts @@ -0,0 +1,253 @@ +import type { SpanAttributeValue } from '@sentry/core'; +import { stringify } from '@sentry/core'; +import { + ERROR_TYPE, + GEN_AI_AGENT_NAME, + GEN_AI_CONVERSATION_ID, + GEN_AI_INPUT_MESSAGES, + GEN_AI_OPERATION_NAME, + GEN_AI_OUTPUT_MESSAGES, + GEN_AI_PIPELINE_NAME, + GEN_AI_PROVIDER_NAME, + GEN_AI_REQUEST_FREQUENCY_PENALTY, + GEN_AI_REQUEST_MAX_TOKENS, + GEN_AI_REQUEST_MODEL, + GEN_AI_REQUEST_PRESENCE_PENALTY, + GEN_AI_REQUEST_SEED, + GEN_AI_REQUEST_STOP_SEQUENCES, + GEN_AI_REQUEST_TEMPERATURE, + GEN_AI_REQUEST_TOP_K, + GEN_AI_REQUEST_TOP_P, + GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_RESPONSE_ID, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, + GEN_AI_RESPONSE_TEXT, + GEN_AI_SYSTEM_INSTRUCTIONS, + GEN_AI_TOOL_CALL_ARGUMENTS, + GEN_AI_TOOL_CALL_RESULT, + GEN_AI_TOOL_DEFINITIONS, + GEN_AI_TOOL_DESCRIPTION, + GEN_AI_TOOL_NAME, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { AGENT_SPAN_TYPES, MODEL_SPAN_TYPES, SPAN_TYPE_OPS, TOOL_SPAN_TYPES } from './constants'; +import type { MastraExportedSpan, MastraSpanType, MastraUsageStats } from './types'; + +export interface AttributeRecordingOptions { + recordInputs: boolean; + recordOutputs: boolean; +} + +export type SpanAttributes = Record; + +export function isExportedSpanType(spanType: MastraSpanType): boolean { + return spanType in SPAN_TYPE_OPS; +} + +export function getOperation(spanType: MastraSpanType): { op: string; operationName: string } | undefined { + return SPAN_TYPE_OPS[spanType]; +} + +/** + * `{operation} {identifier}` per the gen_ai name templates. Mastra's own span name is never used. + * https://getsentry.github.io/sentry-conventions/names/ + */ +export function getSpanName(span: MastraExportedSpan): string { + const operationName = getOperation(span.type)?.operationName ?? span.type; + const identifier = + MODEL_SPAN_TYPES.has(span.type) || span.type === 'rag_embedding' + ? span.attributes?.model + : (span.entityName ?? span.entityId); + + return identifier ? `${operationName} ${identifier}` : operationName; +} + +function serialize(value: unknown): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + return typeof value === 'string' ? value : stringify(value); +} + +function positive(value: number | undefined): number | undefined { + return value !== undefined && value > 0 ? value : undefined; +} + +export function getUsageAttributes(usage: MastraUsageStats | undefined): SpanAttributes { + if (!usage) { + return {}; + } + + const inputTokens = usage.inputTokens; + const outputTokens = usage.outputTokens; + // Mastra only reports `totalTokens` for some providers, so derive it when both halves are known. + const totalTokens = + usage.totalTokens ?? + (inputTokens !== undefined || outputTokens !== undefined ? (inputTokens ?? 0) + (outputTokens ?? 0) : undefined); + + return { + [GEN_AI_USAGE_INPUT_TOKENS]: inputTokens, + [GEN_AI_USAGE_OUTPUT_TOKENS]: outputTokens, + [GEN_AI_USAGE_TOTAL_TOKENS]: totalTokens, + // Mastra reports these as 0 for providers that don't cache or reason. + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: positive(usage.inputDetails?.cacheRead), + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: positive(usage.inputDetails?.cacheWrite), + [GEN_AI_USAGE_REASONING_OUTPUT_TOKENS]: positive(usage.outputDetails?.reasoning), + }; +} + +const USAGE_ATTRIBUTE_KEYS = [ + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, +] as const; + +function addTokenCounts( + left: SpanAttributeValue | undefined, + right: SpanAttributeValue | undefined, +): number | undefined { + const a = typeof left === 'number' ? left : undefined; + const b = typeof right === 'number' ? right : undefined; + if (a === undefined && b === undefined) { + return undefined; + } + return (a ?? 0) + (b ?? 0); +} + +/** Sum usage across generations. `setAttributes(undefined)` would drop the previous total. */ +export function mergeUsageAttributes(existing: SpanAttributes, incoming: SpanAttributes): SpanAttributes { + const merged: SpanAttributes = {}; + for (const key of USAGE_ATTRIBUTE_KEYS) { + const value = addTokenCounts(existing[key], incoming[key]); + if (value !== undefined) { + merged[key] = value; + } + } + return merged; +} + +/** Convention attributes only. Called on start and end — Mastra fills fields progressively. */ +export function getSpanAttributes(span: MastraExportedSpan, options: AttributeRecordingOptions): SpanAttributes { + const operationName = getOperation(span.type)?.operationName; + + const attributes: SpanAttributes = { + [GEN_AI_OPERATION_NAME]: operationName, + [GEN_AI_CONVERSATION_ID]: span.attributes?.conversationId ?? span.metadata?.threadId, + }; + + if (AGENT_SPAN_TYPES.has(span.type)) { + addAgentAttributes(attributes, span, options); + } + + if (MODEL_SPAN_TYPES.has(span.type) || span.type === 'rag_embedding') { + addModelAttributes(attributes, span, options); + } + + if (TOOL_SPAN_TYPES.has(span.type)) { + addToolAttributes(attributes, span, options); + } + + if (span.errorInfo) { + attributes[ERROR_TYPE] = span.errorInfo.name ?? span.errorInfo.id; + } + + return attributes; +} + +function addAgentAttributes( + attributes: SpanAttributes, + span: MastraExportedSpan, + options: AttributeRecordingOptions, +): void { + const attrs = span.attributes ?? {}; + const name = span.entityName ?? span.entityId; + + attributes[GEN_AI_AGENT_NAME] = name; + attributes[GEN_AI_PIPELINE_NAME] = name; + attributes[GEN_AI_TOOL_DEFINITIONS] = serialize(attrs.availableTools); + + if (options.recordInputs) { + // `prompt` is Mastra's legacy alias for the system prompt. + attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = attrs.instructions ?? attrs.prompt; + attributes[GEN_AI_INPUT_MESSAGES] = serialize(span.input); + } + if (options.recordOutputs) { + attributes[GEN_AI_OUTPUT_MESSAGES] = serialize(span.output); + // oxlint-disable-next-line typescript/no-deprecated -- still emitted by the other AI integrations + attributes[GEN_AI_RESPONSE_TEXT] = responseText(span.output); + } +} + +function addModelAttributes( + attributes: SpanAttributes, + span: MastraExportedSpan, + options: AttributeRecordingOptions, +): void { + const attrs = span.attributes ?? {}; + const parameters = attrs.parameters ?? {}; + + attributes[GEN_AI_REQUEST_MODEL] = attrs.model; + attributes[GEN_AI_PROVIDER_NAME] = attrs.provider; + attributes[GEN_AI_RESPONSE_MODEL] = attrs.responseModel; + attributes[GEN_AI_RESPONSE_ID] = attrs.responseId; + attributes[GEN_AI_RESPONSE_STREAMING] = attrs.streaming; + attributes[GEN_AI_RESPONSE_FINISH_REASONS] = attrs.finishReason ? JSON.stringify([attrs.finishReason]) : undefined; + attributes[GEN_AI_AGENT_NAME] = span.entityName; + + attributes[GEN_AI_REQUEST_TEMPERATURE] = parameters.temperature; + attributes[GEN_AI_REQUEST_MAX_TOKENS] = parameters.maxOutputTokens; + attributes[GEN_AI_REQUEST_TOP_P] = parameters.topP; + attributes[GEN_AI_REQUEST_TOP_K] = parameters.topK; + attributes[GEN_AI_REQUEST_PRESENCE_PENALTY] = parameters.presencePenalty; + attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY] = parameters.frequencyPenalty; + attributes[GEN_AI_REQUEST_SEED] = parameters.seed; + attributes[GEN_AI_REQUEST_STOP_SEQUENCES] = serialize(parameters.stopSequences); + + Object.assign(attributes, getUsageAttributes(attrs.usage)); + + if (options.recordInputs) { + attributes[GEN_AI_INPUT_MESSAGES] = serialize(span.input); + } + if (options.recordOutputs) { + attributes[GEN_AI_OUTPUT_MESSAGES] = serialize(span.output); + // oxlint-disable-next-line typescript/no-deprecated -- still emitted by the other AI integrations + attributes[GEN_AI_RESPONSE_TEXT] = responseText(span.output); + } +} + +function addToolAttributes( + attributes: SpanAttributes, + span: MastraExportedSpan, + options: AttributeRecordingOptions, +): void { + const attrs = span.attributes ?? {}; + + attributes[GEN_AI_TOOL_NAME] = span.entityName ?? span.entityId; + attributes[GEN_AI_TOOL_DESCRIPTION] = attrs.toolDescription; + + if (options.recordInputs) { + attributes[GEN_AI_TOOL_CALL_ARGUMENTS] = serialize(span.input); + } + if (options.recordOutputs) { + attributes[GEN_AI_TOOL_CALL_RESULT] = serialize(span.output); + } +} + +function responseText(output: unknown): string | undefined { + if (typeof output === 'string') { + return output; + } + if (output && typeof output === 'object' && 'text' in output && typeof output.text === 'string') { + return output.text; + } + return undefined; +} diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index e772e02c5c3d..2f918e2d5422 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -40,6 +40,8 @@ export { kafkaIntegration } from './integrations/kafkajs'; export { knexIntegration } from './integrations/knex'; export { langChainIntegration } from './integrations/langchain'; export { langGraphIntegration } from './integrations/langgraph'; +export { mastraIntegration } from './integrations/mastra'; +export { SentryMastraExporter } from './ai/mastra'; export { lruMemoizerIntegration } from './integrations/lru-memoizer'; export { mongoIntegration } from './integrations/mongodb'; export { mongooseIntegration } from './integrations/mongoose'; diff --git a/packages/server-utils/src/integrations/index.ts b/packages/server-utils/src/integrations/index.ts index 22bd0f0f1664..849a873cb1e3 100644 --- a/packages/server-utils/src/integrations/index.ts +++ b/packages/server-utils/src/integrations/index.ts @@ -13,6 +13,7 @@ import { mongooseIntegration } from './mongoose'; import { lruMemoizerIntegration } from './lru-memoizer'; import { langChainIntegration } from './langchain'; import { langGraphIntegration } from './langgraph'; +import { mastraIntegration } from './mastra'; import { vercelAIIntegration } from './vercel-ai'; import { openAIIntegration } from './openai'; import { anthropicAIIntegration } from './anthropic'; @@ -47,6 +48,7 @@ export function getTracingIntegrations(): Integration[] { // LangChain must come first to disable AI provider integrations before they instrument langChainIntegration(), langGraphIntegration(), + mastraIntegration(), vercelAIIntegration(), openAIIntegration(), anthropicAIIntegration(), diff --git a/packages/server-utils/src/integrations/mastra.ts b/packages/server-utils/src/integrations/mastra.ts new file mode 100644 index 000000000000..f141748b45be --- /dev/null +++ b/packages/server-utils/src/integrations/mastra.ts @@ -0,0 +1,261 @@ +import * as diagnosticsChannel from 'node:diagnostics_channel'; +import { createRequire } from 'node:module'; +import { join } from 'node:path'; +import type { IntegrationFn } from '@sentry/core'; +import { consoleSandbox, debug, defineIntegration } from '@sentry/core'; +import { + COMMUNITY_MASTRA_SENTRY_EXPORTER_NAME, + MASTRA_EXPORTER_BRAND, + MASTRA_INTEGRATION_NAME, +} from '../ai/mastra/constants'; +import { SentryMastraExporter } from '../ai/mastra'; +import type { MastraExporterOptions } from '../ai/mastra'; +import type { MastraObservabilityExporter } from '../ai/mastra/types'; +import { DEBUG_BUILD } from '../debug-build'; +import { CHANNELS } from '../orchestrion/channels'; +import { mastraModuleNames } from '../orchestrion/config/mastra'; +import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; + +export interface MastraOptions extends MastraExporterOptions { + /** + * Construct a Mastra observability pipeline when the app has not configured one. Defaults to + * `true`. Uses an `@mastra/observability` the app already has; the SDK never installs it. + */ + bootstrapObservability?: boolean; +} + +interface MastraObservabilityInstance { + getExporters?: () => readonly { name?: string; [MASTRA_EXPORTER_BRAND]?: boolean }[]; +} + +interface MastraInstance { + registerExporter?: (exporter: unknown, instance: unknown, entrypoint: unknown) => void; + observability?: { getDefaultInstance?: () => MastraObservabilityInstance | undefined }; +} + +interface ConstructorChannelContext { + arguments: unknown[]; + self?: unknown; +} + +// `registerExporter` does not dedupe; WeakSet so short-lived instances stay collectable. +const registered = new WeakSet(); + +// Process-wide: several Mastra instances should not reprint this. +let warnedAboutCommunityExporter = false; +let warnedAboutMissingObservability = false; + +const _mastraIntegration = ((options: MastraOptions = {}) => { + return { + name: MASTRA_INTEGRATION_NAME, + setup(client) { + // The subscriber opens no spans, so a missing async-context binding must not defer it. + invokeOrchestrionInstrumentation(client, mastraModuleNames, instrumentMastra, [options], { + requiresTracingChannelBinding: false, + }); + }, + }; +}) satisfies IntegrationFn; + +function instrumentMastra(options: MastraOptions): void { + diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.subscribe(message => { + try { + const { self } = message as ConstructorChannelContext; + attachExporter(self, options); + } catch (error) { + DEBUG_BUILD && debug.error('[instrumentation:mastra] failed to register the Sentry exporter', error); + } + }); +} + +function attachExporter(instance: unknown, options: MastraOptions): void { + if (typeof instance !== 'object' || instance === null || registered.has(instance)) { + return; + } + + const mastra = instance as MastraInstance; + if (typeof mastra.registerExporter !== 'function') { + DEBUG_BUILD && + debug.log('[instrumentation:mastra] `Mastra.registerExporter` is missing; needs @mastra/core >= 1.63.2'); + return; + } + + const { bootstrapObservability: _bootstrapObservability, ...exporterOptions } = options; + const exporter = new SentryMastraExporter(exporterOptions); + + const defaultInstance = mastra.observability?.getDefaultInstance?.(); + if (defaultInstance) { + const exporters = defaultInstance.getExporters?.() ?? []; + + if (exporters.some(registeredExporter => registeredExporter?.[MASTRA_EXPORTER_BRAND])) { + markAttached(instance); + DEBUG_BUILD && debug.log('[instrumentation:mastra] a Sentry exporter is already registered, skipping'); + return; + } + + // Community exporter already called `Sentry.init()` and cannot be removed (add-only API). + if (exporters.some(registeredExporter => registeredExporter?.name === COMMUNITY_MASTRA_SENTRY_EXPORTER_NAME)) { + warnAboutCommunityExporter(); + } + + mastra.registerExporter(exporter, undefined, undefined); + markAttached(instance); + return; + } + + if (options.bootstrapObservability === false) { + DEBUG_BUILD && + debug.log('[instrumentation:mastra] no observability configured and bootstrapping is disabled, skipping'); + return; + } + + const bootstrap = createObservabilityBootstrap(exporter); + if (!bootstrap) { + return; + } + + mastra.registerExporter(exporter, bootstrap.instance, bootstrap.entrypoint); + markAttached(instance); +} + +function markAttached(instance: object): void { + registered.add(instance); +} + +function warnAboutMissingObservability(): void { + if (warnedAboutMissingObservability) { + return; + } + warnedAboutMissingObservability = true; + + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] Mastra has no observability pipeline and `@mastra/observability` could not be loaded, ' + + 'so the Mastra integration will not create spans. Install `@mastra/observability`, or pass an ' + + '`Observability` to the `Mastra` constructor. Disable this with ' + + '`mastraIntegration({ bootstrapObservability: false })`.', + ); + }); +} + +/** `console.warn` rather than `debug`: the app's Sentry client has been replaced. */ +function warnAboutCommunityExporter(): void { + if (warnedAboutCommunityExporter) { + return; + } + warnedAboutCommunityExporter = true; + + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] The `@mastra/sentry` package is registered alongside the built-in Mastra integration, ' + + 'and both are now exporting spans. Its exporter also calls `Sentry.init()` itself: depending on which ' + + '`@sentry/node` version it resolves to, that either replaces the client configured in your ' + + '`instrument.ts` — losing your release, environment, integrations and sampling — or starts a second, ' + + 'independent SDK whose events never reach yours. Remove `@mastra/sentry` from your Mastra `exporters`; ' + + 'the Sentry SDK instruments Mastra on its own.', + ); + }); +} + +function appRequire(): ReturnType { + return createRequire(join(process.cwd(), 'noop.js')); +} + +/** + * `@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`. + */ +function findLoadedMastraCoreFilename(): string | undefined { + const cache = appRequire().cache; + if (!cache) { + return undefined; + } + + for (const filename of Object.keys(cache)) { + if (filename.replace(/\\/g, '/').includes('/@mastra/core/')) { + return filename; + } + } + + return undefined; +} + +function tryRequireObservability(parent: string): Record | undefined { + try { + return createRequire(parent)('@mastra/observability') as Record; + } catch { + return undefined; + } +} + +/** + * 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`. + */ +function loadMastraObservability(): Record { + const parents = new Set(); + const loadedCore = findLoadedMastraCoreFilename(); + if (loadedCore) { + parents.add(loadedCore); + } + try { + parents.add(appRequire().resolve('@mastra/core')); + } catch { + // cwd is not the app, or `@mastra/core` is ESM-only and not in the CJS resolver. + } + parents.add(join(process.cwd(), 'noop.js')); + + for (const parent of parents) { + const observability = tryRequireObservability(parent); + if (observability) { + return observability; + } + } + + throw new Error('Cannot resolve @mastra/observability'); +} + +/** + * Mastra's default is a no-op with no pipeline. `@mastra/observability` is not a dependency of + * `@mastra/core`, so this no-ops if the app has not installed it. + */ +function createObservabilityBootstrap( + exporter: MastraObservabilityExporter, +): { instance: unknown; entrypoint: unknown } | undefined { + let observabilityModule: Record; + try { + observabilityModule = loadMastraObservability(); + } catch { + warnAboutMissingObservability(); + return undefined; + } + + const Observability = observabilityModule.Observability as + | (new (config: unknown) => Record) + | undefined; + const DefaultObservabilityInstance = observabilityModule.DefaultObservabilityInstance as + | (new (config: unknown) => unknown) + | undefined; + + if (!Observability || !DefaultObservabilityInstance) { + DEBUG_BUILD && debug.log('[instrumentation:mastra] `@mastra/observability` is missing expected exports'); + return undefined; + } + + return { + instance: new DefaultObservabilityInstance({ serviceName: 'mastra', exporters: [exporter] }), + // Empty registry: `new Observability({ configs: { default } })` already registers + // `"default"`, and `Mastra.registerExporter` then throws `Tracing instance 'default' already registered`. + entrypoint: new Observability({}), + }; +} + +/** + * Hooks the `Mastra` constructor and registers a Sentry exporter via `registerExporter()`. + * Enabled by default. Disable with + * `defaultIntegrations: integrations => integrations.filter(i => i.name !== 'Mastra')`. + * Requires the runtime hook or bundler plugin, and `@mastra/core >= 1.63.2`. + */ +export const mastraIntegration = defineIntegration(_mastraIntegration); diff --git a/packages/server-utils/src/orchestrion/channels.ts b/packages/server-utils/src/orchestrion/channels.ts index 24a5ae35da0c..bc6d1b2524b9 100644 --- a/packages/server-utils/src/orchestrion/channels.ts +++ b/packages/server-utils/src/orchestrion/channels.ts @@ -15,6 +15,7 @@ import { koaChannels } from './config/koa'; import { langchainChannels } from './config/langchain'; import { langgraphChannels } from './config/langgraph'; import { lruMemoizerChannels } from './config/lru-memoizer'; +import { mastraChannels } from './config/mastra'; import { mongodbChannels } from './config/mongodb'; import { mongooseChannels } from './config/mongoose'; import { mysql2Channels } from './config/mysql2'; @@ -62,6 +63,7 @@ export const CHANNELS = { ...langchainChannels, ...langgraphChannels, ...lruMemoizerChannels, + ...mastraChannels, ...mongodbChannels, ...mongooseChannels, ...mysql2Channels, diff --git a/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts b/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts index 379b4406a017..d293b9d22baf 100644 --- a/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts +++ b/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts @@ -41,6 +41,7 @@ export const CHANNEL_INTEGRATION_DEFINITIONS = [ ], }, { exportName: 'langGraphIntegration', modules: ['@langchain/langgraph'] }, + { exportName: 'mastraIntegration', modules: ['@mastra/core'] }, { exportName: 'awsIntegration', modules: ['@aws-sdk/smithy-client', '@smithy/core', '@smithy/smithy-client'] }, { exportName: 'firebaseIntegration', modules: ['@firebase/firestore', 'firebase-functions'] }, { exportName: 'amqplibIntegration', modules: ['amqplib'] }, diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index 71b7d7100a8b..1fec4fb2c5ad 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -19,6 +19,7 @@ import { koaConfig } from './koa'; import { langchainConfig } from './langchain'; import { langgraphConfig } from './langgraph'; import { lruMemoizerConfig } from './lru-memoizer'; +import { mastraConfig } from './mastra'; import { mongodbConfig } from './mongodb'; import { mongooseConfig } from './mongoose'; import { mysql2Config } from './mysql2'; @@ -65,6 +66,7 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ ...langchainConfig, ...langgraphConfig, ...lruMemoizerConfig, + ...mastraConfig, ...mongodbConfig, ...mongooseConfig, ...mysql2Config, diff --git a/packages/server-utils/src/orchestrion/config/mastra.ts b/packages/server-utils/src/orchestrion/config/mastra.ts new file mode 100644 index 000000000000..7f54a84f1066 --- /dev/null +++ b/packages/server-utils/src/orchestrion/config/mastra.ts @@ -0,0 +1,27 @@ +import type { InstrumentationConfig } from '../apmTypes'; + +import { getModuleNames } from './module-names'; + +// `class Mastra` lives only in tsdown's content-hashed chunk (`dist/mastra-RpLTNzL-.js` in 1.63.2). +// The stable files (`dist/index.js`, `dist/mastra/index.js`) are one-line re-exports, so an exact +// `filePath` cannot wrap the constructor. `registerExporter()` exists since 1.63.2. +const mastraConstructorConfig: InstrumentationConfig[] = [ + { + channelName: 'mastraConstructor', + module: { + name: '@mastra/core', + versionRange: '>=1.63.2 <2.0.0', + filePath: /^dist\/mastra-[A-Za-z0-9_-]+\.(cjs|mjs|js)$/, + }, + // No `methodName` → class constructor. The `end` message's `self` is the instance. + functionQuery: { className: 'Mastra' }, + }, +]; + +export const mastraConfig = mastraConstructorConfig satisfies InstrumentationConfig[]; + +export const mastraModuleNames = getModuleNames(mastraConfig); + +export const mastraChannels = { + MASTRA_CONSTRUCTOR: 'orchestrion:@mastra/core:mastraConstructor', +} as const; diff --git a/packages/server-utils/test/ai/lib/tracing/mastra-exporter.test.ts b/packages/server-utils/test/ai/lib/tracing/mastra-exporter.test.ts new file mode 100644 index 000000000000..186aeb4f4664 --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/mastra-exporter.test.ts @@ -0,0 +1,402 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import type { Span } from '@sentry/core'; +import { + _INTERNAL_clearAiProviderSkips, + _INTERNAL_shouldSkipAiProviderWrapping, + getMainCarrier, + setCurrentClient, + spanToStaticSpanJSON, +} from '@sentry/core'; +import { SentryMastraExporter } from '../../../../src/ai/mastra'; +import type { MastraExportedSpan, MastraTracingEvent } from '../../../../src/ai/mastra/types'; +import { OPENAI_INTEGRATION_NAME } from '../../../../src/ai/openai/constants'; +import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; + +function makeSpan(overrides: Partial = {}): MastraExportedSpan { + return { + id: 'span-1', + name: 'agent run', + type: 'agent_run', + startTime: new Date('2026-01-01T00:00:00Z'), + endTime: new Date('2026-01-01T00:00:01Z'), + ...overrides, + }; +} + +function started(span: MastraExportedSpan): MastraTracingEvent { + return { type: 'span_started', exportedSpan: span }; +} + +function ended(span: MastraExportedSpan): MastraTracingEvent { + return { type: 'span_ended', exportedSpan: span }; +} + +describe('SentryMastraExporter', () => { + let endedSpans: Span[]; + let exporter: SentryMastraExporter; + + beforeEach(() => { + _INTERNAL_clearAiProviderSkips(); + getMainCarrier().__SENTRY__ = undefined; + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + traceLifecycle: 'stream', + }), + ); + setCurrentClient(client); + client.init(); + + endedSpans = []; + client.on('spanEnd', span => endedSpans.push(span)); + exporter = new SentryMastraExporter(); + }); + + afterEach(() => { + _INTERNAL_clearAiProviderSkips(); + getMainCarrier().__SENTRY__ = undefined; + }); + + async function run(...events: MastraTracingEvent[]): Promise { + for (const event of events) { + await exporter.exportTracingEvent(event); + } + } + + it('skips OpenAI wrapping on the first tracing event, not at construction', async () => { + expect(_INTERNAL_shouldSkipAiProviderWrapping(OPENAI_INTEGRATION_NAME)).toBe(false); + + await exporter.exportTracingEvent(started(makeSpan())); + + expect(_INTERNAL_shouldSkipAiProviderWrapping(OPENAI_INTEGRATION_NAME)).toBe(true); + }); + + it('names agent spans `invoke_agent {name}` and sets the gen_ai op', async () => { + const span = makeSpan({ entityName: 'weather_agent' }); + await run(started(span), ended(span)); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.description).toBe('invoke_agent weather_agent'); + expect(json.data['sentry.op']).toBe('gen_ai.invoke_agent'); + expect(json.data['sentry.origin']).toBe('auto.ai.mastra'); + expect(json.data['gen_ai.operation.name']).toBe('invoke_agent'); + expect(json.data['gen_ai.agent.name']).toBe('weather_agent'); + expect(json.data['gen_ai.pipeline.name']).toBe('weather_agent'); + }); + + it('names generation spans `chat {model}` and maps usage to the current conventions', async () => { + const span = makeSpan({ + id: 'gen-1', + type: 'model_generation', + name: 'generation', + attributes: { + model: 'gpt-5', + provider: 'openai', + usage: { + inputTokens: 10, + outputTokens: 4, + outputDetails: { reasoning: 3 }, + inputDetails: { cacheRead: 2, cacheWrite: 5 }, + }, + }, + }); + await run(started(span), ended(span)); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.description).toBe('chat gpt-5'); + expect(json.data['sentry.op']).toBe('gen_ai.chat'); + expect(json.data['gen_ai.provider.name']).toBe('openai'); + expect(json.data['gen_ai.usage.input_tokens']).toBe(10); + expect(json.data['gen_ai.usage.output_tokens']).toBe(4); + expect(json.data['gen_ai.usage.total_tokens']).toBe(14); + expect(json.data['gen_ai.usage.reasoning.output_tokens']).toBe(3); + expect(json.data['gen_ai.usage.cache_read.input_tokens']).toBe(2); + expect(json.data['gen_ai.usage.cache_creation.input_tokens']).toBe(5); + expect(json.data['gen_ai.usage.reasoning_tokens']).toBeUndefined(); + expect(json.data['gen_ai.response.text']).toBeUndefined(); + expect(json.data['gen_ai.response.tool_calls']).toBeUndefined(); + }); + + it('records the agent-level prompt and response as gen_ai messages', async () => { + const span = makeSpan({ + entityName: 'agent', + input: [{ role: 'user', content: 'hi' }], + output: { text: 'hello' }, + attributes: { instructions: 'be brief' }, + }); + await run(started(span), ended(span)); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.data['gen_ai.input.messages']).toBe('[{"role":"user","content":"hi"}]'); + expect(json.data['gen_ai.output.messages']).toBe('{"text":"hello"}'); + expect(json.data['gen_ai.response.text']).toBe('hello'); + expect(json.data['gen_ai.system_instructions']).toBe('be brief'); + }); + + it.each([ + ['a string output', 'hello', 'hello'], + ['a `{ text }` output', { text: 'hello' }, 'hello'], + ] as const)('sets gen_ai.response.text from %s on a generation span', async (_label, output, expected) => { + const span = makeSpan({ + id: 'gen-text', + type: 'model_generation', + name: 'generation', + output, + attributes: { model: 'gpt-5' }, + }); + await run(started(span), ended(span)); + + expect(spanToStaticSpanJSON(endedSpans[0]!).data['gen_ai.response.text']).toBe(expected); + }); + + it.each([ + [ + 'workflow_run', + { type: 'workflow_run' as const, entityName: 'math_workflow' }, + 'invoke_agent math_workflow', + 'gen_ai.invoke_agent', + ], + [ + 'rag_embedding', + { type: 'rag_embedding' as const, attributes: { model: 'text-embedding-3' } }, + 'embeddings text-embedding-3', + 'gen_ai.embeddings', + ], + [ + 'mcp_tool_call', + { type: 'mcp_tool_call' as const, entityName: 'search' }, + 'execute_tool search', + 'gen_ai.execute_tool', + ], + ['entityId when entityName is missing', { entityId: 'agent-42' }, 'invoke_agent agent-42', 'gen_ai.invoke_agent'], + ])('names a %s span', async (_label, overrides, description, op) => { + const span = makeSpan(overrides); + await run(started(span), ended(span)); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.description).toBe(description); + expect(json.data['sentry.op']).toBe(op); + }); + + it('omits inputs and outputs when recording is disabled', async () => { + exporter = new SentryMastraExporter({ recordInputs: false, recordOutputs: false }); + const span = makeSpan({ entityName: 'agent', input: { secret: 'pii' }, output: { text: 'pii' } }); + await run(started(span), ended(span)); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.data['gen_ai.input.messages']).toBeUndefined(); + expect(json.data['gen_ai.output.messages']).toBeUndefined(); + expect(JSON.stringify(json.data)).not.toContain('pii'); + }); + + it('re-parents a tool onto the generation across a dropped model_step', async () => { + const agent = makeSpan({ id: 'agent-1', entityName: 'agent' }); + const gen = makeSpan({ + id: 'gen-1', + type: 'model_generation', + parentSpanId: 'agent-1', + attributes: { model: 'gpt-5' }, + }); + const step = makeSpan({ id: 'step-1', type: 'model_step', parentSpanId: 'gen-1' }); + const tool = makeSpan({ + id: 'tool-1', + type: 'tool_call', + parentSpanId: 'step-1', + entityName: 'get_weather', + input: { city: 'Berlin' }, + }); + + await run( + started(agent), + started(gen), + started(step), + started(tool), + ended(tool), + ended(step), + ended(gen), + ended(agent), + ); + + const json = endedSpans.map(span => spanToStaticSpanJSON(span)); + const toolJson = json.find(span => span.description === 'execute_tool get_weather'); + const chatJson = json.find(span => span.description === 'chat gpt-5'); + expect(toolJson).toBeDefined(); + expect(chatJson).toBeDefined(); + expect(toolJson!.data['sentry.op']).toBe('gen_ai.execute_tool'); + expect(toolJson!.data['gen_ai.tool.name']).toBe('get_weather'); + expect(toolJson!.data['gen_ai.tool.call.arguments']).toBe('{"city":"Berlin"}'); + expect(toolJson!.parent_span_id).toBe(chatJson!.span_id); + }); + + it('rolls model usage up onto the parent agent', async () => { + const agent = makeSpan({ id: 'agent-1', entityName: 'agent' }); + const gen = makeSpan({ + id: 'gen-1', + type: 'model_generation', + parentSpanId: 'agent-1', + attributes: { + model: 'gpt-5', + responseModel: 'gpt-5-2026', + usage: { inputTokens: 10, outputTokens: 4 }, + }, + }); + + await run(started(agent), started(gen), ended(gen), ended(agent)); + + const agentJson = endedSpans + .map(span => spanToStaticSpanJSON(span)) + .find(span => span.description === 'invoke_agent agent'); + expect(agentJson).toBeDefined(); + expect(agentJson!.data['gen_ai.usage.input_tokens']).toBe(10); + expect(agentJson!.data['gen_ai.usage.output_tokens']).toBe(4); + expect(agentJson!.data['gen_ai.usage.total_tokens']).toBe(14); + expect(agentJson!.data['gen_ai.response.model']).toBe('gpt-5-2026'); + }); + + it('sums usage across multiple generations onto the parent agent', async () => { + const agent = makeSpan({ id: 'agent-1', entityName: 'agent' }); + const first = makeSpan({ + id: 'gen-1', + type: 'model_generation', + parentSpanId: 'agent-1', + attributes: { + model: 'gpt-5', + usage: { inputTokens: 10, outputTokens: 4, inputDetails: { cacheRead: 2 } }, + }, + }); + const second = makeSpan({ + id: 'gen-2', + type: 'model_generation', + parentSpanId: 'agent-1', + attributes: { + model: 'gpt-5', + responseModel: 'gpt-5-2026', + usage: { inputTokens: 30, outputTokens: 8, inputDetails: { cacheRead: 1 } }, + }, + }); + + await run(started(agent), started(first), ended(first), started(second), ended(second), ended(agent)); + + const agentJson = endedSpans + .map(span => spanToStaticSpanJSON(span)) + .find(span => span.description === 'invoke_agent agent'); + expect(agentJson).toBeDefined(); + expect(agentJson!.data['gen_ai.usage.input_tokens']).toBe(40); + expect(agentJson!.data['gen_ai.usage.output_tokens']).toBe(12); + expect(agentJson!.data['gen_ai.usage.total_tokens']).toBe(52); + expect(agentJson!.data['gen_ai.usage.cache_read.input_tokens']).toBe(3); + expect(agentJson!.data['gen_ai.response.model']).toBe('gpt-5-2026'); + }); + + it('marks the span errored without capturing a reconstructed exception', async () => { + const span = makeSpan({ errorInfo: { message: 'boom', name: 'ToolError' } }); + await run(started(span), ended(span)); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.status).toBe('internal_error'); + expect(json.data['error.type']).toBe('ToolError'); + }); + + it.each([ + ['model_chunk'], + ['model_step'], + ['model_inference'], + ['workflow_step'], + ['memory_operation'], + ['processor_run'], + ['scorer_run'], + ['generic'], + ['some_future_span_type'], + ])('drops %s spans, which have no conventional gen_ai op', async spanType => { + const span = makeSpan({ id: 'x-1', type: spanType, name: spanType }); + await run(started(span), ended(span)); + + expect(endedSpans).toHaveLength(0); + }); + + it('drops event spans, which have no duration', async () => { + const span = makeSpan({ isEvent: true, entityName: 'agent' }); + await run(started(span), ended(span)); + + expect(endedSpans).toHaveLength(0); + }); + + it('emits only conventional attributes', async () => { + const span = makeSpan({ + entityName: 'agent', + metadata: { threadId: 'thread-9' }, + }); + await run(started(span), ended(span)); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(Object.keys(json.data).filter(key => key.startsWith('mastra.'))).toEqual([]); + expect(json.data['gen_ai.conversation.id']).toBe('thread-9'); + }); + + it.each([ + [ + 'conversationId over threadId', + { attributes: { conversationId: 'conv-1' }, metadata: { threadId: 'thread-9', runId: 'run-3' } }, + 'conv-1', + ], + ['threadId, ignoring runId', { metadata: { threadId: 'thread-9', runId: 'run-3' } }, 'thread-9'], + ] as const)('sets gen_ai.conversation.id from %s', async (_label, overrides, expected) => { + const span = makeSpan({ entityName: 'agent', ...overrides }); + await run(started(span), ended(span)); + + expect(spanToStaticSpanJSON(endedSpans[0]!).data['gen_ai.conversation.id']).toBe(expected); + }); + + it('does not use runId as gen_ai.conversation.id', async () => { + const span = makeSpan({ entityName: 'agent', metadata: { runId: 'run-3' } }); + await run(started(span), ended(span)); + + expect(spanToStaticSpanJSON(endedSpans[0]!).data['gen_ai.conversation.id']).toBeUndefined(); + }); + + it('falls back to the bare operation name when there is no identifier', async () => { + const span = makeSpan({ id: 'gen-2', type: 'model_generation', name: 'mastra name', attributes: {} }); + await run(started(span), ended(span)); + + expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('chat'); + }); + + it('stays inert when no Sentry client is present', async () => { + getMainCarrier().__SENTRY__ = undefined; + const span = makeSpan(); + await expect(exporter.exportTracingEvent(started(span))).resolves.toBeUndefined(); + }); + + it('does not hang on a cyclic parent chain of dropped spans', async () => { + const a = makeSpan({ id: 'A', type: 'model_chunk', parentSpanId: 'B' }); + const b = makeSpan({ id: 'B', type: 'model_chunk', parentSpanId: 'A' }); + await run(started(a), started(b)); + + const child = makeSpan({ id: 'child', entityName: 'agent', parentSpanId: 'A' }); + await run(started(child), ended(child)); + + expect(endedSpans).toHaveLength(1); + expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('invoke_agent agent'); + }); + + it('does not grow without bound when spans never end', async () => { + for (let i = 0; i < 1_500; i++) { + await run(started(makeSpan({ id: `never-ends-${i}`, entityName: 'agent' }))); + } + + const fresh = makeSpan({ id: 'fresh', entityName: 'agent' }); + await run(started(fresh), ended(fresh)); + expect(endedSpans.some(span => spanToStaticSpanJSON(span).description === 'invoke_agent agent')).toBe(true); + }); + + it('ends the oldest Sentry span when the tracking map evicts it', async () => { + await run(started(makeSpan({ id: 'oldest', entityName: 'oldest' }))); + + for (let i = 0; i < 1_000; i++) { + await run(started(makeSpan({ id: `later-${i}`, entityName: 'agent' }))); + } + + expect(endedSpans.map(span => spanToStaticSpanJSON(span).description)).toEqual(['invoke_agent oldest']); + }); +}); diff --git a/packages/server-utils/test/integrations/mastra/bootstrap-resolve.test.ts b/packages/server-utils/test/integrations/mastra/bootstrap-resolve.test.ts new file mode 100644 index 000000000000..ce8f78ca0060 --- /dev/null +++ b/packages/server-utils/test/integrations/mastra/bootstrap-resolve.test.ts @@ -0,0 +1,135 @@ +import { tracingChannel } from 'node:diagnostics_channel'; +import type { Client } from '@sentry/core'; +import { GLOBAL_OBJ } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mastraIntegration } from '../../../src/integrations/mastra'; +import { CHANNELS } from '../../../src/orchestrion/channels'; + +const observabilityFrom = vi.hoisted(() => ({ + parent: undefined as string | undefined, + cache: {} as NodeJS.Dict, + loadedCore: '/app/node_modules/@mastra/core/dist/index.js', + cwdCore: '/cwd/node_modules/@mastra/core/index.js', + cwd: `${process.cwd()}/noop.js`, + failParents: [] as string[], +})); + +vi.mock('node:module', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + createRequire: (filename: string) => { + const req = ((id: string) => { + if (id === '@mastra/observability') { + if (observabilityFrom.failParents.includes(filename)) { + throw new Error(`not visible from ${filename}`); + } + observabilityFrom.parent = filename; + return { + Observability: class Observability {}, + DefaultObservabilityInstance: class DefaultObservabilityInstance {}, + }; + } + throw new Error(`unexpected require: ${id}`); + }) as NodeJS.Require; + req.cache = observabilityFrom.cache; + req.resolve = (id: string) => { + if (id === '@mastra/core') { + return observabilityFrom.cwdCore; + } + throw new Error(`unexpected resolve: ${id}`); + }; + return req; + }, + }; +}); + +describe('mastraIntegration observability resolve', () => { + beforeEach(() => { + observabilityFrom.parent = undefined; + observabilityFrom.cache = {}; + observabilityFrom.failParents = []; + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { runtime: ['@mastra/core'] }; + mastraIntegration().setup?.({ + on: () => () => undefined, + } as unknown as Client); + }); + + afterEach(() => { + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + }); + + it('loads @mastra/observability from a loaded @mastra/core file, not cwd', () => { + observabilityFrom.cache[observabilityFrom.loadedCore] = {} as NodeModule; + const registerExporter = vi.fn(); + + tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({ + self: { registerExporter }, + arguments: [], + }); + + expect(observabilityFrom.parent).toBe(observabilityFrom.loadedCore); + expect(registerExporter).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything()); + }); + + it('falls back to resolving @mastra/core from cwd when it is not in the CJS cache', () => { + const registerExporter = vi.fn(); + + tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({ + self: { registerExporter }, + arguments: [], + }); + + expect(observabilityFrom.parent).toBe(observabilityFrom.cwdCore); + expect(registerExporter).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything()); + }); + + it('falls back to cwd-resolved core when the cached copy cannot see @mastra/observability', () => { + observabilityFrom.cache[observabilityFrom.loadedCore] = {} as NodeModule; + observabilityFrom.failParents = [observabilityFrom.loadedCore]; + const registerExporter = vi.fn(); + + tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({ + self: { registerExporter }, + arguments: [], + }); + + expect(observabilityFrom.parent).toBe(observabilityFrom.cwdCore); + expect(registerExporter).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything()); + }); + + it('loads @mastra/observability from cwd when no core parent can see it', () => { + observabilityFrom.cache[observabilityFrom.loadedCore] = {} as NodeModule; + observabilityFrom.failParents = [observabilityFrom.loadedCore, observabilityFrom.cwdCore]; + const registerExporter = vi.fn(); + + tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({ + self: { registerExporter }, + arguments: [], + }); + + expect(observabilityFrom.parent).toBe(observabilityFrom.cwd); + expect(registerExporter).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything()); + }); + + it('warns once when @mastra/observability cannot be loaded', () => { + observabilityFrom.failParents = [observabilityFrom.loadedCore, observabilityFrom.cwdCore, observabilityFrom.cwd]; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const registerExporter = vi.fn(); + + tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({ + self: { registerExporter }, + arguments: [], + }); + tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({ + self: { registerExporter }, + arguments: [], + }); + + expect(registerExporter).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('@mastra/observability')); + + warn.mockRestore(); + }); +}); diff --git a/packages/server-utils/test/integrations/mastra/skip-providers.test.ts b/packages/server-utils/test/integrations/mastra/skip-providers.test.ts new file mode 100644 index 000000000000..0f646d27c9ab --- /dev/null +++ b/packages/server-utils/test/integrations/mastra/skip-providers.test.ts @@ -0,0 +1,61 @@ +import { tracingChannel } from 'node:diagnostics_channel'; +import type { Client } from '@sentry/core'; +import { _INTERNAL_clearAiProviderSkips, _INTERNAL_shouldSkipAiProviderWrapping, GLOBAL_OBJ } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ANTHROPIC_AI_INTEGRATION_NAME } from '../../../src/ai/anthropic-ai/constants'; +import { GOOGLE_GENAI_INTEGRATION_NAME } from '../../../src/ai/google-genai/constants'; +import { OPENAI_INTEGRATION_NAME } from '../../../src/ai/openai/constants'; +import { mastraIntegration } from '../../../src/integrations/mastra'; +import { CHANNELS } from '../../../src/orchestrion/channels'; + +function fakeMastra(): { + registerExporter: (exporter: unknown, instance: unknown, entrypoint: unknown) => void; + observability: { getDefaultInstance: () => { getExporters: () => unknown[] } }; +} { + const exporters: unknown[] = []; + return { + registerExporter: exporter => { + exporters.push(exporter); + }, + observability: { + getDefaultInstance: () => ({ + getExporters: () => exporters, + }), + }, + }; +} + +describe('mastraIntegration provider skip', () => { + beforeEach(() => { + _INTERNAL_clearAiProviderSkips(); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { runtime: ['@mastra/core'] }; + mastraIntegration().setup?.({ + on: () => () => undefined, + } as unknown as Client); + }); + + afterEach(() => { + _INTERNAL_clearAiProviderSkips(); + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + }); + + it('does not skip raw providers at setup, before any Mastra instance is constructed', () => { + expect(_INTERNAL_shouldSkipAiProviderWrapping(OPENAI_INTEGRATION_NAME)).toBe(false); + expect(_INTERNAL_shouldSkipAiProviderWrapping(ANTHROPIC_AI_INTEGRATION_NAME)).toBe(false); + expect(_INTERNAL_shouldSkipAiProviderWrapping(GOOGLE_GENAI_INTEGRATION_NAME)).toBe(false); + }); + + it('does not skip raw providers when a Mastra instance is constructed', () => { + tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({ self: fakeMastra(), arguments: [] }); + + expect(_INTERNAL_shouldSkipAiProviderWrapping(OPENAI_INTEGRATION_NAME)).toBe(false); + expect(_INTERNAL_shouldSkipAiProviderWrapping(ANTHROPIC_AI_INTEGRATION_NAME)).toBe(false); + expect(_INTERNAL_shouldSkipAiProviderWrapping(GOOGLE_GENAI_INTEGRATION_NAME)).toBe(false); + }); + + it('does not skip providers when the instance has no registerExporter', () => { + tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.publish({ self: {}, arguments: [] }); + + expect(_INTERNAL_shouldSkipAiProviderWrapping(OPENAI_INTEGRATION_NAME)).toBe(false); + }); +}); diff --git a/packages/server-utils/test/orchestrion/webpack-loader.test.ts b/packages/server-utils/test/orchestrion/webpack-loader.test.ts index 5e7e20eb2365..99a5a9cb272f 100644 --- a/packages/server-utils/test/orchestrion/webpack-loader.test.ts +++ b/packages/server-utils/test/orchestrion/webpack-loader.test.ts @@ -48,6 +48,7 @@ describe('orchestrion webpack/Turbopack loader', () => { root = mkdtempSync(join(tmpdir(), 'orch-webpack-loader-')); makePackage(root, 'mysql', '2.18.1'); makePackage(root, 'left-pad', '1.3.0'); + makePackage(root, '@mastra/core', '1.63.2'); }); afterAll(() => { @@ -104,4 +105,38 @@ describe('orchestrion webpack/Turbopack loader', () => { expect(code).toBe(source); }); + + it('transforms the hashed `@mastra/core` chunk that contains `class Mastra`', () => { + // `class Mastra` lives only in tsdown's content-hashed file; the stable + // `dist/mastra/index.js` is a one-line re-export and cannot be wrapped. + const hashed = join(root, 'node_modules/@mastra/core/dist/mastra-RpLTNzL-.js'); + const source = 'var Mastra = class Mastra {\n constructor() {}\n};\nexport { Mastra };\n'; + const { error, code } = runLoader(hashed, source, { instrumentations }); + + expect(error).toBeNull(); + expect(code).toContain('orchestrion:@mastra/core:mastraConstructor'); + expect(code).toContain('import {orchestrionModuleInjected, mastraIntegration} from "@sentry/server-utils"'); + expect(code).toContain('orchestrionModuleInjected("@mastra/core", mastraIntegration)'); + }); + + it('transforms a hashed `.mjs` `@mastra/core` chunk that contains `class Mastra`', () => { + const hashed = join(root, 'node_modules/@mastra/core/dist/mastra-RpLTNzL-.mjs'); + const source = 'var Mastra = class Mastra {\n constructor() {}\n};\nexport { Mastra };\n'; + const { error, code } = runLoader(hashed, source, { instrumentations }); + + expect(error).toBeNull(); + expect(code).toContain('orchestrion:@mastra/core:mastraConstructor'); + expect(code).toContain('import {orchestrionModuleInjected, mastraIntegration} from "@sentry/server-utils"'); + expect(code).toContain('orchestrionModuleInjected("@mastra/core", mastraIntegration)'); + }); + + it('does not transform the stable Mastra re-export that does not contain the class', () => { + const source = 'import { t as Mastra } from "../mastra-RpLTNzL-.js";\nexport { Mastra };\n'; + const { error, code } = runLoader(join(root, 'node_modules/@mastra/core/dist/mastra/index.js'), source, { + instrumentations, + }); + + expect(error).toBeNull(); + expect(code).toBe(source); + }); }); diff --git a/yarn.lock b/yarn.lock index 27491c31a197..257b7baeae23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,21 @@ # yarn lockfile v1 +"@a2a-js/sdk-v0_3@npm:@a2a-js/sdk@~0.3.14": + version "0.3.14" + resolved "https://sfw.security.sentry.io/npm/@a2a-js/sdk/-/sdk-0.3.14.tgz#e807b4e6516ea8c4af621ad9789d8dad6173058d" + integrity sha512-F6Ew1AtPzCLhTn8h9yiqTe7DiDf6XVrSnq9V1YqSl9eWqPm6anMveTiKdCSb/76cW0YiJc24rNaUrVezFFHbqQ== + dependencies: + uuid "^11.1.0" + +"@a2a-js/sdk-v1@npm:@a2a-js/sdk@~1.0.1": + version "1.0.1" + resolved "https://sfw.security.sentry.io/npm/@a2a-js/sdk/-/sdk-1.0.1.tgz#4a66be6a8cacc3276d6b71db9bf88ce6733f53f6" + integrity sha512-CJQdh3Wzwo8qIx5UUkSJ7+7BEI16PB+MXMHHNSmx8JQsQed2HlQgvx1ENOiKUfYA3PlcEvxIwv14dBblhDuPmw== + dependencies: + jose "^6.2.3" + uuid "^11.1.0" + "@actions/artifact@^6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-6.2.1.tgz#3f813d42d08d8d105fa46010f6ce2bb35e292b66" @@ -131,6 +146,34 @@ "@ai-sdk/provider-utils" "4.0.35" "@vercel/oidc" "3.2.0" +"@ai-sdk/provider-utils-v5@npm:@ai-sdk/provider-utils@3.0.30": + version "3.0.30" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider-utils/-/provider-utils-3.0.30.tgz#c9e611246706c8f5b617ad8c75eefb16479f47e9" + integrity sha512-NCJ9JKow5ENAgEZxzvEvF20thwDiH+hutvzmrUDbloRX0azpJHNst8+7pZIVryYhLM9wgpT5/ShTSjPTFhkxEQ== + dependencies: + "@ai-sdk/provider" "2.0.3" + "@standard-schema/spec" "^1.0.0" + eventsource-parser "^3.0.6" + +"@ai-sdk/provider-utils-v6@npm:@ai-sdk/provider-utils@4.0.40": + version "4.0.40" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider-utils/-/provider-utils-4.0.40.tgz#5daa46e0cc8d876887994e335fca7f14eca53fd3" + integrity sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw== + dependencies: + "@ai-sdk/provider" "3.0.14" + "@standard-schema/spec" "^1.1.0" + eventsource-parser "^3.0.8" + +"@ai-sdk/provider-utils-v7@npm:@ai-sdk/provider-utils@5.0.13": + version "5.0.13" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider-utils/-/provider-utils-5.0.13.tgz#0baab013772b1611a0c743b951ddf4c67cc1cd5c" + integrity sha512-fScDJMDnTbx32kLDQqp0MvPjvwkgiwvlBxlmIg7XW5PbS91LG6JjH3PQG+34oMFglqfpQA355e24OdGj5PPoDw== + dependencies: + "@ai-sdk/provider" "4.0.4" + "@standard-schema/spec" "^1.1.0" + "@workflow/serde" "4.1.0" + eventsource-parser "^3.0.8" + "@ai-sdk/provider-utils@2.2.8": version "2.2.8" resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz#ad11b92d5a1763ab34ba7b5fc42494bfe08b76d1" @@ -149,6 +192,27 @@ "@standard-schema/spec" "^1.1.0" eventsource-parser "^3.0.8" +"@ai-sdk/provider-v5@npm:@ai-sdk/provider@2.0.3": + version "2.0.3" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider/-/provider-2.0.3.tgz#8a3727d31947c6238e59dcbb72b7e1a871fd570c" + integrity sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww== + dependencies: + json-schema "^0.4.0" + +"@ai-sdk/provider-v6@npm:@ai-sdk/provider@3.0.14": + version "3.0.14" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider/-/provider-3.0.14.tgz#5893a90e0b5355ec6a5c148f06bd7f08355c1a6f" + integrity sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA== + dependencies: + json-schema "^0.4.0" + +"@ai-sdk/provider-v7@npm:@ai-sdk/provider@4.0.4": + version "4.0.4" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider/-/provider-4.0.4.tgz#7b1a3616d3e1c731a06a90f0a0e26e4d767a5b25" + integrity sha512-tbHKNLirllUNF3ZlkCsXnwab2ZV1Sl4b1H/Cp9ruCce15IBmskE8Gwkk0yo9xDWY+jho2of7lVXtwSsyrq7cwQ== + dependencies: + json-schema "^0.4.0" + "@ai-sdk/provider@1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-1.1.3.tgz#ebdda8077b8d2b3f290dcba32c45ad19b2704681" @@ -156,6 +220,13 @@ dependencies: json-schema "^0.4.0" +"@ai-sdk/provider@2.0.3": + version "2.0.3" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider/-/provider-2.0.3.tgz#8a3727d31947c6238e59dcbb72b7e1a871fd570c" + integrity sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww== + dependencies: + json-schema "^0.4.0" + "@ai-sdk/provider@3.0.13": version "3.0.13" resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-3.0.13.tgz#8d2d64bcf5bd077133c82cc1eeefad3bee0b192d" @@ -163,6 +234,20 @@ dependencies: json-schema "^0.4.0" +"@ai-sdk/provider@3.0.14": + version "3.0.14" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider/-/provider-3.0.14.tgz#5893a90e0b5355ec6a5c148f06bd7f08355c1a6f" + integrity sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA== + dependencies: + json-schema "^0.4.0" + +"@ai-sdk/provider@4.0.4": + version "4.0.4" + resolved "https://sfw.security.sentry.io/npm/@ai-sdk/provider/-/provider-4.0.4.tgz#7b1a3616d3e1c731a06a90f0a0e26e4d767a5b25" + integrity sha512-tbHKNLirllUNF3ZlkCsXnwab2ZV1Sl4b1H/Cp9ruCce15IBmskE8Gwkk0yo9xDWY+jho2of7lVXtwSsyrq7cwQ== + dependencies: + json-schema "^0.4.0" + "@ai-sdk/react@1.2.12": version "1.2.12" resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-1.2.12.tgz#f4250b6df566b170af98a71d5708b52108dd0ce1" @@ -5334,6 +5419,11 @@ dependencies: minipass "^7.0.4" +"@isaacs/ttlcache@^2.1.5": + version "2.1.5" + resolved "https://sfw.security.sentry.io/npm/@isaacs/ttlcache/-/ttlcache-2.1.5.tgz#9972fc5e801698907feb0b112ea19d8ff7d7689d" + integrity sha512-VwGZqqjAWPICTmxUZnbpEfO60LhPWzquik+bmyXGY7pYRn6diEvCI5i6Ca+J6o2y4vS73HrpuMTo2dOvUevH8w== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -5556,11 +5646,18 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== -"@lukeed/csprng@^1.0.0": +"@lukeed/csprng@^1.0.0", "@lukeed/csprng@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== +"@lukeed/uuid@^2.0.1": + version "2.0.1" + resolved "https://sfw.security.sentry.io/npm/@lukeed/uuid/-/uuid-2.0.1.tgz#4f6c34259ee0982a455e1797d56ac27bb040fd74" + integrity sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w== + dependencies: + "@lukeed/csprng" "^1.1.0" + "@mapbox/node-pre-gyp@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz#236aa1f62c101ce4c9db15697cb652ec69dca379" @@ -5574,6 +5671,57 @@ semver "^7.5.3" tar "^7.4.0" +"@mastra/core@1.63.2": + version "1.63.2" + resolved "https://sfw.security.sentry.io/npm/@mastra/core/-/core-1.63.2.tgz#3a04cc5397cec6edd77cf7570395d2f6aec514f9" + integrity sha512-BHVDF4GtQnIqRND/lPVVoTnmoNzZ7+voz+EpI4YSY0W7In6c/EOrmMnshuNQSDJ9NpxQNrwip0WGYpRYwzFirQ== + dependencies: + "@a2a-js/sdk-v0_3" "npm:@a2a-js/sdk@~0.3.14" + "@a2a-js/sdk-v1" "npm:@a2a-js/sdk@~1.0.1" + "@ai-sdk/provider-utils-v5" "npm:@ai-sdk/provider-utils@3.0.30" + "@ai-sdk/provider-utils-v6" "npm:@ai-sdk/provider-utils@4.0.40" + "@ai-sdk/provider-utils-v7" "npm:@ai-sdk/provider-utils@5.0.13" + "@ai-sdk/provider-v5" "npm:@ai-sdk/provider@2.0.3" + "@ai-sdk/provider-v6" "npm:@ai-sdk/provider@3.0.14" + "@ai-sdk/provider-v7" "npm:@ai-sdk/provider@4.0.4" + "@isaacs/ttlcache" "^2.1.5" + "@lukeed/uuid" "^2.0.1" + "@mastra/schema-compat" "1.3.7" + "@modelcontextprotocol/server" "2.0.0" + "@sindresorhus/slugify" "^2.2.1" + "@standard-schema/spec" "^1.1.0" + ajv "^8.20.0" + chat "^4.34.0" + croner "^10.0.1" + dotenv "^17.3.1" + execa "^9.6.1" + fastq "^1.20.1" + gray-matter "^4.0.3" + ignore "^7.0.5" + jpeg-js "^0.4.4" + json-schema "^0.4.0" + lru-cache "^11.2.7" + p-map "^7.0.4" + p-retry "^7.1.1" + picomatch "^4.0.3" + posthog-node "^5.46.1" + tokenx "^1.3.0" + ws "^8.21.0" + xxhash-wasm "^1.1.0" + +"@mastra/observability@1.17.4": + version "1.17.4" + resolved "https://sfw.security.sentry.io/npm/@mastra/observability/-/observability-1.17.4.tgz#dc49c11c0640b5d2f0fcd1af03c9f1d2c71ccfec" + integrity sha512-7TzbNNPR6h1LPUPW+kPkqqBKyubF9fZapGWJF4Y2f5JAhGva8Qj/NX0hbDgx54hqtvkTNh56+flAerwuNf1mMw== + +"@mastra/schema-compat@1.3.7": + version "1.3.7" + resolved "https://sfw.security.sentry.io/npm/@mastra/schema-compat/-/schema-compat-1.3.7.tgz#f5e5cc8769e7c113c0ec1a8e18ab95c828870457" + integrity sha512-06WfY+j9rulYnDp8CM7pL7JIyrntkMolqmyyb1VJNEzlYTQCIcJcY0SwQiubZv2tztmoYGikszDiRVyXp0E9gA== + dependencies: + json-schema-to-zod "^2.7.0" + zod-from-json-schema "^0.5.2" + "@mjackson/node-fetch-server@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz#577c0c25d8aae9f69a97738b7b0d03d1471cdc49" @@ -5599,7 +5747,7 @@ dependencies: zod "^4.2.0" -"@modelcontextprotocol/server@^2.0.0": +"@modelcontextprotocol/server@2.0.0", "@modelcontextprotocol/server@^2.0.0": version "2.0.0" resolved "https://sfw.security.sentry.io/npm/@modelcontextprotocol/server/-/server-2.0.0.tgz#4fae5ccb8f7925ed9a6bf6595bb3b58617063ffd" integrity sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw== @@ -7114,6 +7262,18 @@ resolved "https://registry.yarnpkg.com/@poppinss/exception/-/exception-1.2.2.tgz#8d30d42e126c54fe84e997433e4dcac610090743" integrity sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg== +"@posthog/core@^1.49.1": + version "1.49.2" + resolved "https://sfw.security.sentry.io/npm/@posthog/core/-/core-1.49.2.tgz#9b2772038c8f390e2fe7e1681539d8f8c5d12401" + integrity sha512-AXHDo/4nisUg7OPG1TQNgREK7n+chBQXLQyttp7bDTDsDg2k9lV06TmnpvuNbwJs53q9mP9hjezKEZu4fYadfg== + dependencies: + "@posthog/types" "^1.407.1" + +"@posthog/types@^1.407.1": + version "1.407.1" + resolved "https://sfw.security.sentry.io/npm/@posthog/types/-/types-1.407.1.tgz#aca89563a8b6799751b20ef9678641c13ba9441b" + integrity sha512-WhbkXPC2rgylXqmxHqv70ffI3k+KxyR6s7DBIfr5NvIqHkxp6v0pk31D/jbz0DNVbzwkLjyll2pxr4FNbJiYzg== + "@prisma/adapter-d1@6.15.0": version "6.15.0" resolved "https://registry.yarnpkg.com/@prisma/adapter-d1/-/adapter-d1-6.15.0.tgz#a3fd43a1b966d0012f1461ea278732b7464fb371" @@ -7895,6 +8055,11 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" +"@sec-ant/readable-stream@^0.4.1": + version "0.4.1" + resolved "https://sfw.security.sentry.io/npm/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c" + integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg== + "@sentry/conventions@0.20.0", "@sentry/conventions@^0.20.0": version "0.20.0" resolved "https://sfw.security.sentry.io/npm/@sentry/conventions/-/conventions-0.20.0.tgz#f73f6f46934d754dacef447755b5adf1bf7f3245" @@ -8038,6 +8203,21 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz#abb11d99aeb6d27f1b563c38147a72d50058e339" integrity sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ== +"@sindresorhus/slugify@^2.2.1": + version "2.2.1" + resolved "https://sfw.security.sentry.io/npm/@sindresorhus/slugify/-/slugify-2.2.1.tgz#fa2e2e25d6e1e74a2eeb5e2c37f5ccc516ed2c4b" + integrity sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw== + dependencies: + "@sindresorhus/transliterate" "^1.0.0" + escape-string-regexp "^5.0.0" + +"@sindresorhus/transliterate@^1.0.0": + version "1.6.0" + resolved "https://sfw.security.sentry.io/npm/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz#2309fff65a868047e6d2dd70dec747c5b36a8327" + integrity sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ== + dependencies: + escape-string-regexp "^5.0.0" + "@size-limit/esbuild@~12.1.0": version "12.1.0" resolved "https://registry.yarnpkg.com/@size-limit/esbuild/-/esbuild-12.1.0.tgz#d0527ee8eed98794966b089c7fe61760cf4e6c7b" @@ -10683,6 +10863,16 @@ dependencies: tslib "^2.6.3" +"@workflow/serde@4.1.0": + version "4.1.0" + resolved "https://sfw.security.sentry.io/npm/@workflow/serde/-/serde-4.1.0.tgz#82dbbaf2370f7b8ae46d9a9a4ab0917258f8db62" + integrity sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ== + +"@workflow/serde@4.1.0-beta.2": + version "4.1.0-beta.2" + resolved "https://sfw.security.sentry.io/npm/@workflow/serde/-/serde-4.1.0-beta.2.tgz#ac10377f50e9f83dbf4994edd4fe9c940bcef6a9" + integrity sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww== + "@xstate/fsm@^1.4.0": version "1.6.5" resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.5.tgz#f599e301997ad7e3c572a0b1ff0696898081bea5" @@ -10920,7 +11110,7 @@ ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.10.0, ajv@^8.12.0, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.10.0, ajv@^8.12.0, ajv@^8.20.0, ajv@^8.9.0: version "8.20.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== @@ -12557,6 +12747,19 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chat@^4.34.0: + version "4.39.0" + resolved "https://sfw.security.sentry.io/npm/chat/-/chat-4.39.0.tgz#dce04af60c2b5c1c0c75ab3528f7f7ca9fffc07f" + integrity sha512-o7D+oAbQAYFVoc9+OH8wya11609ybRThNTF3s/5C1LAABGGi6IrxoQQEN/aIe+4h5pmKYzUjXeqTcuOZcpIAZA== + dependencies: + "@workflow/serde" "4.1.0-beta.2" + mdast-util-to-string "^4.0.0" + remark-gfm "^4.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + remend "^1.2.1" + unified "^11.0.5" + check-error@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" @@ -15391,7 +15594,7 @@ events@^3.0.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -eventsource-parser@^3.0.0, eventsource-parser@^3.0.1, eventsource-parser@^3.0.8: +eventsource-parser@^3.0.0, eventsource-parser@^3.0.1, eventsource-parser@^3.0.6, eventsource-parser@^3.0.8: version "3.1.1" resolved "https://sfw.security.sentry.io/npm/eventsource-parser/-/eventsource-parser-3.1.1.tgz#b96cbb7dace4f3774f58a9e3b1ae9a4a524872e2" integrity sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ== @@ -15458,6 +15661,24 @@ execa@^8.0.1: signal-exit "^4.1.0" strip-final-newline "^3.0.0" +execa@^9.6.1: + version "9.6.1" + resolved "https://sfw.security.sentry.io/npm/execa/-/execa-9.6.1.tgz#5b90acedc6bdc0fa9b9a6ddf8f9cbb0c75a7c471" + integrity sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA== + dependencies: + "@sindresorhus/merge-streams" "^4.0.0" + cross-spawn "^7.0.6" + figures "^6.1.0" + get-stream "^9.0.0" + human-signals "^8.0.1" + is-plain-obj "^4.1.0" + is-stream "^4.0.1" + npm-run-path "^6.0.0" + pretty-ms "^9.2.0" + signal-exit "^4.1.0" + strip-final-newline "^4.0.0" + yoctocolors "^2.1.1" + exit-hook@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-2.2.1.tgz#007b2d92c6428eda2b76e7016a34351586934593" @@ -15783,10 +16004,10 @@ fastify@^5.7.0: semver "^7.6.0" toad-cache "^3.7.0" -fastq@^1.17.1, fastq@^1.6.0: - version "1.20.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" - integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== +fastq@^1.17.1, fastq@^1.20.1, fastq@^1.6.0: + version "1.20.3" + resolved "https://sfw.security.sentry.io/npm/fastq/-/fastq-1.20.3.tgz#7ab8731e647b56abcfeee997dae333ca3ee1d04d" + integrity sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw== dependencies: reusify "^1.0.4" @@ -15838,6 +16059,13 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" +figures@^6.1.0: + version "6.1.0" + resolved "https://sfw.security.sentry.io/npm/figures/-/figures-6.1.0.tgz#935479f51865fa7479f6fa94fc6fc7ac14e62c4a" + integrity sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg== + dependencies: + is-unicode-supported "^2.0.0" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -16397,6 +16625,14 @@ get-stream@^8.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== +get-stream@^9.0.0: + version "9.0.1" + resolved "https://sfw.security.sentry.io/npm/get-stream/-/get-stream-9.0.1.tgz#95157d21df8eb90d1647102b63039b1df60ebd27" + integrity sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA== + dependencies: + "@sec-ant/readable-stream" "^0.4.1" + is-stream "^4.0.1" + get-symbol-description@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" @@ -17326,6 +17562,11 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +human-signals@^8.0.1: + version "8.0.1" + resolved "https://sfw.security.sentry.io/npm/human-signals/-/human-signals-8.0.1.tgz#f08bb593b6d1db353933d06156cedec90abe51fb" + integrity sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -17832,6 +18073,11 @@ is-negative-zero@^2.0.3: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== +is-network-error@^1.1.0: + version "1.3.2" + resolved "https://sfw.security.sentry.io/npm/is-network-error/-/is-network-error-1.3.2.tgz#9460bc30f8419a4bca77114f4de88a3ee5e0c519" + integrity sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA== + is-node-process@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" @@ -17870,7 +18116,7 @@ is-plain-obj@^3.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== -is-plain-obj@^4.0.0: +is-plain-obj@^4.0.0, is-plain-obj@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== @@ -17953,6 +18199,11 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== +is-stream@^4.0.1: + version "4.0.1" + resolved "https://sfw.security.sentry.io/npm/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b" + integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A== + is-string@^1.0.7, is-string@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" @@ -18219,11 +18470,16 @@ jiti@^2.4.2, jiti@^2.6.1, jiti@^2.7.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64" integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== -jose@^6.1.3: +jose@^6.1.3, jose@^6.2.3: version "6.2.10" resolved "https://sfw.security.sentry.io/npm/jose/-/jose-6.2.10.tgz#b70436c920c4b3f97314c28a8b8612c15b1d9e7f" integrity sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g== +jpeg-js@^0.4.4: + version "0.4.4" + resolved "https://sfw.security.sentry.io/npm/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" + integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg== + js-md4@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/js-md4/-/js-md4-0.3.2.tgz#cd3b3dc045b0c404556c81ddb5756c23e59d7cf5" @@ -18421,6 +18677,11 @@ json-schema-to-ts@^3.1.1: "@babel/runtime" "^7.18.3" ts-algebra "^2.0.0" +json-schema-to-zod@^2.7.0: + version "2.8.1" + resolved "https://sfw.security.sentry.io/npm/json-schema-to-zod/-/json-schema-to-zod-2.8.1.tgz#684fcbe64cb178c1008c498209fff0890c9de725" + integrity sha512-fRr1mHgZ7hboLKBUdR428gd9dIHUFGivUqOeiDcSmyXkNZCtB1uGaZLvsjZ4GaN5pwBIs+TGIOf6s+Rp5/R/zA== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -21708,10 +21969,10 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-map@^7.0.3: - version "7.0.4" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.4.tgz#b81814255f542e252d5729dca4d66e5ec14935b8" - integrity sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ== +p-map@^7.0.3, p-map@^7.0.4: + version "7.0.7" + resolved "https://sfw.security.sentry.io/npm/p-map/-/p-map-7.0.7.tgz#a82175dd690468a930b9ae45c2b9829c5bf60fa3" + integrity sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ== p-queue@^6.6.2: version "6.6.2" @@ -21737,6 +21998,13 @@ p-retry@4, p-retry@^4.5.0: "@types/retry" "0.12.0" retry "^0.13.1" +p-retry@^7.1.1: + version "7.1.1" + resolved "https://sfw.security.sentry.io/npm/p-retry/-/p-retry-7.1.1.tgz#7470fdecb1152ba50f1334e48378c9e401330e24" + integrity sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w== + dependencies: + is-network-error "^1.1.0" + p-timeout@^3.0.0, p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -21859,6 +22127,11 @@ parse-ms@^2.1.0: resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== +parse-ms@^4.0.0: + version "4.0.0" + resolved "https://sfw.security.sentry.io/npm/parse-ms/-/parse-ms-4.0.0.tgz#c0c058edd47c2a590151a718990533fd62803df4" + integrity sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw== + parse-node-version@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -22959,6 +23232,13 @@ postgres@^3.4.7: resolved "https://registry.yarnpkg.com/postgres/-/postgres-3.4.7.tgz#122f460a808fe300cae53f592108b9906e625345" integrity sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw== +posthog-node@^5.46.1: + version "5.51.4" + resolved "https://sfw.security.sentry.io/npm/posthog-node/-/posthog-node-5.51.4.tgz#0a6e86f772e26399b62ee515ba7f3a9be93b36f7" + integrity sha512-gI6JMBnU3vjDNclUBWonw3y7k8Y0UPIAVO4AQ2zu9eyW+7sY8UQRofx4JIA7IGYLMEbs4gykfogOmXp16+oUdg== + dependencies: + "@posthog/core" "^1.49.1" + powershell-utils@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.1.0.tgz#5a42c9a824fb4f2f251ccb41aaae73314f5d6ac2" @@ -23072,6 +23352,13 @@ pretty-ms@^7.0.1: dependencies: parse-ms "^2.1.0" +pretty-ms@^9.2.0: + version "9.3.1" + resolved "https://sfw.security.sentry.io/npm/pretty-ms/-/pretty-ms-9.3.1.tgz#7b58d8fbf376f01602ff05230358e8fa5e0f072b" + integrity sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA== + dependencies: + parse-ms "^4.0.0" + prisma@6.15.0: version "6.15.0" resolved "https://registry.yarnpkg.com/prisma/-/prisma-6.15.0.tgz#198f95a4468aba8f3c95b58bf3400f65daeb8942" @@ -23913,6 +24200,11 @@ remark-stringify@^11.0.0: mdast-util-to-markdown "^2.0.0" unified "^11.0.0" +remend@^1.2.1: + version "1.3.1" + resolved "https://sfw.security.sentry.io/npm/remend/-/remend-1.3.1.tgz#474575f709d39d01f5d3b04333d4fa40d4491133" + integrity sha512-N3DiY5qbRPoa5vkxn1oDLMyXOVTeo6Hp+XOj6SIqJAYUgLS0Q587gILPMom/qm86AQ/ZrcOdwEIzCz8V3J0nxQ== + remove-types@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/remove-types/-/remove-types-1.0.0.tgz#491119e8fce23f2f961fe2254e5a119a017d9a3d" @@ -25676,6 +25968,11 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== +strip-final-newline@^4.0.0: + version "4.0.0" + resolved "https://sfw.security.sentry.io/npm/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c" + integrity sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw== + strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -26246,6 +26543,11 @@ token-types@^6.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +tokenx@^1.3.0: + version "1.6.0" + resolved "https://sfw.security.sentry.io/npm/tokenx/-/tokenx-1.6.0.tgz#8a5fde974ebd559d43081f143c7d70750b6dbea1" + integrity sha512-CKTjk345ajvBAUp5xUI9a5KKN0zU0lBueVHQbCskH1Hp6WkUKsPW2qGCYNs0pxNyfzxfo+IIjdt2W4sMbw/qBw== + toml@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/toml/-/toml-4.1.1.tgz#ab8248d0403ba2c02ffcf8515b42f0dcf0d6d1b5" @@ -28274,7 +28576,7 @@ ws@8.18.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -ws@^8.13.0, ws@^8.18.0, ws@^8.20.1, ws@^8.21.1, ws@^8.4.2: +ws@^8.13.0, ws@^8.18.0, ws@^8.20.1, ws@^8.21.0, ws@^8.21.1, ws@^8.4.2: version "8.21.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.3.tgz#660b4faddb6a3e575c86e078126919961f4de4fc" integrity sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw== @@ -28463,6 +28765,11 @@ yocto-queue@^1.1.1: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.2.tgz#3e09c95d3f1aa89a58c114c99223edf639152c00" integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== +yoctocolors@^2.1.1: + version "2.2.0" + resolved "https://sfw.security.sentry.io/npm/yoctocolors/-/yoctocolors-2.2.0.tgz#c4b68ee477f3982c33e4de24a5aa4a354be506d3" + integrity sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg== + youch-core@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/youch-core/-/youch-core-0.3.3.tgz#c5d3d85aeea0d8bc7b36e9764ed3f14b7ceddc7d" @@ -28502,6 +28809,13 @@ zip-stream@^6.0.1: compress-commons "^6.0.2" readable-stream "^4.0.0" +zod-from-json-schema@^0.5.2: + version "0.5.6" + resolved "https://sfw.security.sentry.io/npm/zod-from-json-schema/-/zod-from-json-schema-0.5.6.tgz#a8c7a188a795715d6fff15604101a37dd455d4b1" + integrity sha512-U33AJ7ZWS6y9XNSzMWcdy8hRAvZmWhTtpYJu0SXPT5AArbc9nq2ur7Magzmn5RF9KBV4b3FP0nCpmqqlfXlR9w== + dependencies: + zod "^4.0.17" + zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.23.5, zod-to-json-schema@^3.24.1: version "3.25.2" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz#3fa799a7badd554541472fb65843fdc460b2e5aa" @@ -28517,10 +28831,10 @@ zod@^3.23.8, zod@^3.24.1, zod@^3.25.32: resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== -zod@^4.0.0, zod@^4.2.0: - version "4.4.3" - resolved "https://sfw.security.sentry.io/npm/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" - integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== +zod@^4.0.0, zod@^4.0.17, zod@^4.2.0: + version "4.5.4" + resolved "https://sfw.security.sentry.io/npm/zod/-/zod-4.5.4.tgz#e215c62420c528dd7951e31fb52c5438f1fd184a" + integrity sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA== zone.js@^0.12.0: version "0.12.0"