Skip to content

Commit dd4766c

Browse files
authored
feat(browser): Add ingest_settings to span v2 envelope payload (#20411)
Adds `version: 2` and `ingest_settings` to the span streaming envelope payload so Relay can infer the end-user IP address and User-Agent from the incoming request ([link to spec](https://develop.sentry.dev/sdk/telemetry/spans/span-protocol/#version-and-ingest_settings-properties)). This is only emitted by the browser SDK. Both settings are currently gated behind `sendDefaultPii` (modeled after how `event.sdk.settings.infer_ip` works today). Closes #20275
1 parent f72f743 commit dd4766c

4 files changed

Lines changed: 93 additions & 5 deletions

File tree

dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ sentryTest(
5656
[
5757
{ content_type: 'application/vnd.sentry.items.span.v2+json', item_count: 4, type: 'span' },
5858
{
59+
version: 2,
60+
ingest_settings: { infer_ip: 'never', infer_user_agent: 'never' },
5961
items: expect.any(Array),
6062
},
6163
],

packages/core/src/tracing/spans/envelope.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { DynamicSamplingContext, SpanContainerItem, StreamedSpanEnvelope }
33
import type { SerializedStreamedSpan } from '../../types-hoist/span';
44
import { dsnToString } from '../../utils/dsn';
55
import { createEnvelope, getSdkMetadataForEnvelopeHeader } from '../../utils/envelope';
6+
import { isBrowser } from '../../utils/isBrowser';
67

78
/**
89
* Creates a span v2 span streaming envelope
@@ -12,9 +13,10 @@ export function createStreamedSpanEnvelope(
1213
dsc: Partial<DynamicSamplingContext>,
1314
client: Client,
1415
): StreamedSpanEnvelope {
16+
const options = client.getOptions();
1517
const dsn = client.getDsn();
16-
const tunnel = client.getOptions().tunnel;
17-
const sdk = getSdkMetadataForEnvelopeHeader(client.getOptions()._metadata);
18+
const tunnel = options.tunnel;
19+
const sdk = getSdkMetadataForEnvelopeHeader(options._metadata);
1820

1921
const headers: StreamedSpanEnvelope[0] = {
2022
sent_at: new Date().toISOString(),
@@ -23,9 +25,17 @@ export function createStreamedSpanEnvelope(
2325
...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
2426
};
2527

28+
const inferSetting = options.sendDefaultPii ? 'auto' : 'never';
29+
2630
const spanContainer: SpanContainerItem = [
2731
{ type: 'span', item_count: serializedSpans.length, content_type: 'application/vnd.sentry.items.span.v2+json' },
28-
{ items: serializedSpans },
32+
{
33+
version: 2,
34+
...(isBrowser() && {
35+
ingest_settings: { infer_ip: inferSetting, infer_user_agent: inferSetting },
36+
}),
37+
items: serializedSpans,
38+
},
2939
];
3040

3141
return createEnvelope<StreamedSpanEnvelope>(headers, [spanContainer]);

packages/core/src/types-hoist/span.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export type SerializedStreamedSpan = Omit<StreamedSpanJSON, 'attributes' | 'link
6969
* Envelope span item container.
7070
*/
7171
export type SerializedStreamedSpanContainer = {
72+
version?: number;
73+
ingest_settings?: {
74+
infer_ip?: 'auto' | 'never';
75+
infer_user_agent?: 'auto' | 'never';
76+
};
7277
items: Array<SerializedStreamedSpan>;
7378
};
7479

packages/core/test/lib/tracing/spans/envelope.test.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
22
import { createStreamedSpanEnvelope } from '../../../../src/tracing/spans/envelope';
33
import type { DynamicSamplingContext } from '../../../../src/types-hoist/envelope';
44
import type { SerializedStreamedSpan } from '../../../../src/types-hoist/span';
5+
import { isBrowser } from '../../../../src/utils/isBrowser';
56
import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client';
67

8+
vi.mock('../../../../src/utils/isBrowser', () => ({
9+
isBrowser: vi.fn(() => false),
10+
}));
11+
12+
afterEach(() => {
13+
vi.mocked(isBrowser).mockReturnValue(false);
14+
});
15+
716
function createMockSerializedSpan(overrides: Partial<SerializedStreamedSpan> = {}): SerializedStreamedSpan {
817
return {
918
trace_id: 'abc123',
@@ -181,6 +190,7 @@ describe('createStreamedSpanEnvelope', () => {
181190
type: 'span',
182191
},
183192
{
193+
version: 2,
184194
items: [mockSpan],
185195
},
186196
],
@@ -199,7 +209,7 @@ describe('createStreamedSpanEnvelope', () => {
199209
expect(envelopeItems).toEqual([
200210
[
201211
{ type: 'span', item_count: 3, content_type: 'application/vnd.sentry.items.span.v2+json' },
202-
{ items: [mockSpan1, mockSpan2, mockSpan3] },
212+
{ version: 2, items: [mockSpan1, mockSpan2, mockSpan3] },
203213
],
204214
]);
205215
});
@@ -222,11 +232,72 @@ describe('createStreamedSpanEnvelope', () => {
222232
type: 'span',
223233
},
224234
{
235+
version: 2,
225236
items: [],
226237
},
227238
],
228239
],
229240
]);
230241
});
242+
243+
it("includes ingest_settings with 'auto' values when in browser and sendDefaultPii is true", () => {
244+
vi.mocked(isBrowser).mockReturnValue(true);
245+
246+
const mockSpan = createMockSerializedSpan();
247+
const mockClient = new TestClient(getDefaultTestClientOptions({ sendDefaultPii: true }));
248+
const dsc: Partial<DynamicSamplingContext> = {};
249+
250+
const envelopeItems = createStreamedSpanEnvelope([mockSpan], dsc, mockClient)[1];
251+
252+
expect(envelopeItems).toEqual([
253+
[
254+
{ type: 'span', item_count: 1, content_type: 'application/vnd.sentry.items.span.v2+json' },
255+
{
256+
version: 2,
257+
ingest_settings: { infer_ip: 'auto', infer_user_agent: 'auto' },
258+
items: [mockSpan],
259+
},
260+
],
261+
]);
262+
});
263+
264+
it("includes ingest_settings with 'never' values when in browser and sendDefaultPii is false", () => {
265+
vi.mocked(isBrowser).mockReturnValue(true);
266+
267+
const mockSpan = createMockSerializedSpan();
268+
const mockClient = new TestClient(getDefaultTestClientOptions({ sendDefaultPii: false }));
269+
const dsc: Partial<DynamicSamplingContext> = {};
270+
271+
const envelopeItems = createStreamedSpanEnvelope([mockSpan], dsc, mockClient)[1];
272+
273+
expect(envelopeItems).toEqual([
274+
[
275+
{ type: 'span', item_count: 1, content_type: 'application/vnd.sentry.items.span.v2+json' },
276+
{
277+
version: 2,
278+
ingest_settings: { infer_ip: 'never', infer_user_agent: 'never' },
279+
items: [mockSpan],
280+
},
281+
],
282+
]);
283+
});
284+
285+
it('omits ingest_settings when not in browser', () => {
286+
const mockSpan = createMockSerializedSpan();
287+
const mockClient = new TestClient(getDefaultTestClientOptions({ sendDefaultPii: true }));
288+
const dsc: Partial<DynamicSamplingContext> = {};
289+
290+
const envelopeItems = createStreamedSpanEnvelope([mockSpan], dsc, mockClient)[1];
291+
292+
expect(envelopeItems).toEqual([
293+
[
294+
{ type: 'span', item_count: 1, content_type: 'application/vnd.sentry.items.span.v2+json' },
295+
{
296+
version: 2,
297+
items: [mockSpan],
298+
},
299+
],
300+
]);
301+
});
231302
});
232303
});

0 commit comments

Comments
 (0)