Skip to content

Commit 1456b9c

Browse files
authored
feat(tracing): Propagate environment and release values in baggage Http headers (#5193)
Add the sentry-environment and sentry-release values to the baggage HTTP headers of outgoing requests. We add these values to baggage as late as possible with the rationale that other baggage values (to be added in the future) will not be available right away, e.g. when intercepting baggage from incoming requests. As a result, the flow described below happens, when the first call to span.getBaggage is made (e.g. in callbacks of instrumented outgoing requests): 1. In span.getBaggage, check if there is baggage present in the span (in which case it was intercepted from incoming headers/meta tags) and it is empty (or only includes 3rd party data) OR if there is no baggage yet 2. In both of these cases, populate the baggage with Sentry data (and leave 3rd party content untouched). Else do nothing 3. Add this baggage to outgoing headers (and merge with possible 3rd party header) Additionally, add and improve tests to check correct handling and propagation of baggage
1 parent 5791c49 commit 1456b9c

File tree

11 files changed

+163
-32
lines changed

11 files changed

+163
-32
lines changed

packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as path from 'path';
33
import { getAPIResponse, runServer } from '../../../../utils/index';
44
import { TestAPIResponse } from '../server';
55

6-
test('Should assign `baggage` header which contains 3rd party trace baggage data of an outgoing request.', async () => {
6+
test('Should assign `baggage` header which contains 3rd party trace baggage data to an outgoing request.', async () => {
77
const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`);
88

99
const response = (await getAPIResponse(new URL(`${url}/express`), {
@@ -14,39 +14,39 @@ test('Should assign `baggage` header which contains 3rd party trace baggage data
1414
expect(response).toMatchObject({
1515
test_data: {
1616
host: 'somewhere.not.sentry',
17-
baggage: expect.stringContaining('foo=bar,bar=baz'),
17+
baggage: 'foo=bar,bar=baz,sentry-environment=prod,sentry-release=1.0',
1818
},
1919
});
2020
});
2121

22-
test('Should assign `baggage` header which contains sentry trace baggage data of an outgoing request.', async () => {
22+
test('Should not overwrite baggage if the incoming request already has Sentry baggage data.', async () => {
2323
const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`);
2424

2525
const response = (await getAPIResponse(new URL(`${url}/express`), {
26-
baggage: 'sentry-version=1.0.0,sentry-environment=production',
26+
baggage: 'sentry-version=2.0.0,sentry-environment=myEnv',
2727
})) as TestAPIResponse;
2828

2929
expect(response).toBeDefined();
3030
expect(response).toMatchObject({
3131
test_data: {
3232
host: 'somewhere.not.sentry',
33-
baggage: expect.stringContaining('sentry-version=1.0.0,sentry-environment=production'),
33+
baggage: 'sentry-version=2.0.0,sentry-environment=myEnv',
3434
},
3535
});
3636
});
3737

38-
test('Should assign `baggage` header which contains sentry and 3rd party trace baggage data of an outgoing request.', async () => {
38+
test('Should pass along sentry and 3rd party trace baggage data from an incoming to an outgoing request.', async () => {
3939
const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`);
4040

4141
const response = (await getAPIResponse(new URL(`${url}/express`), {
42-
baggage: 'sentry-version=1.0.0,sentry-environment=production,dogs=great',
42+
baggage: 'sentry-version=2.0.0,sentry-environment=myEnv,dogs=great',
4343
})) as TestAPIResponse;
4444

4545
expect(response).toBeDefined();
4646
expect(response).toMatchObject({
4747
test_data: {
4848
host: 'somewhere.not.sentry',
49-
baggage: expect.stringContaining('dogs=great,sentry-version=1.0.0,sentry-environment=production'),
49+
baggage: expect.stringContaining('dogs=great,sentry-version=2.0.0,sentry-environment=myEnv'),
5050
},
5151
});
5252
});

packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ test('should attach a `baggage` header to an outgoing request.', async () => {
1212
expect(response).toMatchObject({
1313
test_data: {
1414
host: 'somewhere.not.sentry',
15-
// TODO this is currently still empty but eventually it should contain sentry data
16-
baggage: expect.stringMatching(''),
15+
baggage: expect.stringMatching('sentry-environment=prod,sentry-release=1.0'),
1716
},
1817
});
1918
});

packages/node-integration-tests/suites/express/sentry-trace/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': strin
1111
Sentry.init({
1212
dsn: 'https://[email protected]/1337',
1313
release: '1.0',
14+
environment: 'prod',
1415
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
1516
tracesSampleRate: 1.0,
1617
});

packages/node/test/integrations/http.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ describe('tracing', () => {
2121
dsn: 'https://[email protected]/12312012',
2222
tracesSampleRate: 1.0,
2323
integrations: [new HttpIntegration({ tracing: true })],
24+
release: '1.0.0',
25+
environment: 'production',
2426
});
2527
const hub = new Hub(new NodeClient(options));
2628
addExtensionMethods();
@@ -40,7 +42,7 @@ describe('tracing', () => {
4042
nock('http://dogs.are.great').get('/').reply(200);
4143

4244
const transaction = createTransactionOnScope();
43-
const spans = (transaction as Span).spanRecorder?.spans as Span[];
45+
const spans = (transaction as unknown as Span).spanRecorder?.spans as Span[];
4446

4547
http.get('http://dogs.are.great/');
4648

@@ -55,7 +57,7 @@ describe('tracing', () => {
5557
nock('http://squirrelchasers.ingest.sentry.io').get('/api/12312012/store/').reply(200);
5658

5759
const transaction = createTransactionOnScope();
58-
const spans = (transaction as Span).spanRecorder?.spans as Span[];
60+
const spans = (transaction as unknown as Span).spanRecorder?.spans as Span[];
5961

6062
http.get('http://squirrelchasers.ingest.sentry.io/api/12312012/store/');
6163

@@ -96,8 +98,7 @@ describe('tracing', () => {
9698
const baggageHeader = request.getHeader('baggage') as string;
9799

98100
expect(baggageHeader).toBeDefined();
99-
// this might change once we actually add our baggage data to the header
100-
expect(baggageHeader).toEqual('');
101+
expect(baggageHeader).toEqual('sentry-environment=production,sentry-release=1.0.0');
101102
});
102103

103104
it('propagates 3rd party baggage header data to outgoing non-sentry requests', async () => {
@@ -109,8 +110,7 @@ describe('tracing', () => {
109110
const baggageHeader = request.getHeader('baggage') as string;
110111

111112
expect(baggageHeader).toBeDefined();
112-
// this might change once we actually add our baggage data to the header
113-
expect(baggageHeader).toEqual('dog=great');
113+
expect(baggageHeader).toEqual('dog=great,sentry-environment=production,sentry-release=1.0.0');
114114
});
115115

116116
it("doesn't attach the sentry-trace header to outgoing sentry requests", () => {

packages/tracing/src/browser/request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable max-lines */
2+
import type { Span } from '@sentry/types';
23
import {
34
addInstrumentationHandler,
45
BAGGAGE_HEADER_NAME,
@@ -7,7 +8,6 @@ import {
78
mergeAndSerializeBaggage,
89
} from '@sentry/utils';
910

10-
import { Span } from '../span';
1111
import { getActiveTransaction, hasTracingEnabled } from '../utils';
1212

1313
export const DEFAULT_TRACING_ORIGINS = ['localhost', /^\//];

packages/tracing/src/span.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
/* eslint-disable max-lines */
2-
import { Baggage, Primitive, Span as SpanInterface, SpanContext, Transaction } from '@sentry/types';
3-
import { dropUndefinedKeys, timestampWithMs, uuid4 } from '@sentry/utils';
2+
import { getCurrentHub } from '@sentry/hub';
3+
import { Baggage, Hub, Primitive, Span as SpanInterface, SpanContext, Transaction } from '@sentry/types';
4+
import {
5+
createBaggage,
6+
dropUndefinedKeys,
7+
isBaggageEmpty,
8+
isSentryBaggageEmpty,
9+
setBaggageValue,
10+
timestampWithMs,
11+
uuid4,
12+
} from '@sentry/utils';
413

514
/**
615
* Keeps track of finished spans for a given transaction
@@ -302,7 +311,14 @@ export class Span implements SpanInterface {
302311
* @inheritdoc
303312
*/
304313
public getBaggage(): Baggage | undefined {
305-
return this.transaction && this.transaction.metadata.baggage;
314+
const existingBaggage = this.transaction && this.transaction.metadata.baggage;
315+
316+
const finalBaggage =
317+
!existingBaggage || isSentryBaggageEmpty(existingBaggage)
318+
? this._getBaggageWithSentryValues(existingBaggage)
319+
: existingBaggage;
320+
321+
return isBaggageEmpty(finalBaggage) ? undefined : finalBaggage;
306322
}
307323

308324
/**
@@ -334,6 +350,24 @@ export class Span implements SpanInterface {
334350
trace_id: this.traceId,
335351
});
336352
}
353+
354+
/**
355+
*
356+
* @param baggage
357+
* @returns
358+
*/
359+
private _getBaggageWithSentryValues(baggage: Baggage = createBaggage({})): Baggage {
360+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
361+
const hub: Hub = ((this.transaction as any) && (this.transaction as any)._hub) || getCurrentHub();
362+
const client = hub.getClient();
363+
364+
const { environment, release } = (client && client.getOptions()) || {};
365+
366+
environment && setBaggageValue(baggage, 'environment', environment);
367+
release && setBaggageValue(baggage, 'release', release);
368+
369+
return baggage;
370+
}
337371
}
338372

339373
export type SpanStatusType =

packages/tracing/src/transaction.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ export class Transaction extends SpanClass implements TransactionInterface {
1616

1717
public metadata: TransactionMetadata;
1818

19-
private _measurements: Measurements = {};
20-
2119
/**
2220
* The reference to the current hub.
2321
*/
24-
private readonly _hub: Hub;
22+
protected readonly _hub: Hub;
23+
24+
private _measurements: Measurements = {};
2525

2626
private _trimEnd?: boolean;
2727

packages/tracing/test/browser/browsertracing.test.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BrowserClient } from '@sentry/browser';
22
import { Hub, makeMain } from '@sentry/hub';
3-
import { BaggageObj } from '@sentry/types';
3+
import type { BaggageObj, BaseTransportOptions, ClientOptions } from '@sentry/types';
44
import { getGlobalObject, InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils';
55
import { JSDOM } from 'jsdom';
66

@@ -415,7 +415,16 @@ describe('BrowserTracing', () => {
415415
});
416416
});
417417

418-
describe('using the data', () => {
418+
describe('using the <meta> tag data', () => {
419+
beforeEach(() => {
420+
hub.getClient()!.getOptions = () => {
421+
return {
422+
release: '1.0.0',
423+
environment: 'production',
424+
} as ClientOptions<BaseTransportOptions>;
425+
};
426+
});
427+
419428
it('uses the tracing data for pageload transactions', () => {
420429
// make sampled false here, so we can see that it's being used rather than the tracesSampleRate-dictated one
421430
document.head.innerHTML =
@@ -439,11 +448,34 @@ describe('BrowserTracing', () => {
439448
expect(baggage[1]).toEqual('foo=bar');
440449
});
441450

451+
it('adds Sentry baggage data to pageload transactions if not present in meta tags', () => {
452+
// make sampled false here, so we can see that it's being used rather than the tracesSampleRate-dictated one
453+
document.head.innerHTML =
454+
'<meta name="sentry-trace" content="12312012123120121231201212312012-1121201211212012-0">' +
455+
'<meta name="baggage" content="foo=bar">';
456+
457+
// pageload transactions are created as part of the BrowserTracing integration's initialization
458+
createBrowserTracing(true);
459+
const transaction = getActiveTransaction(hub) as IdleTransaction;
460+
const baggage = transaction.getBaggage()!;
461+
462+
expect(transaction).toBeDefined();
463+
expect(transaction.op).toBe('pageload');
464+
expect(transaction.traceId).toEqual('12312012123120121231201212312012');
465+
expect(transaction.parentSpanId).toEqual('1121201211212012');
466+
expect(transaction.sampled).toBe(false);
467+
expect(baggage).toBeDefined();
468+
expect(baggage[0]).toBeDefined();
469+
expect(baggage[0]).toEqual({ environment: 'production', release: '1.0.0' });
470+
expect(baggage[1]).toBeDefined();
471+
expect(baggage[1]).toEqual('foo=bar');
472+
});
473+
442474
it('ignores the data for navigation transactions', () => {
443475
mockChangeHistory = () => undefined;
444476
document.head.innerHTML =
445477
'<meta name="sentry-trace" content="12312012123120121231201212312012-1121201211212012-0">' +
446-
'<meta name="baggage" content="sentry-release=2.1.14,foo=bar">';
478+
'<meta name="baggage" content="sentry-release=2.1.14">';
447479

448480
createBrowserTracing(true);
449481

@@ -455,7 +487,11 @@ describe('BrowserTracing', () => {
455487
expect(transaction.op).toBe('navigation');
456488
expect(transaction.traceId).not.toEqual('12312012123120121231201212312012');
457489
expect(transaction.parentSpanId).toBeUndefined();
458-
expect(baggage).toBeUndefined();
490+
expect(baggage).toBeDefined();
491+
expect(baggage[0]).toBeDefined();
492+
expect(baggage[0]).toEqual({ release: '1.0.0', environment: 'production' });
493+
expect(baggage[1]).toBeDefined();
494+
expect(baggage[1]).toEqual('');
459495
});
460496
});
461497
});

packages/tracing/test/span.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { BrowserClient } from '@sentry/browser';
22
import { Hub, makeMain, Scope } from '@sentry/hub';
3+
import { BaseTransportOptions, ClientOptions } from '@sentry/types';
4+
import { createBaggage, getSentryBaggageItems, getThirdPartyBaggage, isSentryBaggageEmpty } from '@sentry/utils';
35

46
import { Span, Transaction } from '../src';
57
import { TRACEPARENT_REGEXP } from '../src/utils';
@@ -390,4 +392,57 @@ describe('Span', () => {
390392
expect(span.data).toStrictEqual({ data0: 'foo', data1: 'bar' });
391393
});
392394
});
395+
396+
describe('getBaggage and _getBaggageWithSentryValues', () => {
397+
beforeEach(() => {
398+
hub.getClient()!.getOptions = () => {
399+
return {
400+
release: '1.0.1',
401+
environment: 'production',
402+
} as ClientOptions<BaseTransportOptions>;
403+
};
404+
});
405+
406+
test('leave baggage content untouched and just return baggage if there already is Sentry content in it', () => {
407+
const transaction = new Transaction(
408+
{
409+
name: 'tx',
410+
metadata: { baggage: createBaggage({ environment: 'myEnv' }, '') },
411+
},
412+
hub,
413+
);
414+
415+
const hubSpy = jest.spyOn(hub.getClient()!, 'getOptions');
416+
417+
const span = transaction.startChild();
418+
419+
const baggage = span.getBaggage();
420+
421+
expect(hubSpy).toHaveBeenCalledTimes(0);
422+
expect(baggage && isSentryBaggageEmpty(baggage)).toBeFalsy();
423+
expect(baggage && getSentryBaggageItems(baggage)).toStrictEqual({ environment: 'myEnv' });
424+
expect(baggage && getThirdPartyBaggage(baggage)).toStrictEqual('');
425+
});
426+
427+
test('add Sentry baggage data to baggage if Sentry content is empty', () => {
428+
const transaction = new Transaction(
429+
{
430+
name: 'tx',
431+
metadata: { baggage: createBaggage({}, '') },
432+
},
433+
hub,
434+
);
435+
436+
const hubSpy = jest.spyOn(hub.getClient()!, 'getOptions');
437+
438+
const span = transaction.startChild();
439+
440+
const baggage = span.getBaggage();
441+
442+
expect(hubSpy).toHaveBeenCalledTimes(1);
443+
expect(baggage && isSentryBaggageEmpty(baggage)).toBeFalsy();
444+
expect(baggage && getSentryBaggageItems(baggage)).toStrictEqual({ release: '1.0.1', environment: 'production' });
445+
expect(baggage && getThirdPartyBaggage(baggage)).toStrictEqual('');
446+
});
447+
});
393448
});

packages/utils/src/baggage.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,17 @@ export function setBaggageValue(baggage: Baggage, key: keyof BaggageObj, value:
3030
baggage[0][key] = value;
3131
}
3232

33-
/** Check if the baggage object (i.e. the first element in the tuple) is empty */
34-
export function isBaggageEmpty(baggage: Baggage): boolean {
33+
/** Check if the Sentry part of the passed baggage (i.e. the first element in the tuple) is empty */
34+
export function isSentryBaggageEmpty(baggage: Baggage): boolean {
3535
return Object.keys(baggage[0]).length === 0;
3636
}
3737

38+
/** Check if the Sentry part of the passed baggage (i.e. the first element in the tuple) is empty */
39+
export function isBaggageEmpty(baggage: Baggage): boolean {
40+
const thirdPartyBaggage = getThirdPartyBaggage(baggage);
41+
return isSentryBaggageEmpty(baggage) && (thirdPartyBaggage == undefined || thirdPartyBaggage.length === 0);
42+
}
43+
3844
/** Returns Sentry specific baggage values */
3945
export function getSentryBaggageItems(baggage: Baggage): BaggageObj {
4046
return baggage[0];

packages/utils/test/baggage.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
createBaggage,
33
getBaggageValue,
4-
isBaggageEmpty,
4+
isSentryBaggageEmpty,
55
mergeAndSerializeBaggage,
66
parseBaggageString,
77
serializeBaggage,
@@ -98,12 +98,12 @@ describe('Baggage', () => {
9898
});
9999
});
100100

101-
describe('isBaggageEmpty', () => {
101+
describe('isSentryBaggageEmpty', () => {
102102
it.each([
103103
['returns true if the modifyable part of baggage is empty', createBaggage({}), true],
104104
['returns false if the modifyable part of baggage is not empty', createBaggage({ release: '10.0.2' }), false],
105105
])('%s', (_: string, baggage, outcome) => {
106-
expect(isBaggageEmpty(baggage)).toEqual(outcome);
106+
expect(isSentryBaggageEmpty(baggage)).toEqual(outcome);
107107
});
108108
});
109109

0 commit comments

Comments
 (0)