Skip to content

Commit 77ed26e

Browse files
Lms24AbhiPrasad
authored andcommitted
feat(core): Send Baggage in Envelope Header (#5104)
This patch Introduces the first step of getting Dynamic Sampling working with the JS SDKs. It adds a very simple approach of attaching baggage to the envelope header of a transaction's event envelope. It does not take immutability of baggage in multiple transactions per trace scenarios into account. This is something we have to address when we're going to add the baggage header to outgoing requests. However, the simple approach should be enough to send baggage information to relay to get DS working on individual transactions.
1 parent 06ef836 commit 77ed26e

File tree

11 files changed

+170
-15
lines changed

11 files changed

+170
-15
lines changed

packages/core/src/baseclient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
618618
throw new SentryError('`beforeSend` returned `null`, will not send event.');
619619
}
620620

621-
const session = scope && scope.getSession && scope.getSession();
621+
const session = scope && scope.getSession();
622622
if (!isTransaction && session) {
623623
this._updateSessionFromEvent(session, processedEvent);
624624
}

packages/core/src/envelope.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
DsnComponents,
33
Event,
44
EventEnvelope,
5+
EventEnvelopeHeaders,
56
EventItem,
67
SdkInfo,
78
SdkMetadata,
@@ -10,7 +11,15 @@ import {
1011
SessionEnvelope,
1112
SessionItem,
1213
} from '@sentry/types';
13-
import { createEnvelope, dsnToString } from '@sentry/utils';
14+
import {
15+
BaggageObj,
16+
createBaggage,
17+
createEnvelope,
18+
dropUndefinedKeys,
19+
dsnToString,
20+
isBaggageEmpty,
21+
serializeBaggage,
22+
} from '@sentry/utils';
1423

1524
/** Extract sdk info from from the API metadata */
1625
function getSdkMetadataForEnvelopeHeader(metadata?: SdkMetadata): SdkInfo | undefined {
@@ -101,12 +110,8 @@ export function createEventEnvelope(
101110
// TODO: This is NOT part of the hack - DO NOT DELETE
102111
delete event.sdkProcessingMetadata;
103112

104-
const envelopeHeaders = {
105-
event_id: event.event_id as string,
106-
sent_at: new Date().toISOString(),
107-
...(sdkInfo && { sdk: sdkInfo }),
108-
...(!!tunnel && { dsn: dsnToString(dsn) }),
109-
};
113+
const envelopeHeaders = createEventEnvelopeHeaders(event, sdkInfo, tunnel, dsn);
114+
110115
const eventItem: EventItem = [
111116
{
112117
type: eventType,
@@ -116,3 +121,31 @@ export function createEventEnvelope(
116121
];
117122
return createEnvelope<EventEnvelope>(envelopeHeaders, [eventItem]);
118123
}
124+
125+
function createEventEnvelopeHeaders(
126+
event: Event,
127+
sdkInfo: SdkInfo | undefined,
128+
tunnel: string | undefined,
129+
dsn: DsnComponents,
130+
): EventEnvelopeHeaders {
131+
const baggage =
132+
event.type === 'transaction' &&
133+
createBaggage(
134+
dropUndefinedKeys({
135+
environment: event.environment,
136+
release: event.release,
137+
transaction: event.transaction,
138+
userid: event.user && event.user.id,
139+
// user.segment currently doesn't exist explicitly in interface User (just as a record key)
140+
usersegment: event.user && event.user.segment,
141+
} as BaggageObj),
142+
);
143+
144+
return {
145+
event_id: event.event_id as string,
146+
sent_at: new Date().toISOString(),
147+
...(sdkInfo && { sdk: sdkInfo }),
148+
...(!!tunnel && { dsn: dsnToString(dsn) }),
149+
...(baggage && !isBaggageEmpty(baggage) && { baggage: serializeBaggage(baggage) }),
150+
};
151+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { DsnComponents, Event } from '@sentry/types';
2+
3+
import { createEventEnvelope } from '../../src/envelope';
4+
5+
const testDsn: DsnComponents = { protocol: 'https', projectId: 'abc', host: 'testry.io' };
6+
7+
describe('createEventEnvelope', () => {
8+
describe('baggage header', () => {
9+
it("doesn't add baggage header if event is not a transaction", () => {
10+
const event: Event = {};
11+
const envelopeHeaders = createEventEnvelope(event, testDsn)[0];
12+
13+
expect(envelopeHeaders).toBeDefined();
14+
expect(envelopeHeaders.baggage).toBeUndefined();
15+
});
16+
17+
it("doesn't add baggage header if no baggage data is available", () => {
18+
const event: Event = {
19+
type: 'transaction',
20+
};
21+
const envelopeHeaders = createEventEnvelope(event, testDsn)[0];
22+
23+
expect(envelopeHeaders).toBeDefined();
24+
expect(envelopeHeaders.baggage).toBeUndefined();
25+
});
26+
27+
const testTable: Array<[string, Event, string]> = [
28+
['adds only baggage item', { type: 'transaction', release: '1.0.0' }, 'sentry-release=1.0.0'],
29+
[
30+
'adds two baggage items',
31+
{ type: 'transaction', release: '1.0.0', environment: 'prod' },
32+
'sentry-environment=prod,sentry-release=1.0.0',
33+
],
34+
[
35+
'adds all baggageitems',
36+
{
37+
type: 'transaction',
38+
release: '1.0.0',
39+
environment: 'prod',
40+
user: { id: 'bob', segment: 'segmentA' },
41+
transaction: 'TX',
42+
},
43+
'sentry-environment=prod,sentry-release=1.0.0,sentry-transaction=TX,sentry-userid=bob,sentry-usersegment=segmentA',
44+
],
45+
];
46+
it.each(testTable)('%s', (_: string, event, serializedBaggage) => {
47+
const envelopeHeaders = createEventEnvelope(event, testDsn)[0];
48+
49+
expect(envelopeHeaders).toBeDefined();
50+
expect(envelopeHeaders.baggage).toBeDefined();
51+
expect(envelopeHeaders.baggage).toEqual(serializedBaggage);
52+
});
53+
});
54+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { Integrations } from '@sentry/tracing';
3+
4+
window.Sentry = Sentry;
5+
6+
Sentry.init({
7+
dsn: 'https://[email protected]/1337',
8+
integrations: [new Integrations.BrowserTracing({ tracingOrigins: [/.*/] })],
9+
environment: 'production',
10+
tracesSampleRate: 1,
11+
});
12+
13+
Sentry.configureScope(scope => {
14+
scope.setUser({ id: 'user123', segment: 'segmentB' });
15+
scope.setTransactionName('testTransactionBaggage');
16+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expect } from '@playwright/test';
2+
import { Event, EventEnvelopeHeaders } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../utils/fixtures';
5+
import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
6+
7+
sentryTest('should send baggage data in transaction envelope header', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const envHeader = await getFirstSentryEnvelopeRequest<EventEnvelopeHeaders>(page, url, envelopeHeaderRequestParser);
11+
12+
expect(envHeader.baggage).toBeDefined();
13+
expect(envHeader.baggage).toEqual(
14+
'sentry-environment=production,sentry-transaction=testTransactionBaggage,sentry-userid=user123,sentry-usersegment=segmentB',
15+
);
16+
});

packages/integration-tests/utils/helpers.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Page, Request } from '@playwright/test';
2-
import { Event } from '@sentry/types';
2+
import { Event, EventEnvelopeHeaders } from '@sentry/types';
33

44
const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
55

@@ -11,6 +11,14 @@ const envelopeRequestParser = (request: Request | null): Event => {
1111
return envelope.split('\n').map(line => JSON.parse(line))[2];
1212
};
1313

14+
export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => {
15+
// https://develop.sentry.dev/sdk/envelopes/
16+
const envelope = request?.postData() || '';
17+
18+
// First row of the envelop is the event payload.
19+
return envelope.split('\n').map(line => JSON.parse(line))[0];
20+
};
21+
1422
/**
1523
* Run script at the given path inside the test environment.
1624
*
@@ -122,10 +130,11 @@ async function getMultipleSentryEnvelopeRequests<T>(
122130
url?: string;
123131
timeout?: number;
124132
},
133+
requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T,
125134
): Promise<T[]> {
126135
// TODO: This is not currently checking the type of envelope, just casting for now.
127136
// We can update this to include optional type-guarding when we have types for Envelope.
128-
return getMultipleRequests(page, count, envelopeUrlRegex, envelopeRequestParser, options) as Promise<T[]>;
137+
return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise<T[]>;
129138
}
130139

131140
/**
@@ -136,8 +145,12 @@ async function getMultipleSentryEnvelopeRequests<T>(
136145
* @param {string} [url]
137146
* @return {*} {Promise<T>}
138147
*/
139-
async function getFirstSentryEnvelopeRequest<T>(page: Page, url?: string): Promise<T> {
140-
return (await getMultipleSentryEnvelopeRequests<T>(page, 1, { url }))[0];
148+
async function getFirstSentryEnvelopeRequest<T>(
149+
page: Page,
150+
url?: string,
151+
requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T,
152+
): Promise<T> {
153+
return (await getMultipleSentryEnvelopeRequests<T>(page, 1, { url }, requestParser))[0];
141154
}
142155

143156
/**

packages/types/src/envelope.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export type SessionItem =
5656
| BaseEnvelopeItem<SessionAggregatesItemHeaders, SessionAggregates>;
5757
export type ClientReportItem = BaseEnvelopeItem<ClientReportItemHeaders, ClientReport>;
5858

59-
type EventEnvelopeHeaders = { event_id: string; sent_at: string };
59+
export type EventEnvelopeHeaders = { event_id: string; sent_at: string; baggage?: string };
6060
type SessionEnvelopeHeaders = { sent_at: string };
6161
type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders;
6262

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type {
1515
EnvelopeItemType,
1616
EnvelopeItem,
1717
EventEnvelope,
18+
EventEnvelopeHeaders,
1819
EventItem,
1920
SessionEnvelope,
2021
SessionItem,

packages/utils/src/baggage.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IS_DEBUG_BUILD } from './flags';
22
import { logger } from './logger';
33

4-
export type AllowedBaggageKeys = 'environment' | 'release'; // TODO: Add remaining allowed baggage keys | 'transaction' | 'userid' | 'usersegment';
4+
export type AllowedBaggageKeys = 'environment' | 'release' | 'userid' | 'transaction' | 'usersegment';
55
export type BaggageObj = Partial<Record<AllowedBaggageKeys, string> & Record<string, string>>;
66

77
/**
@@ -47,6 +47,11 @@ export function setBaggageValue(baggage: Baggage, key: keyof BaggageObj, value:
4747
baggage[0][key] = value;
4848
}
4949

50+
/** Check if the baggage object (i.e. the first element in the tuple) is empty */
51+
export function isBaggageEmpty(baggage: Baggage): boolean {
52+
return Object.keys(baggage[0]).length === 0;
53+
}
54+
5055
/** Serialize a baggage object */
5156
export function serializeBaggage(baggage: Baggage): string {
5257
return Object.keys(baggage[0]).reduce((prev, key: keyof BaggageObj) => {

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ export * from './env';
2323
export * from './envelope';
2424
export * from './clientreport';
2525
export * from './ratelimit';
26+
export * from './baggage';

packages/utils/test/baggage.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { createBaggage, getBaggageValue, parseBaggageString, serializeBaggage, setBaggageValue } from '../src/baggage';
1+
import {
2+
createBaggage,
3+
getBaggageValue,
4+
isBaggageEmpty,
5+
parseBaggageString,
6+
serializeBaggage,
7+
setBaggageValue,
8+
} from '../src/baggage';
29

310
describe('Baggage', () => {
411
describe('createBaggage', () => {
@@ -89,4 +96,13 @@ describe('Baggage', () => {
8996
expect(parseBaggageString(baggageString)).toEqual(baggage);
9097
});
9198
});
99+
100+
describe('isBaggageEmpty', () => {
101+
it.each([
102+
['returns true if the modifyable part of baggage is empty', createBaggage({}), true],
103+
['returns false if the modifyable part of baggage is not empty', createBaggage({ release: '10.0.2' }), false],
104+
])('%s', (_: string, baggage, outcome) => {
105+
expect(isBaggageEmpty(baggage)).toEqual(outcome);
106+
});
107+
});
92108
});

0 commit comments

Comments
 (0)