Skip to content

Commit c966a4a

Browse files
committed
test(node,node-core): Add span streaming integration tests (#19806)
Extends our Node (core) integration test runner API to expect `span` envelopes with both an API to test against span envelope headers as well as the container. In addition, this adds a couple of integration tests testing manually started spans, span name updates and span relationships. Luckily, all of the span relationship logic is independent from span streaming so the tests all pass. I still believe it's valuable to have a `span` version of them.
1 parent e582839 commit c966a4a

File tree

31 files changed

+1039
-7
lines changed

31 files changed

+1039
-7
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ module.exports = [
317317
import: createImport('init'),
318318
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
319319
gzip: true,
320-
limit: '57 KB',
320+
limit: '59 KB',
321321
},
322322
// Node SDK (ESM)
323323
{
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as Sentry from '@sentry/node-core';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
import { setupOtel } from '../../../../utils/setupOtel';
4+
5+
const client = Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
tracesSampleRate: 1.0,
8+
traceLifecycle: 'stream',
9+
integrations: [Sentry.spanStreamingIntegration()],
10+
transport: loggingTransport,
11+
release: '1.0.0',
12+
});
13+
14+
setupOtel(client);
15+
16+
Sentry.startSpan({ name: 'test-span', op: 'test' }, segmentSpan => {
17+
Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => {
18+
// noop
19+
});
20+
21+
const inactiveSpan = Sentry.startInactiveSpan({ name: 'test-inactive-span' });
22+
inactiveSpan.addLink({ context: segmentSpan.spanContext(), attributes: { 'sentry.link.type': 'some_relation' } });
23+
inactiveSpan.end();
24+
25+
Sentry.startSpanManual({ name: 'test-manual-span' }, span => {
26+
span.end();
27+
});
28+
});
29+
30+
void Sentry.flush();
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {
2+
SDK_VERSION,
3+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
4+
SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
5+
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
6+
SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME,
7+
SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION,
8+
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID,
9+
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME,
10+
} from '@sentry/core';
11+
import { expect, test } from 'vitest';
12+
import { createRunner } from '../../../../utils/runner';
13+
14+
test('sends a streamed span envelope with correct envelope header', async () => {
15+
await createRunner(__dirname, 'scenario.ts')
16+
.expectHeader({
17+
span: {
18+
sent_at: expect.any(String),
19+
sdk: {
20+
name: 'sentry.javascript.node-core',
21+
version: SDK_VERSION,
22+
},
23+
trace: expect.objectContaining({
24+
public_key: 'public',
25+
sample_rate: '1',
26+
sampled: 'true',
27+
trace_id: expect.stringMatching(/^[\da-f]{32}$/),
28+
transaction: 'test-span',
29+
}),
30+
},
31+
})
32+
.start()
33+
.completed();
34+
});
35+
36+
test('sends a streamed span envelope with correct spans for a manually started span with children', async () => {
37+
await createRunner(__dirname, 'scenario.ts')
38+
.expect({
39+
span: container => {
40+
const spans = container.items;
41+
expect(spans.length).toBe(4);
42+
43+
const segmentSpan = spans.find(s => !!s.is_segment);
44+
expect(segmentSpan).toBeDefined();
45+
46+
const segmentSpanId = segmentSpan!.span_id;
47+
const traceId = segmentSpan!.trace_id;
48+
49+
const childSpan = spans.find(s => s.name === 'test-child-span');
50+
expect(childSpan).toBeDefined();
51+
expect(childSpan).toEqual({
52+
attributes: {
53+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
54+
type: 'string',
55+
value: 'test-child',
56+
},
57+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' },
58+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
59+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId },
60+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' },
61+
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
62+
},
63+
name: 'test-child-span',
64+
is_segment: false,
65+
parent_span_id: segmentSpanId,
66+
trace_id: traceId,
67+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
68+
start_timestamp: expect.any(Number),
69+
end_timestamp: expect.any(Number),
70+
status: 'ok',
71+
});
72+
73+
const inactiveSpan = spans.find(s => s.name === 'test-inactive-span');
74+
expect(inactiveSpan).toBeDefined();
75+
expect(inactiveSpan).toEqual({
76+
attributes: {
77+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' },
78+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
79+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId },
80+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' },
81+
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
82+
},
83+
links: [
84+
{
85+
attributes: {
86+
'sentry.link.type': {
87+
type: 'string',
88+
value: 'some_relation',
89+
},
90+
},
91+
sampled: true,
92+
span_id: segmentSpanId,
93+
trace_id: traceId,
94+
},
95+
],
96+
name: 'test-inactive-span',
97+
is_segment: false,
98+
parent_span_id: segmentSpanId,
99+
trace_id: traceId,
100+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
101+
start_timestamp: expect.any(Number),
102+
end_timestamp: expect.any(Number),
103+
status: 'ok',
104+
});
105+
106+
const manualSpan = spans.find(s => s.name === 'test-manual-span');
107+
expect(manualSpan).toBeDefined();
108+
expect(manualSpan).toEqual({
109+
attributes: {
110+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' },
111+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
112+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId },
113+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' },
114+
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
115+
},
116+
name: 'test-manual-span',
117+
is_segment: false,
118+
parent_span_id: segmentSpanId,
119+
trace_id: traceId,
120+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
121+
start_timestamp: expect.any(Number),
122+
end_timestamp: expect.any(Number),
123+
status: 'ok',
124+
});
125+
126+
expect(segmentSpan).toEqual({
127+
attributes: {
128+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' },
129+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 },
130+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' },
131+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
132+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId },
133+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' },
134+
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
135+
},
136+
name: 'test-span',
137+
is_segment: true,
138+
trace_id: traceId,
139+
span_id: segmentSpanId,
140+
start_timestamp: expect.any(Number),
141+
end_timestamp: expect.any(Number),
142+
status: 'ok',
143+
});
144+
},
145+
})
146+
.start()
147+
.completed();
148+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as Sentry from '@sentry/node-core';
2+
import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
3+
import { setupOtel } from '../../../../utils/setupOtel';
4+
5+
const client = Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
release: '1.0',
8+
tracesSampleRate: 1.0,
9+
traceLifecycle: 'stream',
10+
transport: loggingTransport,
11+
});
12+
13+
setupOtel(client);
14+
15+
Sentry.getCurrentScope().setPropagationContext({
16+
parentSpanId: '1234567890123456',
17+
traceId: '12345678901234567890123456789012',
18+
sampleRand: Math.random(),
19+
});
20+
21+
const spanIdTraceId = Sentry.startSpan(
22+
{
23+
name: 'test_span_1',
24+
},
25+
span1 => span1.spanContext().traceId,
26+
);
27+
28+
Sentry.startSpan(
29+
{
30+
name: 'test_span_2',
31+
attributes: { spanIdTraceId },
32+
},
33+
() => undefined,
34+
);
35+
36+
Sentry.flush();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { afterAll, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('sends manually started streamed parallel root spans in root context', async () => {
9+
expect.assertions(7);
10+
11+
await createRunner(__dirname, 'scenario.ts')
12+
.expect({ span: { items: [{ name: 'test_span_1' }] } })
13+
.expect({
14+
span: spanContainer => {
15+
expect(spanContainer).toBeDefined();
16+
const traceId = spanContainer.items[0]!.trace_id;
17+
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
18+
19+
// It ignores propagation context of the root context
20+
expect(traceId).not.toBe('12345678901234567890123456789012');
21+
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();
22+
23+
// Different trace ID than the first span
24+
const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value;
25+
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
26+
27+
expect(trace1Id).not.toBe(traceId);
28+
},
29+
})
30+
.start()
31+
.completed();
32+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as Sentry from '@sentry/node-core';
2+
import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
3+
import { setupOtel } from '../../../../utils/setupOtel';
4+
5+
const client = Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
release: '1.0',
8+
tracesSampleRate: 1.0,
9+
traceLifecycle: 'stream',
10+
transport: loggingTransport,
11+
});
12+
13+
setupOtel(client);
14+
15+
Sentry.withScope(() => {
16+
const spanIdTraceId = Sentry.startSpan(
17+
{
18+
name: 'test_span_1',
19+
},
20+
span1 => span1.spanContext().traceId,
21+
);
22+
23+
Sentry.startSpan(
24+
{
25+
name: 'test_span_2',
26+
attributes: { spanIdTraceId },
27+
},
28+
() => undefined,
29+
);
30+
});
31+
32+
Sentry.flush();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { afterAll, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('sends manually started streamed parallel root spans outside of root context', async () => {
9+
expect.assertions(6);
10+
11+
await createRunner(__dirname, 'scenario.ts')
12+
.expect({ span: { items: [{ name: 'test_span_1' }] } })
13+
.expect({
14+
span: spanContainer => {
15+
expect(spanContainer).toBeDefined();
16+
const traceId = spanContainer.items[0]!.trace_id;
17+
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
18+
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();
19+
20+
const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value;
21+
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
22+
23+
// Different trace ID as the first span
24+
expect(trace1Id).not.toBe(traceId);
25+
},
26+
})
27+
.start()
28+
.completed();
29+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as Sentry from '@sentry/node-core';
2+
import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
3+
import { setupOtel } from '../../../../utils/setupOtel';
4+
5+
const client = Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
release: '1.0',
8+
tracesSampleRate: 1.0,
9+
traceLifecycle: 'stream',
10+
transport: loggingTransport,
11+
});
12+
13+
setupOtel(client);
14+
15+
Sentry.withScope(scope => {
16+
scope.setPropagationContext({
17+
parentSpanId: '1234567890123456',
18+
traceId: '12345678901234567890123456789012',
19+
sampleRand: Math.random(),
20+
});
21+
22+
const spanIdTraceId = Sentry.startSpan(
23+
{
24+
name: 'test_span_1',
25+
},
26+
span1 => span1.spanContext().traceId,
27+
);
28+
29+
Sentry.startSpan(
30+
{
31+
name: 'test_span_2',
32+
attributes: { spanIdTraceId },
33+
},
34+
() => undefined,
35+
);
36+
});
37+
38+
Sentry.flush();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { afterAll, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('sends manually started streamed parallel root spans outside of root context with parentSpanId', async () => {
9+
expect.assertions(6);
10+
11+
await createRunner(__dirname, 'scenario.ts')
12+
.expect({ span: { items: [{ name: 'test_span_1' }] } })
13+
.expect({
14+
span: spanContainer => {
15+
expect(spanContainer).toBeDefined();
16+
const traceId = spanContainer.items[0]!.trace_id;
17+
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
18+
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();
19+
20+
const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value;
21+
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
22+
23+
// Different trace ID as the first span
24+
expect(trace1Id).not.toBe(traceId);
25+
},
26+
})
27+
.start()
28+
.completed();
29+
});

0 commit comments

Comments
 (0)