Skip to content

Commit e747276

Browse files
committed
feat(core): Add Spanv2 envelope creation function
1 parent 8681fba commit e747276

4 files changed

Lines changed: 270 additions & 2 deletions

File tree

packages/core/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,6 @@ export type {
453453
SpanContextData,
454454
TraceFlag,
455455
StreamedSpanJSON,
456-
SerializedSpanContainer,
457-
SerializedSpan,
458456
} from './types-hoist/span';
459457
export type { SpanStatus } from './types-hoist/spanStatus';
460458
export type { Log, LogSeverityLevel } from './types-hoist/log';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
For now, all span streaming related tracing code is in this sub directory.
2+
Once we get rid of transaction-based tracing, we can clean up and flatten the entire tracing directory.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Client } from '../../client';
2+
import type { DynamicSamplingContext, SpanContainerItem, SpanV2Envelope } from '../../types-hoist/envelope';
3+
import type { SerializedSpan } from '../../types-hoist/span';
4+
import { dsnToString } from '../../utils/dsn';
5+
import { createEnvelope } from '../../utils/envelope';
6+
7+
/**
8+
* Creates a span v2 span streaming envelope
9+
*/
10+
export function createSpanV2Envelope(
11+
serializedSpans: Array<SerializedSpan>,
12+
dsc: Partial<DynamicSamplingContext>,
13+
client: Client,
14+
): SpanV2Envelope {
15+
const dsn = client.getDsn();
16+
const tunnel = client.getOptions().tunnel;
17+
const sdk = client.getOptions()._metadata?.sdk;
18+
19+
const headers: SpanV2Envelope[0] = {
20+
sent_at: new Date().toISOString(),
21+
...(dscHasRequiredProps(dsc) && { trace: dsc }),
22+
...(sdk && { sdk: sdk }),
23+
...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
24+
};
25+
26+
const spanContainer: SpanContainerItem = [
27+
{ type: 'span', item_count: serializedSpans.length, content_type: 'application/vnd.sentry.items.span.v2+json' },
28+
{ items: serializedSpans },
29+
];
30+
31+
return createEnvelope<SpanV2Envelope>(headers, [spanContainer]);
32+
}
33+
34+
function dscHasRequiredProps(dsc: Partial<DynamicSamplingContext>): dsc is DynamicSamplingContext {
35+
return !!dsc.trace_id && !!dsc.public_key;
36+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { createSpanV2Envelope } from '../../../../src/tracing/spans/envelope';
3+
import type { DynamicSamplingContext } from '../../../../src/types-hoist/envelope';
4+
import type { SerializedSpan } from '../../../../src/types-hoist/span';
5+
import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client';
6+
7+
function createMockSerializedSpan(overrides: Partial<SerializedSpan> = {}): SerializedSpan {
8+
return {
9+
trace_id: 'abc123',
10+
span_id: 'def456',
11+
name: 'test-span',
12+
start_timestamp: 1713859200,
13+
end_timestamp: 1713859201,
14+
status: 'ok',
15+
is_segment: false,
16+
...overrides,
17+
};
18+
}
19+
20+
describe('createSpanV2Envelope', () => {
21+
describe('envelope headers', () => {
22+
it('creates an envelope with sent_at header', () => {
23+
const mockSpan = createMockSerializedSpan();
24+
const mockClient = new TestClient(getDefaultTestClientOptions());
25+
const dsc: Partial<DynamicSamplingContext> = {};
26+
27+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
28+
29+
expect(result[0]).toHaveProperty('sent_at', expect.any(String));
30+
});
31+
32+
it('includes trace header when DSC has required props (trace_id and public_key)', () => {
33+
const mockSpan = createMockSerializedSpan();
34+
const mockClient = new TestClient(getDefaultTestClientOptions());
35+
const dsc: DynamicSamplingContext = {
36+
trace_id: 'trace-123',
37+
public_key: 'public-key-abc',
38+
sample_rate: '1.0',
39+
release: 'v1.0.0',
40+
};
41+
42+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
43+
44+
expect(result[0]).toHaveProperty('trace', dsc);
45+
});
46+
47+
it("does't include trace header when DSC is missing trace_id", () => {
48+
const mockSpan = createMockSerializedSpan();
49+
const mockClient = new TestClient(getDefaultTestClientOptions());
50+
const dsc: Partial<DynamicSamplingContext> = {
51+
public_key: 'public-key-abc',
52+
};
53+
54+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
55+
56+
expect(result[0]).not.toHaveProperty('trace');
57+
});
58+
59+
it("does't include trace header when DSC is missing public_key", () => {
60+
const mockSpan = createMockSerializedSpan();
61+
const mockClient = new TestClient(getDefaultTestClientOptions());
62+
const dsc: Partial<DynamicSamplingContext> = {
63+
trace_id: 'trace-123',
64+
};
65+
66+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
67+
68+
expect(result[0]).not.toHaveProperty('trace');
69+
});
70+
71+
it('includes SDK info when available in client options', () => {
72+
const mockSpan = createMockSerializedSpan();
73+
const mockClient = new TestClient(
74+
getDefaultTestClientOptions({
75+
_metadata: {
76+
sdk: { name: 'sentry.javascript.browser', version: '8.0.0' },
77+
},
78+
}),
79+
);
80+
const dsc: Partial<DynamicSamplingContext> = {};
81+
82+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
83+
84+
expect(result[0]).toHaveProperty('sdk', { name: 'sentry.javascript.browser', version: '8.0.0' });
85+
});
86+
87+
it("does't include SDK info when not available", () => {
88+
const mockSpan = createMockSerializedSpan();
89+
const mockClient = new TestClient(getDefaultTestClientOptions());
90+
const dsc: Partial<DynamicSamplingContext> = {};
91+
92+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
93+
94+
expect(result[0]).not.toHaveProperty('sdk');
95+
});
96+
97+
it('includes DSN when tunnel and DSN are configured', () => {
98+
const mockSpan = createMockSerializedSpan();
99+
const mockClient = new TestClient(
100+
getDefaultTestClientOptions({
101+
dsn: 'https://abc123@example.sentry.io/456',
102+
tunnel: 'https://tunnel.example.com',
103+
}),
104+
);
105+
const dsc: Partial<DynamicSamplingContext> = {};
106+
107+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
108+
109+
expect(result[0]).toHaveProperty('dsn', 'https://abc123@example.sentry.io/456');
110+
});
111+
112+
it("does't include DSN when tunnel is not configured", () => {
113+
const mockSpan = createMockSerializedSpan();
114+
const mockClient = new TestClient(
115+
getDefaultTestClientOptions({
116+
dsn: 'https://abc123@example.sentry.io/456',
117+
}),
118+
);
119+
const dsc: Partial<DynamicSamplingContext> = {};
120+
121+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
122+
123+
expect(result[0]).not.toHaveProperty('dsn');
124+
});
125+
126+
it("does't include DSN when DSN is not available", () => {
127+
const mockSpan = createMockSerializedSpan();
128+
const mockClient = new TestClient(
129+
getDefaultTestClientOptions({
130+
tunnel: 'https://tunnel.example.com',
131+
}),
132+
);
133+
const dsc: Partial<DynamicSamplingContext> = {};
134+
135+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
136+
137+
expect(result[0]).not.toHaveProperty('dsn');
138+
});
139+
140+
it('includes all headers when all options are provided', () => {
141+
const mockSpan = createMockSerializedSpan();
142+
const mockClient = new TestClient(
143+
getDefaultTestClientOptions({
144+
dsn: 'https://abc123@example.sentry.io/456',
145+
tunnel: 'https://tunnel.example.com',
146+
_metadata: {
147+
sdk: { name: 'sentry.javascript.node', version: '10.38.0' },
148+
},
149+
}),
150+
);
151+
const dsc: DynamicSamplingContext = {
152+
trace_id: 'trace-123',
153+
public_key: 'public-key-abc',
154+
environment: 'production',
155+
};
156+
157+
const result = createSpanV2Envelope([mockSpan], dsc, mockClient);
158+
159+
expect(result[0]).toEqual({
160+
sent_at: expect.any(String),
161+
trace: dsc,
162+
sdk: { name: 'sentry.javascript.node', version: '10.38.0' },
163+
dsn: 'https://abc123@example.sentry.io/456',
164+
});
165+
});
166+
});
167+
168+
describe('envelope item', () => {
169+
it('creates a span container item with correct structure', () => {
170+
const mockSpan = createMockSerializedSpan({ name: 'span-1' });
171+
const mockClient = new TestClient(getDefaultTestClientOptions());
172+
const dsc: Partial<DynamicSamplingContext> = {};
173+
174+
const envelopeItems = createSpanV2Envelope([mockSpan], dsc, mockClient)[1];
175+
176+
expect(envelopeItems).toEqual([
177+
[
178+
{
179+
content_type: 'application/vnd.sentry.items.span.v2+json',
180+
item_count: 1,
181+
type: 'span',
182+
},
183+
{
184+
items: [mockSpan],
185+
},
186+
],
187+
]);
188+
});
189+
190+
it('sets correct item_count for multiple spans', () => {
191+
const mockSpan1 = createMockSerializedSpan({ span_id: 'span-1' });
192+
const mockSpan2 = createMockSerializedSpan({ span_id: 'span-2' });
193+
const mockSpan3 = createMockSerializedSpan({ span_id: 'span-3' });
194+
const mockClient = new TestClient(getDefaultTestClientOptions());
195+
const dsc: Partial<DynamicSamplingContext> = {};
196+
197+
const envelopeItems = createSpanV2Envelope([mockSpan1, mockSpan2, mockSpan3], dsc, mockClient)[1];
198+
199+
expect(envelopeItems).toEqual([
200+
[
201+
{ type: 'span', item_count: 3, content_type: 'application/vnd.sentry.items.span.v2+json' },
202+
{ items: [mockSpan1, mockSpan2, mockSpan3] },
203+
],
204+
]);
205+
});
206+
207+
it('handles empty spans array', () => {
208+
const mockClient = new TestClient(getDefaultTestClientOptions());
209+
const dsc: Partial<DynamicSamplingContext> = {};
210+
211+
const result = createSpanV2Envelope([], dsc, mockClient);
212+
213+
expect(result).toEqual([
214+
{
215+
sent_at: expect.any(String),
216+
},
217+
[
218+
[
219+
{
220+
content_type: 'application/vnd.sentry.items.span.v2+json',
221+
item_count: 0,
222+
type: 'span',
223+
},
224+
{
225+
items: [],
226+
},
227+
],
228+
],
229+
]);
230+
});
231+
});
232+
});

0 commit comments

Comments
 (0)