Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
185e876
feat(node): Wire up SentryTracerProvider
andreiborza Jun 9, 2026
e5b2536
Add e2e SentryTracerProvider variants
andreiborza Jun 22, 2026
18a22a6
Set the `response` context in httpServerSpansIntegration
andreiborza Jun 22, 2026
c7190b1
Fix imports
andreiborza Jun 22, 2026
db87f4c
Remove the redundant setOpenTelemetryContextAsyncContextStrategy calls
andreiborza Jun 22, 2026
4079d6b
Fix node-connect tests
andreiborza Jun 23, 2026
3eb06f5
Make SentryTracerProvider the default for @sentry/node
andreiborza Jun 23, 2026
aa1c504
Drop orphan http.client fetch spans in the fetch instrumentation
andreiborza Jun 24, 2026
5becdeb
Drop redundant stream-lifecycle guard in the otel.resource preprocess…
andreiborza Jun 24, 2026
d371483
Resolve outgoing fetch span status from the HTTP response status code
andreiborza Jun 24, 2026
75aab00
Expect a custom source after span.updateName in the streamed test
andreiborza Jun 24, 2026
7434e95
Await the non-streamed updateName-method test and expect a custom source
andreiborza Jun 24, 2026
2f784bc
Run the streamed-span backfill on the SentryTracerProvider path
andreiborza Jun 25, 2026
8328b02
Assert langgraph createReactAgent spans order-independently
andreiborza Jun 25, 2026
fe02757
End the gen_ai span before `.asResponse()` resolves
andreiborza Jun 25, 2026
eee3e5c
Defer the SentryTracerProvider transaction capture by a microtask
andreiborza Jun 25, 2026
4a97d0d
Expect the default manual origin on streamed mysql and postgres db spans
andreiborza Jun 26, 2026
b84dc4d
Skip prisma v5/v6 provider tests pending complete span-tree capture
andreiborza Jun 26, 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
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
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
Expand Up @@ -52,6 +52,13 @@ describe('mysql auto instrumentation (streamed)', () => {
type: 'string',
value: 'db',
},
// The `mysql` (v1) instrumentation sets no explicit span origin, so these spans carry the
// default `manual` origin. The streamed-span path writes it as a first-class attribute (the
// non-streamed/SDK path omits the `manual` default, which is why this wasn't asserted before).
'sentry.origin': {
type: 'string',
value: 'manual',
},
'sentry.release': {
type: 'string',
value: '1.0',
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
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ const COMMON_DB_ATTRIBUTES = {

/**
* Builds the expected strict shape of a streamed postgres db span.
* The `pg.connect` span has neither a `db.statement` nor a `sentry.origin`,
* whereas query spans carry both.
* Query spans carry a `db.statement` and the `auto.db.otel.postgres` origin. The `pg.connect` span
* has no `db.statement`, and since the pg instrumentation sets no origin on it, it carries the
* default `manual` origin (written as an attribute on the streamed-span path; the non-streamed/SDK
* path omits the `manual` default).
*/
function expectedDbSpan({ name, statement }: { name: string; statement?: string }): unknown {
const attributes: Record<string, unknown> = { ...COMMON_DB_ATTRIBUTES };
Expand All @@ -88,6 +90,11 @@ function expectedDbSpan({ name, statement }: { name: string; statement?: string
type: 'string',
value: 'auto.db.otel.postgres',
};
} else {
attributes['sentry.origin'] = {
type: 'string',
value: 'manual',
};
}

return {
Expand Down
Loading
Loading