Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a9c82ff
feat(node): Wire up SentryTracerProvider
andreiborza Jun 9, 2026
5dfd33f
Add e2e SentryTracerProvider variants
andreiborza Jun 22, 2026
6745e2c
Set the `response` context in httpServerSpansIntegration
andreiborza Jun 22, 2026
707d2b0
Fix imports
andreiborza Jun 22, 2026
a71a289
Remove the redundant setOpenTelemetryContextAsyncContextStrategy calls
andreiborza Jun 22, 2026
8e0fc3d
Fix node-connect tests
andreiborza Jun 23, 2026
aad25dd
Make SentryTracerProvider the default for @sentry/node
andreiborza Jun 23, 2026
566e02b
Drop orphan http.client fetch spans in the fetch instrumentation
andreiborza Jun 24, 2026
f06771f
Drop redundant stream-lifecycle guard in the otel.resource preprocess…
andreiborza Jun 24, 2026
98cf9c8
Resolve outgoing fetch span status from the HTTP response status code
andreiborza Jun 24, 2026
24d3de8
Expect a custom source after span.updateName in the streamed test
andreiborza Jun 24, 2026
bd7df38
Await the non-streamed updateName-method test and expect a custom source
andreiborza Jun 24, 2026
820a6ff
Run the streamed-span backfill on the SentryTracerProvider path
andreiborza Jun 25, 2026
ae90cb5
Assert langgraph createReactAgent spans order-independently
andreiborza Jun 25, 2026
0056e94
Defer the Node SDK transaction capture with a debounced timer
andreiborza Jun 25, 2026
bd104d2
Expect the default manual origin on streamed mysql and postgres db spans
andreiborza Jun 26, 2026
03a6e90
Skip prisma v5/v6 provider tests pending complete span-tree capture
andreiborza Jun 26, 2026
60c4bf6
Scope the deferred transaction capture to the SentryTracerProvider
andreiborza Jun 26, 2026
3f7f907
Skip fastify provider E2E tests pending instrumentation streamlining
andreiborza Jun 27, 2026
81d0005
Drop the removed deferral opt-out argument in initOtel
andreiborza Jun 29, 2026
ce4df73
Keep httpServerSpansIntegration under the max-lines limit
andreiborza Jun 29, 2026
502b866
Skip prisma v7 provider tests pending complete span-tree capture
andreiborza Jun 30, 2026
7f39f99
Un-skip prisma provider tests to check on CI
andreiborza Jun 30, 2026
b72f0fc
Skip the Prisma v5 provider test under the SentryTracerProvider
andreiborza Jun 30, 2026
bf1224b
Unskip node-fastify-4/5 API route transaction e2e tests
andreiborza Jul 1, 2026
7f73d42
Enable deferred segment-span capture from the NodeClient constructor
andreiborza Jul 1, 2026
06c32e9
Flush the deferred transaction before asserting envelopes in svelteki…
andreiborza Jul 2, 2026
a67c57d
Advance fake timers in the profiling-node global profile context test…
andreiborza Jul 2, 2026
9866b1a
Document that openTelemetrySpanProcessors forces the full OTel tracer…
andreiborza Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('Sends an API route transaction', async ({ baseURL }) => {
// TODO(provider): The SentryTracerProvider (now the default for @sentry/node) creates native spans,
// so the vendored fastify instrumentation renaming hook spans via `span.updateName()` in its
// `spanStart` listener stamps `sentry.source: 'custom'` on them. The OTel SDK path never set a source
// on these child spans, so this assertion fails. The fix is to name the span at creation in the
// instrumentation instead of renaming it (cf. the fastify streamlining in #21706); re-enable then.
test.skip('Sends an API route transaction', async ({ baseURL }) => {
const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,41 +54,44 @@ test('Sends an API route transaction', async ({ baseURL }) => {
origin: 'auto.http.otel.http',
});

const manualSpanExpectation = {
data: {
'sentry.origin': 'manual',
},
description: 'test-span',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
origin: 'manual',
};

const connectSpanExpectation = {
data: {
'sentry.origin': 'auto.http.otel.connect',
'sentry.op': 'request_handler.connect',
'http.route': '/test-transaction',
'connect.type': 'request_handler',
'connect.name': '/test-transaction',
},
op: 'request_handler.connect',
description: '/test-transaction',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
origin: 'auto.http.otel.connect',
};

expect(transactionEvent).toEqual(
expect.objectContaining({
spans: [
{
data: {
'sentry.origin': 'manual',
},
description: 'test-span',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
origin: 'manual',
},
{
data: {
'sentry.origin': 'auto.http.otel.connect',
'sentry.op': 'request_handler.connect',
'http.route': '/test-transaction',
'connect.type': 'request_handler',
'connect.name': '/test-transaction',
},
op: 'request_handler.connect',
description: '/test-transaction',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
origin: 'auto.http.otel.connect',
},
],
// The SentryTracerProvider serializes native child spans in start/tree order, so the
// Connect handler span appears before the manual span created inside it.
spans: [connectSpanExpectation, manualSpanExpectation],
transaction: 'GET /test-transaction',
type: 'transaction',
transaction_info: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('Sends an API route transaction', async ({ baseURL }) => {
// TODO(provider): The SentryTracerProvider (now the default for @sentry/node) creates native spans,
// so the vendored fastify instrumentation renaming hook spans via `span.updateName()` in its
// `spanStart` listener stamps `sentry.source: 'custom'` on them. The OTel SDK path never set a source
// on these child spans, so this assertion fails. The fix is to name the span at creation in the
// instrumentation instead of renaming it (cf. the fastify streamlining in #21706); re-enable then.
test.skip('Sends an API route transaction', async ({ baseURL }) => {
const pageloadTransactionEventPromise = waitForTransaction('node-fastify-3', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
SDK_VERSION,
SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
Expand Down Expand Up @@ -63,6 +64,7 @@ test('sends a streamed span envelope with correct spans for a manually started s
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' },
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' },
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' },
'sentry.span.source': { type: 'string', value: 'custom' },
},
Expand All @@ -86,6 +88,7 @@ test('sends a streamed span envelope with correct spans for a manually started s
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' },
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' },
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' },
'sentry.span.source': { type: 'string', value: 'custom' },
},
Expand Down Expand Up @@ -122,6 +125,7 @@ test('sends a streamed span envelope with correct spans for a manually started s
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' },
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' },
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' },
'sentry.span.source': { type: 'string', value: 'custom' },
},
Expand All @@ -148,6 +152,7 @@ test('sends a streamed span envelope with correct spans for a manually started s
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' },
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' },
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' },
'sentry.span.source': { type: 'string', value: 'custom' },
'process.runtime.engine.name': { type: 'string', value: 'v8' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ test('updates the span name when calling `span.updateName` (streamed)', async ()
name: 'new name',
is_segment: true,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'url' },
// `updateName` marks the name as explicitly chosen, so the source becomes `custom`,
// overriding the `url` source set at span start (a stale `url` no longer describes the name).
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' },
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ afterAll(() => {
});

test('updates the span name when calling `span.updateName`', async () => {
createRunner(__dirname, 'scenario.ts')
await createRunner(__dirname, 'scenario.ts')
.expect({
transaction: {
transaction: 'new name',
transaction_info: { source: 'url' },
// `updateName` marks the name as explicitly chosen, so the source becomes `custom`,
// overriding the `url` source set at span start (a stale `url` no longer describes the name).
transaction_info: { source: 'custom' },
contexts: {
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' },
data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
},
},
},
Expand Down
147 changes: 77 additions & 70 deletions dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,89 +356,96 @@ describe('LangGraph integration', () => {
},
);

// createReactAgent tests
const EXPECTED_TRANSACTION_REACT_AGENT = {
transaction: 'main',
spans: [
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph',
[GEN_AI_AGENT_NAME_ATTRIBUTE]: 'helpful_assistant',
[GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'helpful_assistant',
}),
description: 'invoke_agent helpful_assistant',
op: 'gen_ai.invoke_agent',
origin: 'auto.ai.langgraph',
status: 'ok',
}),
expect.objectContaining({ op: 'http.client' }),
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_AGENT_NAME_ATTRIBUTE]: 'helpful_assistant',
}),
op: 'gen_ai.chat',
}),
],
};

// createReactAgent tests.
// Spans are asserted order-independently: the span-array order is not a protocol guarantee (Sentry
// rebuilds the tree from `parent_span_id`), and the provider emits tree order while the OTel exporter
// emits finish order (the `http.client` that the chat span wraps finishes before the chat span itself).
createEsmAndCjsTests(__dirname, 'agent-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => {
test('should instrument createReactAgent with agent and chat spans', { timeout: 30000 }, async () => {
await createRunner()
.ignore('event')
.expect({ transaction: EXPECTED_TRANSACTION_REACT_AGENT })
.expect({
transaction: event => {
const spans = event.spans ?? [];
expect(event.transaction).toBe('main');
expect(spans).toHaveLength(3);
expect(spans).toContainEqual(
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph',
[GEN_AI_AGENT_NAME_ATTRIBUTE]: 'helpful_assistant',
[GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'helpful_assistant',
}),
description: 'invoke_agent helpful_assistant',
op: 'gen_ai.invoke_agent',
origin: 'auto.ai.langgraph',
status: 'ok',
}),
);
expect(spans).toContainEqual(expect.objectContaining({ op: 'http.client' }));
expect(spans).toContainEqual(
expect.objectContaining({
data: expect.objectContaining({ [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'helpful_assistant' }),
op: 'gen_ai.chat',
}),
);
},
})
.start()
.completed();
});
});

// createReactAgent with tools - verifies tool execution spans
const EXPECTED_TRANSACTION_REACT_AGENT_TOOLS = {
transaction: 'main',
spans: [
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent',
[GEN_AI_AGENT_NAME_ATTRIBUTE]: 'math_assistant',
}),
op: 'gen_ai.invoke_agent',
status: 'ok',
}),
expect.objectContaining({ op: 'http.client' }),
expect.objectContaining({ op: 'gen_ai.chat' }),
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool',
[GEN_AI_TOOL_NAME_ATTRIBUTE]: 'add',
'gen_ai.tool.type': 'function',
}),
description: 'execute_tool add',
op: 'gen_ai.execute_tool',
status: 'ok',
}),
expect.objectContaining({ op: 'http.client' }),
expect.objectContaining({ op: 'gen_ai.chat' }),
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool',
[GEN_AI_TOOL_NAME_ATTRIBUTE]: 'multiply',
'gen_ai.tool.type': 'function',
}),
description: 'execute_tool multiply',
op: 'gen_ai.execute_tool',
status: 'ok',
}),
expect.objectContaining({ op: 'http.client' }),
expect.objectContaining({ op: 'gen_ai.chat' }),
],
};

// createReactAgent with tools - verifies tool execution spans (asserted order-independently, see above).
createEsmAndCjsTests(__dirname, 'agent-tools-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => {
test('should create tool execution spans for createReactAgent with tools', { timeout: 30000 }, async () => {
await createRunner()
.ignore('event')
.expect({ transaction: EXPECTED_TRANSACTION_REACT_AGENT_TOOLS })
.expect({
transaction: event => {
const spans = event.spans ?? [];
expect(event.transaction).toBe('main');
expect(spans).toHaveLength(9);
expect(spans).toContainEqual(
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent',
[GEN_AI_AGENT_NAME_ATTRIBUTE]: 'math_assistant',
}),
op: 'gen_ai.invoke_agent',
status: 'ok',
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool',
[GEN_AI_TOOL_NAME_ATTRIBUTE]: 'add',
'gen_ai.tool.type': 'function',
}),
description: 'execute_tool add',
op: 'gen_ai.execute_tool',
status: 'ok',
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
data: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool',
[GEN_AI_TOOL_NAME_ATTRIBUTE]: 'multiply',
'gen_ai.tool.type': 'function',
}),
description: 'execute_tool multiply',
op: 'gen_ai.execute_tool',
status: 'ok',
}),
);
expect(spans.filter(span => span.op === 'http.client')).toHaveLength(3);
expect(spans.filter(span => span.op === 'gen_ai.chat')).toHaveLength(3);
},
})
.start()
.completed();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fetch('http://localhost:9999/external').catch(() => {});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,28 @@ describe('no_parent_span client report', () => {
});

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('records no_parent_span outcome for http.client span without a local parent', async () => {
test('records no_parent_span outcome for an outgoing http request without a local parent', async () => {
const runner = createRunner()
.unignore('client_report')
.expect({
client_report: report => {
expect(report.discarded_events).toEqual([
{
category: 'span',
quantity: 1,
reason: 'no_parent_span',
},
]);
},
})
.start();

await runner.completed();
});
});

createEsmAndCjsTests(__dirname, 'scenario-fetch.mjs', 'instrument.mjs', (createRunner, test) => {
test('records no_parent_span outcome for an outgoing fetch request without a local parent', async () => {
const runner = createRunner()
.unignore('client_report')
.expect({
Expand Down
Loading
Loading