From 552897ecaabbfb8b11463a746d9302c2dc4c98f2 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 11:16:12 +0200 Subject: [PATCH 1/7] test(node): Port Mastra tests to span streaming Co-Authored-By: GPT-6 --- .../mastra/instrument-no-bootstrap.mjs | 5 - .../tracing/mastra/instrument-with-pii.mjs | 5 - .../suites/tracing/mastra/instrument.mjs | 5 - .../suites/tracing/mastra/test.ts | 145 +++++++++--------- 4 files changed, 76 insertions(+), 84 deletions(-) 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 index f4ebcd4a74cc..377693b0a7f1 100644 --- 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 @@ -2,15 +2,10 @@ 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 index 6cd8bfaa55bc..1c507ba84b9c 100644 --- 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 @@ -2,14 +2,9 @@ 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 index ec49598a39c9..cc192fb89834 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/instrument.mjs @@ -2,14 +2,9 @@ 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/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts index 728a12762da6..935c066e933a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -1,4 +1,5 @@ import { + DB_OPERATION_NAME, GEN_AI_AGENT_NAME, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, @@ -15,6 +16,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { afterAll, expect } from 'vitest'; import { conditionalTest } from '../../../utils'; @@ -51,15 +54,14 @@ conditionalTest({ min: 22 })('Mastra integration', () => { (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 segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.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')!; + const agentSpan = spans.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'); @@ -71,7 +73,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 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')!; + const chatSpan = spans.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'); @@ -89,10 +91,12 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 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) { + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + for (const span of spans) { expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); expect(span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]).toBeUndefined(); @@ -113,15 +117,17 @@ conditionalTest({ min: 22 })('Mastra integration', () => { (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')!; + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const agentSpan = spans.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')!; + const chatSpan = spans.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.'); }, @@ -140,16 +146,18 @@ conditionalTest({ min: 22 })('Mastra integration', () => { (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([ + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.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')!; + const toolSpan = spans.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'); @@ -158,10 +166,10 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 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')!; + const chatSpan = spans.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')!; + const agentSpan = spans.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); @@ -182,12 +190,14 @@ conditionalTest({ min: 22 })('Mastra integration', () => { (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 segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name)).toEqual(['invoke_agent math_workflow']); - const workflowSpan = container.items[0]!; + const workflowSpan = spans[0]!; expect(workflowSpan.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); expect(workflowSpan.attributes[GEN_AI_PIPELINE_NAME].value).toBe('math_workflow'); }, @@ -206,14 +216,13 @@ conditionalTest({ min: 22 })('Mastra integration', () => { (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) { + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); + for (const span of spans) { expect(span.attributes['sentry.origin'].value).toBe('auto.ai.mastra'); } }, @@ -232,13 +241,12 @@ conditionalTest({ min: 22 })('Mastra integration', () => { (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', - ]); + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); }, }) .start() @@ -255,9 +263,12 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 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.')); + span: container => { + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const genAiSpans = container.items.filter(span => + String(span.attributes[SENTRY_OP]?.value ?? '').startsWith('gen_ai.'), + ); expect(genAiSpans).toEqual([]); }, }) @@ -274,8 +285,6 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('nests the model fetch and tool work under the exporter spans', async () => { - // The exporter's gen_ai spans stream as their own span items; the `dataloader` and model - // `http.client` spans ride in the transaction event. Cross-reference once both have arrived. let chatSpanId: string | undefined; let executeToolSpanId: string | undefined; let modelFetchParentIds: (string | undefined)[] = []; @@ -283,26 +292,27 @@ conditionalTest({ min: 22 })('Mastra integration', () => { await createRunner() .expect({ - transaction: event => { - expect(event.transaction).toBe('mastra-test'); + span: container => { + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); // The model-provider requests Mastra makes for each step. - const modelFetches = (event.spans ?? []).filter( - span => span.op === 'http.client' && (span.description ?? '').includes('/v1/chat/completions'), + const modelFetches = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'http.client' && + String(span.attributes['url.full']?.value ?? '').includes('/v1/chat/completions'), ); expect(modelFetches.length).toBeGreaterThan(0); modelFetchParentIds = modelFetches.map(span => span.parent_span_id); // The per-key `dataloader.load` spans opened while the tool runs. (dataloader also emits a // `dataloader.batch` span nested under a load — its own internal structure.) - const loadSpans = (event.spans ?? []).filter( - span => span.op === 'cache.get' && span.description === 'dataloader.load', + const loadSpans = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && + span.attributes[DB_OPERATION_NAME]?.value === 'load', ); expect(loadSpans.length).toBeGreaterThan(0); cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - }, - }) - .expect({ - span: container => { const chat = container.items.find(span => span.attributes['sentry.op']?.value === 'gen_ai.chat')!; expect(chat).toBeDefined(); chatSpanId = chat.span_id; @@ -310,7 +320,6 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const executeTool = container.items.find( span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items', )!; - expect(executeTool).toBeDefined(); expect(executeTool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); executeToolSpanId = executeTool.span_id; }, @@ -344,23 +353,24 @@ conditionalTest({ min: 22 })('Mastra integration', () => { await createRunner() .expect({ - transaction: event => { - expect(event.transaction).toBe('mastra-test'); - const modelFetches = (event.spans ?? []).filter( - span => span.op === 'http.client' && (span.description ?? '').includes('/v1/chat/completions'), + span: container => { + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + expect(segment).toBeDefined(); + const modelFetches = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'http.client' && + String(span.attributes['url.full']?.value ?? '').includes('/v1/chat/completions'), ); expect(modelFetches.length).toBeGreaterThan(0); modelFetchParentIds = modelFetches.map(span => span.parent_span_id); - const loadSpans = (event.spans ?? []).filter( - span => span.op === 'cache.get' && span.description === 'dataloader.load', + const loadSpans = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && + span.attributes[DB_OPERATION_NAME]?.value === 'load', ); expect(loadSpans.length).toBeGreaterThan(0); cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - }, - }) - .expect({ - span: container => { const chat = container.items.find(span => span.attributes['sentry.op']?.value === 'gen_ai.chat')!; expect(chat).toBeDefined(); chatSpanId = chat.span_id; @@ -368,7 +378,6 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const executeTool = container.items.find( span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items', )!; - expect(executeTool).toBeDefined(); expect(executeTool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); executeToolSpanId = executeTool.span_id; }, @@ -399,16 +408,15 @@ conditionalTest({ min: 22 })('Mastra integration', () => { await createRunner() .expect({ - transaction: event => { - expect(event.transaction).toBe('mastra-multi'); - const loadSpans = (event.spans ?? []).filter( - span => span.op === 'cache.get' && span.description === 'dataloader.load', + span: container => { + const segment = container.items.find(span => span.is_segment && span.name === 'mastra-multi'); + expect(segment).toBeDefined(); + const loadSpans = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && + span.attributes[DB_OPERATION_NAME]?.value === 'load', ); loadParentIds = loadSpans.map(span => span.parent_span_id); - }, - }) - .expect({ - span: container => { for (const toolName of ['count_a', 'count_b']) { const toolSpan = container.items.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === toolName)!; expect(toolSpan).toBeDefined(); @@ -439,10 +447,9 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('captures an error thrown in a Mastra tool as a Sentry issue', async () => { - // The scenario also emits the `mastra-test` transaction and its streamed gen_ai spans; their - // flush order relative to the error event races, so ignore them and match only the issue. + // Span containers can arrive before the error event, so match only the issue. await createRunner() - .ignore('transaction', 'span') + .ignore('span') .expect({ event: event => { const exception = event.exception?.values?.[0]; From 7fb3f3ad54c6ff58a261e17617b72cabdb83810d Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 11:54:19 +0200 Subject: [PATCH 2/7] test(node): Collect mastra spans across envelopes Co-Authored-By: GPT-6 --- .../suites/tracing/mastra/test.ts | 99 +++++++++++++------ 1 file changed, 68 insertions(+), 31 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts index 935c066e933a..1aa203a5020c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { DB_OPERATION_NAME, GEN_AI_AGENT_NAME, @@ -53,12 +54,16 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('creates invoke_agent and chat spans following the gen_ai conventions', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); - const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); const agentSpan = spans.find(span => span.name === 'invoke_agent weather_agent')!; @@ -90,12 +95,16 @@ conditionalTest({ min: 22 })('Mastra integration', () => { }); test('omits prompts and responses when genAI recording is off', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); - const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); for (const span of spans) { expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); @@ -116,12 +125,16 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('records prompts and responses when genAI recording is on', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); - const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); const agentSpan = spans.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?'); @@ -145,12 +158,16 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('creates an execute_tool span parented onto the generation', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); - const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); expect(spans.map(span => span.name).sort()).toEqual([ 'chat gpt-4o-mini', 'execute_tool get_weather', @@ -215,12 +232,16 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('auto-instruments a Mastra app that configured no observability at all', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); - const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); for (const span of spans) { expect(span.attributes['sentry.origin'].value).toBe('auto.ai.mastra'); @@ -240,12 +261,16 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('still exports its own spans when the community @mastra/sentry exporter is present', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); - const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); }, }) @@ -261,12 +286,16 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-no-bootstrap.mjs', (createRunner, test) => { test('leaves an unconfigured app alone when bootstrapObservability is false', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); - const genAiSpans = container.items.filter(span => + const genAiSpans = allSpans.filter(span => String(span.attributes[SENTRY_OP]?.value ?? '').startsWith('gen_ai.'), ); expect(genAiSpans).toEqual([]); @@ -285,18 +314,22 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('nests the model fetch and tool work under the exporter spans', async () => { + const allSpans: SerializedStreamedSpanContainer['items'] = []; + let chatSpanId: string | undefined; let executeToolSpanId: string | undefined; let modelFetchParentIds: (string | undefined)[] = []; let cacheGetParentIds: (string | undefined)[] = []; await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); // The model-provider requests Mastra makes for each step. - const modelFetches = container.items.filter( + const modelFetches = allSpans.filter( span => span.attributes[SENTRY_OP]?.value === 'http.client' && String(span.attributes['url.full']?.value ?? '').includes('/v1/chat/completions'), @@ -306,20 +339,18 @@ conditionalTest({ min: 22 })('Mastra integration', () => { // The per-key `dataloader.load` spans opened while the tool runs. (dataloader also emits a // `dataloader.batch` span nested under a load — its own internal structure.) - const loadSpans = container.items.filter( + const loadSpans = allSpans.filter( span => span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', ); expect(loadSpans.length).toBeGreaterThan(0); cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - const chat = container.items.find(span => span.attributes['sentry.op']?.value === 'gen_ai.chat')!; + const chat = allSpans.find(span => span.attributes['sentry.op']?.value === 'gen_ai.chat')!; expect(chat).toBeDefined(); chatSpanId = chat.span_id; - const executeTool = container.items.find( - span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items', - )!; + const executeTool = allSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; expect(executeTool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); executeToolSpanId = executeTool.span_id; }, @@ -346,17 +377,21 @@ conditionalTest({ min: 22 })('Mastra integration', () => { test('nests the model fetch and tool work under the exporter spans when streaming', async () => { // Same nesting guarantee as `generate`, but the agent is driven via `stream()` — the model // request is a real streaming (SSE) call, and the tool still runs inside `executeWithContext`. + const allSpans: SerializedStreamedSpanContainer['items'] = []; + let chatSpanId: string | undefined; let executeToolSpanId: string | undefined; let modelFetchParentIds: (string | undefined)[] = []; let cacheGetParentIds: (string | undefined)[] = []; await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-test'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); expect(segment).toBeDefined(); - const modelFetches = container.items.filter( + const modelFetches = allSpans.filter( span => span.attributes[SENTRY_OP]?.value === 'http.client' && String(span.attributes['url.full']?.value ?? '').includes('/v1/chat/completions'), @@ -364,20 +399,18 @@ conditionalTest({ min: 22 })('Mastra integration', () => { expect(modelFetches.length).toBeGreaterThan(0); modelFetchParentIds = modelFetches.map(span => span.parent_span_id); - const loadSpans = container.items.filter( + const loadSpans = allSpans.filter( span => span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', ); expect(loadSpans.length).toBeGreaterThan(0); cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - const chat = container.items.find(span => span.attributes['sentry.op']?.value === 'gen_ai.chat')!; + const chat = allSpans.find(span => span.attributes['sentry.op']?.value === 'gen_ai.chat')!; expect(chat).toBeDefined(); chatSpanId = chat.span_id; - const executeTool = container.items.find( - span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items', - )!; + const executeTool = allSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; expect(executeTool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); executeToolSpanId = executeTool.span_id; }, @@ -403,22 +436,26 @@ conditionalTest({ min: 22 })('Mastra integration', () => { // Two instances run concurrently in one trace. Each instance's `dataloader.load` must nest under // that instance's own `execute_tool` span — if the registry cross-talked, one tool span would // have no dataloader child. + const allSpans: SerializedStreamedSpanContainer['items'] = []; + const toolSpanIds: Record = {}; let loadParentIds: (string | undefined)[] = []; await createRunner() + .unordered() .expect({ span: container => { - const segment = container.items.find(span => span.is_segment && span.name === 'mastra-multi'); + allSpans.push(...container.items); + const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-multi'); expect(segment).toBeDefined(); - const loadSpans = container.items.filter( + const loadSpans = allSpans.filter( span => span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', ); loadParentIds = loadSpans.map(span => span.parent_span_id); for (const toolName of ['count_a', 'count_b']) { - const toolSpan = container.items.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === toolName)!; + const toolSpan = allSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === toolName)!; expect(toolSpan).toBeDefined(); toolSpanIds[toolName] = toolSpan.span_id; } From dc75c1c6bc53c580692f8abef01879a64cf1f154 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 12:06:59 +0200 Subject: [PATCH 3/7] test(node): Use the SENTRY_ORIGIN convention in mastra tests Co-Authored-By: GPT-6 --- .../node-integration-tests/suites/tracing/mastra/test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts index 1aa203a5020c..9bd2b6d3b6fc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -69,7 +69,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const agentSpan = spans.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[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'); @@ -81,7 +81,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const chatSpan = spans.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[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'); @@ -177,7 +177,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const toolSpan = spans.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[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'); @@ -244,7 +244,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); for (const span of spans) { - expect(span.attributes['sentry.origin'].value).toBe('auto.ai.mastra'); + expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.mastra'); } }, }) From 48a37964cca929356ab062d81b991f00fd9b3351 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 12:12:56 +0200 Subject: [PATCH 4/7] test(node): Use convention constants for mastra span attributes Co-Authored-By: GPT-6 --- .../suites/tracing/mastra/test.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts index 9bd2b6d3b6fc..738fdcdfb2d6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -17,8 +17,9 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, - SENTRY_ORIGIN, SENTRY_OP, + SENTRY_ORIGIN, + URL_FULL, } from '@sentry/conventions/attributes'; import { afterAll, expect } from 'vitest'; import { conditionalTest } from '../../../utils'; @@ -68,7 +69,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const agentSpan = spans.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_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'); @@ -80,7 +81,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const chatSpan = spans.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_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'); @@ -176,7 +177,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const toolSpan = spans.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_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'); @@ -215,7 +216,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { expect(spans.map(span => span.name)).toEqual(['invoke_agent math_workflow']); const workflowSpan = spans[0]!; - expect(workflowSpan.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(workflowSpan.attributes[SENTRY_OP].value).toBe('gen_ai.invoke_agent'); expect(workflowSpan.attributes[GEN_AI_PIPELINE_NAME].value).toBe('math_workflow'); }, }) @@ -332,7 +333,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const modelFetches = allSpans.filter( span => span.attributes[SENTRY_OP]?.value === 'http.client' && - String(span.attributes['url.full']?.value ?? '').includes('/v1/chat/completions'), + String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), ); expect(modelFetches.length).toBeGreaterThan(0); modelFetchParentIds = modelFetches.map(span => span.parent_span_id); @@ -346,12 +347,12 @@ conditionalTest({ min: 22 })('Mastra integration', () => { ); expect(loadSpans.length).toBeGreaterThan(0); cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - const chat = allSpans.find(span => span.attributes['sentry.op']?.value === 'gen_ai.chat')!; + const chat = allSpans.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; expect(chat).toBeDefined(); chatSpanId = chat.span_id; const executeTool = allSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; - expect(executeTool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); + expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); executeToolSpanId = executeTool.span_id; }, }) @@ -394,7 +395,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { const modelFetches = allSpans.filter( span => span.attributes[SENTRY_OP]?.value === 'http.client' && - String(span.attributes['url.full']?.value ?? '').includes('/v1/chat/completions'), + String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), ); expect(modelFetches.length).toBeGreaterThan(0); modelFetchParentIds = modelFetches.map(span => span.parent_span_id); @@ -406,12 +407,12 @@ conditionalTest({ min: 22 })('Mastra integration', () => { ); expect(loadSpans.length).toBeGreaterThan(0); cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - const chat = allSpans.find(span => span.attributes['sentry.op']?.value === 'gen_ai.chat')!; + const chat = allSpans.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; expect(chat).toBeDefined(); chatSpanId = chat.span_id; const executeTool = allSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; - expect(executeTool.attributes['sentry.op']?.value).toBe('gen_ai.execute_tool'); + expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); executeToolSpanId = executeTool.span_id; }, }) From 0a220549d529e9f0f71087ac32d6ff8d48064947 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 09:57:48 +0200 Subject: [PATCH 5/7] test(node): Assert Mastra spans after envelope selection Co-Authored-By: GPT-6 --- .../suites/tracing/mastra/test.ts | 343 +++++++++--------- 1 file changed, 170 insertions(+), 173 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts index 738fdcdfb2d6..ab0d304cdb25 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -55,66 +55,66 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('creates invoke_agent and chat spans following the gen_ai conventions', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); - - const agentSpan = spans.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 = spans.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); + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); + + const agentSpan = spans.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 = spans.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); }); test('omits prompts and responses when genAI recording is off', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - for (const span of spans) { - expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); - expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); - expect(span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]).toBeUndefined(); - } + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + for (const span of spans) { + expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); + expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); + expect(span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]).toBeUndefined(); + } }); }, MASTRA_DEPENDENCIES, @@ -126,28 +126,28 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('records prompts and responses when genAI recording is on', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - const agentSpan = spans.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 = spans.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.'); + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const agentSpan = spans.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 = spans.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.'); }); }, MASTRA_DEPENDENCIES, @@ -159,43 +159,43 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('creates an execute_tool span parented onto the generation', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat gpt-4o-mini', - 'execute_tool get_weather', - 'invoke_agent weather_agent', - ]); - - const toolSpan = spans.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 = spans.find(span => span.name === 'chat gpt-4o-mini')!; - expect(toolSpan.parent_span_id).toBe(chatSpan.span_id); - - const agentSpan = spans.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); + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat gpt-4o-mini', + 'execute_tool get_weather', + 'invoke_agent weather_agent', + ]); + + const toolSpan = spans.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 = spans.find(span => span.name === 'chat gpt-4o-mini')!; + expect(toolSpan.parent_span_id).toBe(chatSpan.span_id); + + const agentSpan = spans.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); }); }, MASTRA_DEPENDENCIES, @@ -233,24 +233,24 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('auto-instruments a Mastra app that configured no observability at all', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); - for (const span of spans) { - expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.mastra'); - } + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); + for (const span of spans) { + expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.mastra'); + } }); }, MASTRA_DEPENDENCIES, @@ -262,21 +262,21 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('still exports its own spans when the community @mastra/sentry exporter is present', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - const spans = allSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); }); }, MASTRA_DEPENDENCIES, @@ -287,23 +287,23 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-no-bootstrap.mjs', (createRunner, test) => { test('leaves an unconfigured app alone when bootstrapObservability is false', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; await createRunner() .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - const genAiSpans = allSpans.filter(span => - String(span.attributes[SENTRY_OP]?.value ?? '').startsWith('gen_ai.'), - ); - expect(genAiSpans).toEqual([]); + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + + const genAiSpans = receivedSpans.filter(span => + String(span.attributes[SENTRY_OP]?.value ?? '').startsWith('gen_ai.'), + ); + expect(genAiSpans).toEqual([]); }); }, MASTRA_DEPENDENCIES, @@ -315,7 +315,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('nests the model fetch and tool work under the exporter spans', async () => { - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; let chatSpanId: string | undefined; let executeToolSpanId: string | undefined; @@ -326,39 +326,38 @@ conditionalTest({ min: 22 })('Mastra integration', () => { .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - // The model-provider requests Mastra makes for each step. - const modelFetches = allSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'http.client' && - String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), - ); - expect(modelFetches.length).toBeGreaterThan(0); - modelFetchParentIds = modelFetches.map(span => span.parent_span_id); - - // The per-key `dataloader.load` spans opened while the tool runs. (dataloader also emits a - // `dataloader.batch` span nested under a load — its own internal structure.) - const loadSpans = allSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'cache.get' && - span.attributes[DB_OPERATION_NAME]?.value === 'load', - ); - expect(loadSpans.length).toBeGreaterThan(0); - cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - const chat = allSpans.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; - expect(chat).toBeDefined(); - chatSpanId = chat.span_id; - - const executeTool = allSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; - expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); - executeToolSpanId = executeTool.span_id; + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + // The model-provider requests Mastra makes for each step. + const modelFetches = receivedSpans.filter( + span => + span.attributes[SENTRY_OP]?.value === 'http.client' && + String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), + ); + expect(modelFetches.length).toBeGreaterThan(0); + modelFetchParentIds = modelFetches.map(span => span.parent_span_id); + + // The per-key `dataloader.load` spans opened while the tool runs. (dataloader also emits a + // `dataloader.batch` span nested under a load — its own internal structure.) + const loadSpans = receivedSpans.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', + ); + expect(loadSpans.length).toBeGreaterThan(0); + cacheGetParentIds = loadSpans.map(span => span.parent_span_id); + const chat = receivedSpans.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; + expect(chat).toBeDefined(); + chatSpanId = chat.span_id; + + const executeTool = receivedSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; + expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); + executeToolSpanId = executeTool.span_id; + // The `executeWithContext` bridge makes the exporter spans active during the real work, so the // model fetch parents onto the `chat` span and the dataloader loads onto `execute_tool`. expect(chatSpanId).toBeDefined(); @@ -378,7 +377,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { test('nests the model fetch and tool work under the exporter spans when streaming', async () => { // Same nesting guarantee as `generate`, but the agent is driven via `stream()` — the model // request is a real streaming (SSE) call, and the tool still runs inside `executeWithContext`. - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; let chatSpanId: string | undefined; let executeToolSpanId: string | undefined; @@ -389,36 +388,35 @@ conditionalTest({ min: 22 })('Mastra integration', () => { .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-test'); - expect(segment).toBeDefined(); - const modelFetches = allSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'http.client' && - String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), - ); - expect(modelFetches.length).toBeGreaterThan(0); - modelFetchParentIds = modelFetches.map(span => span.parent_span_id); - - const loadSpans = allSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'cache.get' && - span.attributes[DB_OPERATION_NAME]?.value === 'load', - ); - expect(loadSpans.length).toBeGreaterThan(0); - cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - const chat = allSpans.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; - expect(chat).toBeDefined(); - chatSpanId = chat.span_id; - - const executeTool = allSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; - expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); - executeToolSpanId = executeTool.span_id; + expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + const modelFetches = receivedSpans.filter( + span => + span.attributes[SENTRY_OP]?.value === 'http.client' && + String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), + ); + expect(modelFetches.length).toBeGreaterThan(0); + modelFetchParentIds = modelFetches.map(span => span.parent_span_id); + + const loadSpans = receivedSpans.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', + ); + expect(loadSpans.length).toBeGreaterThan(0); + cacheGetParentIds = loadSpans.map(span => span.parent_span_id); + const chat = receivedSpans.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; + expect(chat).toBeDefined(); + chatSpanId = chat.span_id; + + const executeTool = receivedSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; + expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); + executeToolSpanId = executeTool.span_id; + expect(chatSpanId).toBeDefined(); expect(executeToolSpanId).toBeDefined(); expect(new Set(modelFetchParentIds)).toEqual(new Set([chatSpanId])); @@ -437,7 +435,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { // Two instances run concurrently in one trace. Each instance's `dataloader.load` must nest under // that instance's own `execute_tool` span — if the registry cross-talked, one tool span would // have no dataloader child. - const allSpans: SerializedStreamedSpanContainer['items'] = []; + let receivedSpans: SerializedStreamedSpanContainer['items'] = []; const toolSpanIds: Record = {}; let loadParentIds: (string | undefined)[] = []; @@ -446,25 +444,24 @@ conditionalTest({ min: 22 })('Mastra integration', () => { .unordered() .expect({ span: container => { - allSpans.push(...container.items); - const segment = allSpans.find(span => span.is_segment && span.name === 'mastra-multi'); - expect(segment).toBeDefined(); - const loadSpans = allSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'cache.get' && - span.attributes[DB_OPERATION_NAME]?.value === 'load', - ); - loadParentIds = loadSpans.map(span => span.parent_span_id); - for (const toolName of ['count_a', 'count_b']) { - const toolSpan = allSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === toolName)!; - expect(toolSpan).toBeDefined(); - toolSpanIds[toolName] = toolSpan.span_id; - } + expect(container.items.find(span => span.is_segment && span.name === 'mastra-multi')).toBeDefined(); + receivedSpans = container.items; }, }) .start() .completed(); + const loadSpans = receivedSpans.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', + ); + loadParentIds = loadSpans.map(span => span.parent_span_id); + for (const toolName of ['count_a', 'count_b']) { + const toolSpan = receivedSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === toolName)!; + expect(toolSpan).toBeDefined(); + toolSpanIds[toolName] = toolSpan.span_id; + } + // Each instance's tool span parents at least one dataloader load — neither is starved. for (const toolName of ['count_a', 'count_b']) { expect(loadParentIds).toContain(toolSpanIds[toolName]); From ec420535adf5bd1ead24fa3d3711ed51cc513e89 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 15:01:13 +0200 Subject: [PATCH 6/7] test(node): Simplify Mastra span assertions Co-Authored-By: GPT-6 --- .../suites/tracing/mastra/test.ts | 374 ++++++++---------- 1 file changed, 160 insertions(+), 214 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts index ab0d304cdb25..1b7a40ebcd34 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -1,4 +1,3 @@ -import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { DB_OPERATION_NAME, GEN_AI_AGENT_NAME, @@ -55,66 +54,56 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('creates invoke_agent and chat spans following the gen_ai conventions', async () => { - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); + + const agentSpan = spans.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 = spans.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(); - - const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); - - const agentSpan = spans.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 = spans.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); }); test('omits prompts and responses when genAI recording is off', async () => { - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + for (const span of spans) { + 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(); - - const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - for (const span of spans) { - expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); - expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); - expect(span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]).toBeUndefined(); - } }); }, MASTRA_DEPENDENCIES, @@ -126,28 +115,23 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('records prompts and responses when genAI recording is on', async () => { - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + const agentSpan = spans.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 = spans.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(); - - const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - const agentSpan = spans.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 = spans.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.'); }); }, MASTRA_DEPENDENCIES, @@ -159,43 +143,38 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('creates an execute_tool span parented onto the generation', async () => { - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual([ + 'chat gpt-4o-mini', + 'execute_tool get_weather', + 'invoke_agent weather_agent', + ]); + + const toolSpan = spans.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 = spans.find(span => span.name === 'chat gpt-4o-mini')!; + expect(toolSpan.parent_span_id).toBe(chatSpan.span_id); + + const agentSpan = spans.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(); - - const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - expect(spans.map(span => span.name).sort()).toEqual([ - 'chat gpt-4o-mini', - 'execute_tool get_weather', - 'invoke_agent weather_agent', - ]); - - const toolSpan = spans.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 = spans.find(span => span.name === 'chat gpt-4o-mini')!; - expect(toolSpan.parent_span_id).toBe(chatSpan.span_id); - - const agentSpan = spans.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); }); }, MASTRA_DEPENDENCIES, @@ -233,24 +212,19 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('auto-instruments a Mastra app that configured no observability at all', async () => { - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); + for (const span of spans) { + expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.mastra'); + } }, }) .start() .completed(); - - const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); - for (const span of spans) { - expect(span.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.mastra'); - } }); }, MASTRA_DEPENDENCIES, @@ -262,21 +236,16 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('still exports its own spans when the community @mastra/sentry exporter is present', async () => { - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); }, }) .start() .completed(); - - const spans = receivedSpans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); - expect(spans.map(span => span.name).sort()).toEqual(['chat gpt-4o-mini', 'invoke_agent weather_agent']); }); }, MASTRA_DEPENDENCIES, @@ -287,23 +256,18 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument-no-bootstrap.mjs', (createRunner, test) => { test('leaves an unconfigured app alone when bootstrapObservability is false', async () => { - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + const genAiSpans = container.items.filter(span => + String(span.attributes[SENTRY_OP]?.value ?? '').startsWith('gen_ai.'), + ); + expect(genAiSpans).toEqual([]); }, }) .start() .completed(); - - const genAiSpans = receivedSpans.filter(span => - String(span.attributes[SENTRY_OP]?.value ?? '').startsWith('gen_ai.'), - ); - expect(genAiSpans).toEqual([]); }); }, MASTRA_DEPENDENCIES, @@ -315,55 +279,48 @@ conditionalTest({ min: 22 })('Mastra integration', () => { 'instrument.mjs', (createRunner, test) => { test('nests the model fetch and tool work under the exporter spans', async () => { - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - - let chatSpanId: string | undefined; - let executeToolSpanId: string | undefined; - let modelFetchParentIds: (string | undefined)[] = []; - let cacheGetParentIds: (string | undefined)[] = []; - await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + // The model-provider requests Mastra makes for each step. + const modelFetches = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'http.client' && + String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), + ); + expect(modelFetches.length).toBeGreaterThan(0); + const modelFetchParentIds = modelFetches.map(span => span.parent_span_id); + + // The per-key `dataloader.load` spans opened while the tool runs. (dataloader also emits a + // `dataloader.batch` span nested under a load — its own internal structure.) + const loadSpans = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && + span.attributes[DB_OPERATION_NAME]?.value === 'load', + ); + expect(loadSpans.length).toBeGreaterThan(0); + const cacheGetParentIds = loadSpans.map(span => span.parent_span_id); + const chat = container.items.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; + expect(chat).toBeDefined(); + const chatSpanId = chat.span_id; + + const executeTool = container.items.find( + span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items', + )!; + expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); + const executeToolSpanId = executeTool.span_id; + + // The `executeWithContext` bridge makes the exporter spans active during the real work, so the + // model fetch parents onto the `chat` span and the dataloader loads onto `execute_tool`. + expect(chatSpanId).toBeDefined(); + expect(executeToolSpanId).toBeDefined(); + expect(new Set(modelFetchParentIds)).toEqual(new Set([chatSpanId])); + expect(new Set(cacheGetParentIds)).toEqual(new Set([executeToolSpanId])); }, }) .start() .completed(); - - // The model-provider requests Mastra makes for each step. - const modelFetches = receivedSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'http.client' && - String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), - ); - expect(modelFetches.length).toBeGreaterThan(0); - modelFetchParentIds = modelFetches.map(span => span.parent_span_id); - - // The per-key `dataloader.load` spans opened while the tool runs. (dataloader also emits a - // `dataloader.batch` span nested under a load — its own internal structure.) - const loadSpans = receivedSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', - ); - expect(loadSpans.length).toBeGreaterThan(0); - cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - const chat = receivedSpans.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; - expect(chat).toBeDefined(); - chatSpanId = chat.span_id; - - const executeTool = receivedSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; - expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); - executeToolSpanId = executeTool.span_id; - - // The `executeWithContext` bridge makes the exporter spans active during the real work, so the - // model fetch parents onto the `chat` span and the dataloader loads onto `execute_tool`. - expect(chatSpanId).toBeDefined(); - expect(executeToolSpanId).toBeDefined(); - expect(new Set(modelFetchParentIds)).toEqual(new Set([chatSpanId])); - expect(new Set(cacheGetParentIds)).toEqual(new Set([executeToolSpanId])); }); }, MASTRA_NESTING_DEPENDENCIES, @@ -377,50 +334,44 @@ conditionalTest({ min: 22 })('Mastra integration', () => { test('nests the model fetch and tool work under the exporter spans when streaming', async () => { // Same nesting guarantee as `generate`, but the agent is driven via `stream()` — the model // request is a real streaming (SSE) call, and the tool still runs inside `executeWithContext`. - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - - let chatSpanId: string | undefined; - let executeToolSpanId: string | undefined; - let modelFetchParentIds: (string | undefined)[] = []; - let cacheGetParentIds: (string | undefined)[] = []; await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); - receivedSpans = container.items; + const modelFetches = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'http.client' && + String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), + ); + expect(modelFetches.length).toBeGreaterThan(0); + const modelFetchParentIds = modelFetches.map(span => span.parent_span_id); + + const loadSpans = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && + span.attributes[DB_OPERATION_NAME]?.value === 'load', + ); + expect(loadSpans.length).toBeGreaterThan(0); + const cacheGetParentIds = loadSpans.map(span => span.parent_span_id); + const chat = container.items.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; + expect(chat).toBeDefined(); + const chatSpanId = chat.span_id; + + const executeTool = container.items.find( + span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items', + )!; + expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); + const executeToolSpanId = executeTool.span_id; + + expect(chatSpanId).toBeDefined(); + expect(executeToolSpanId).toBeDefined(); + expect(new Set(modelFetchParentIds)).toEqual(new Set([chatSpanId])); + expect(new Set(cacheGetParentIds)).toEqual(new Set([executeToolSpanId])); }, }) .start() .completed(); - - const modelFetches = receivedSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'http.client' && - String(span.attributes[URL_FULL]?.value ?? '').includes('/v1/chat/completions'), - ); - expect(modelFetches.length).toBeGreaterThan(0); - modelFetchParentIds = modelFetches.map(span => span.parent_span_id); - - const loadSpans = receivedSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', - ); - expect(loadSpans.length).toBeGreaterThan(0); - cacheGetParentIds = loadSpans.map(span => span.parent_span_id); - const chat = receivedSpans.find(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')!; - expect(chat).toBeDefined(); - chatSpanId = chat.span_id; - - const executeTool = receivedSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'count_items')!; - expect(executeTool.attributes[SENTRY_OP]?.value).toBe('gen_ai.execute_tool'); - executeToolSpanId = executeTool.span_id; - - expect(chatSpanId).toBeDefined(); - expect(executeToolSpanId).toBeDefined(); - expect(new Set(modelFetchParentIds)).toEqual(new Set([chatSpanId])); - expect(new Set(cacheGetParentIds)).toEqual(new Set([executeToolSpanId])); }); }, MASTRA_NESTING_DEPENDENCIES, @@ -435,42 +386,37 @@ conditionalTest({ min: 22 })('Mastra integration', () => { // Two instances run concurrently in one trace. Each instance's `dataloader.load` must nest under // that instance's own `execute_tool` span — if the registry cross-talked, one tool span would // have no dataloader child. - let receivedSpans: SerializedStreamedSpanContainer['items'] = []; - - const toolSpanIds: Record = {}; - let loadParentIds: (string | undefined)[] = []; await createRunner() - .unordered() .expect({ span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-multi')).toBeDefined(); - receivedSpans = container.items; + const loadSpans = container.items.filter( + span => + span.attributes[SENTRY_OP]?.value === 'cache.get' && + span.attributes[DB_OPERATION_NAME]?.value === 'load', + ); + const loadParentIds = loadSpans.map(span => span.parent_span_id); + const toolSpanIds: Record = {}; + for (const toolName of ['count_a', 'count_b']) { + const toolSpan = container.items.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === toolName)!; + expect(toolSpan).toBeDefined(); + toolSpanIds[toolName] = toolSpan.span_id; + } + + // Each instance's tool span parents at least one dataloader load — neither is starved. + for (const toolName of ['count_a', 'count_b']) { + expect(loadParentIds).toContain(toolSpanIds[toolName]); + } + // And every load parents onto one of the two tool spans (no stray reparenting). + const toolIds = new Set(Object.values(toolSpanIds)); + for (const parent of loadParentIds) { + expect(toolIds.has(parent!)).toBe(true); + } }, }) .start() .completed(); - - const loadSpans = receivedSpans.filter( - span => - span.attributes[SENTRY_OP]?.value === 'cache.get' && span.attributes[DB_OPERATION_NAME]?.value === 'load', - ); - loadParentIds = loadSpans.map(span => span.parent_span_id); - for (const toolName of ['count_a', 'count_b']) { - const toolSpan = receivedSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === toolName)!; - expect(toolSpan).toBeDefined(); - toolSpanIds[toolName] = toolSpan.span_id; - } - - // Each instance's tool span parents at least one dataloader load — neither is starved. - for (const toolName of ['count_a', 'count_b']) { - expect(loadParentIds).toContain(toolSpanIds[toolName]); - } - // And every load parents onto one of the two tool spans (no stray reparenting). - const toolIds = new Set(Object.values(toolSpanIds)); - for (const parent of loadParentIds) { - expect(toolIds.has(parent!)).toBe(true); - } }); }, MASTRA_NESTING_DEPENDENCIES, From ef489ce581267d80d6c552f77c0fca8be8595316 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 15:19:10 +0200 Subject: [PATCH 7/7] test(node): Require Mastra spans in recording-off test Co-Authored-By: GPT-6 --- .../node-integration-tests/suites/tracing/mastra/test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts index 1b7a40ebcd34..281362c074f4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mastra/test.ts @@ -95,6 +95,7 @@ conditionalTest({ min: 22 })('Mastra integration', () => { span: container => { expect(container.items.find(span => span.is_segment && span.name === 'mastra-test')).toBeDefined(); const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.mastra'); + expect(spans).toHaveLength(2); for (const span of spans) { expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined();