Skip to content

Commit 18c2393

Browse files
committed
add tracestate property to Transaction class
1 parent 6124bdb commit 18c2393

File tree

4 files changed

+127
-3
lines changed

4 files changed

+127
-3
lines changed

packages/tracing/src/transaction.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Event, Measurements, Transaction as TransactionInterface, TransactionCo
33
import { dropUndefinedKeys, isInstanceOf, logger } from '@sentry/utils';
44

55
import { Span as SpanClass, SpanRecorder } from './span';
6+
import { computeTracestateValue } from './utils';
67

78
interface TransactionMetadata {
89
transactionSampling?: { [key: string]: string | number };
@@ -12,6 +13,8 @@ interface TransactionMetadata {
1213
export class Transaction extends SpanClass implements TransactionInterface {
1314
public name: string;
1415

16+
public readonly tracestate: string;
17+
1518
private _metadata: TransactionMetadata = {};
1619

1720
private _measurements: Measurements = {};
@@ -41,6 +44,10 @@ export class Transaction extends SpanClass implements TransactionInterface {
4144

4245
this._trimEnd = transactionContext.trimEnd;
4346

47+
// _getNewTracestate only returns undefined in the absence of a client or dsn, in which case it doesn't matter what
48+
// the header values are - nothing can be sent anyway - so the third alternative here is just to make TS happy
49+
this.tracestate = transactionContext.tracestate || this._getNewTracestate() || 'things are broken';
50+
4451
// this is because transactions are also spans, and spans have a transaction pointer
4552
this.transaction = this;
4653
}
@@ -161,4 +168,27 @@ export class Transaction extends SpanClass implements TransactionInterface {
161168

162169
return this;
163170
}
171+
172+
/**
173+
* Create a new tracestate header value
174+
*
175+
* @returns The new tracestate value, or undefined if there's no client or no dsn
176+
*/
177+
private _getNewTracestate(): string | undefined {
178+
const client = this._hub.getClient();
179+
const dsn = client?.getDsn();
180+
181+
if (!client || !dsn) {
182+
return;
183+
}
184+
185+
const { environment, release } = client.getOptions() || {};
186+
187+
// TODO - the only reason we need the non-null assertion on `dsn.publicKey` (below) is because `dsn.publicKey` has
188+
// to be optional while we transition from `dsn.user` -> `dsn.publicKey`. Once `dsn.user` is removed, we can make
189+
// `dsn.publicKey` required and remove the `!`.
190+
191+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
192+
return computeTracestateValue({ trace_id: this.traceId, environment, release, public_key: dsn.publicKey! });
193+
}
164194
}

packages/tracing/src/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getCurrentHub, Hub } from '@sentry/hub';
22
import { Options, TraceparentData, Transaction } from '@sentry/types';
3+
import { SentryError, unicodeToBase64 } from '@sentry/utils';
34

45
export const TRACEPARENT_REGEXP = new RegExp(
56
'^[ \\t]*' + // whitespace
@@ -66,3 +67,35 @@ export function secToMs(time: number): number {
6667

6768
// so it can be used in manual instrumentation without necessitating a hard dependency on @sentry/utils
6869
export { stripUrlQueryAndFragment } from '@sentry/utils';
70+
71+
/**
72+
* Compute the value of a tracestate header.
73+
*
74+
* @throws SentryError (because using the logger creates a circular dependency)
75+
* @returns the base64-encoded header value
76+
*/
77+
// Note: this is here instead of in the tracing package since @sentry/core tests rely on it
78+
export function computeTracestateValue(tracestateData: {
79+
trace_id: string;
80+
environment: string | undefined | null;
81+
release: string | undefined | null;
82+
public_key: string;
83+
}): string {
84+
// `JSON.stringify` will drop keys with undefined values, but not ones with null values
85+
tracestateData.environment = tracestateData.environment || null;
86+
tracestateData.release = tracestateData.release || null;
87+
88+
// See https://www.w3.org/TR/trace-context/#tracestate-header-field-values
89+
// The spec for tracestate header values calls for a string of the form
90+
//
91+
// identifier1=value1,identifier2=value2,...
92+
//
93+
// which means the value can't include any equals signs, since they already have meaning. Equals signs are commonly
94+
// used to pad the end of base64 values though, so to avoid confusion, we strip them off. (Most languages' base64
95+
// decoding functions (including those in JS) are able to function without the padding.)
96+
try {
97+
return unicodeToBase64(JSON.stringify(tracestateData)).replace(/={1,2}$/, '');
98+
} catch (err) {
99+
throw new SentryError(`[Tracing] Error creating tracestate header: ${err}`);
100+
}
101+
}

packages/tracing/test/hub.test.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/unbound-method */
2-
import { BrowserClient } from '@sentry/browser';
3-
import { Hub } from '@sentry/hub';
2+
import { BrowserClient, init as initSDK } from '@sentry/browser';
3+
import { getCurrentHub, Hub } from '@sentry/hub';
44
import * as hubModule from '@sentry/hub';
55
import { TransactionSamplingMethod } from '@sentry/types';
66
import * as utilsModule from '@sentry/utils'; // for mocking
@@ -9,7 +9,7 @@ import { logger } from '@sentry/utils';
99
import { BrowserTracing } from '../src/browser/browsertracing';
1010
import { addExtensionMethods } from '../src/hubextensions';
1111
import { Transaction } from '../src/transaction';
12-
import { extractTraceparentData, TRACEPARENT_REGEXP } from '../src/utils';
12+
import { computeTracestateValue, extractTraceparentData, TRACEPARENT_REGEXP } from '../src/utils';
1313
import { addDOMPropertiesToGlobal, getSymbolObjectKeyByName, testOnlyIfNodeVersionAtLeast } from './testutils';
1414

1515
addExtensionMethods();
@@ -29,6 +29,61 @@ describe('Hub', () => {
2929
jest.clearAllMocks();
3030
});
3131

32+
describe('transaction creation', () => {
33+
it('uses inherited values when given in transaction context', () => {
34+
const transactionContext = {
35+
name: 'dogpark',
36+
traceId: '12312012123120121231201212312012',
37+
parentSpanId: '1121201211212012',
38+
tracestate: 'doGsaREgReaT',
39+
};
40+
const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }));
41+
const transaction = hub.startTransaction(transactionContext);
42+
43+
expect(transaction).toEqual(expect.objectContaining(transactionContext));
44+
});
45+
46+
it('creates a new tracestate value if not given one in transaction context', () => {
47+
const environment = 'dogpark';
48+
const release = 'off.leash.park';
49+
const hub = new Hub(
50+
new BrowserClient({
51+
dsn: 'https://[email protected]/12312012',
52+
release,
53+
environment,
54+
}),
55+
);
56+
const transaction = hub.startTransaction({ name: 'FETCH /ball' });
57+
58+
const b64Value = computeTracestateValue({
59+
trace_id: transaction.traceId,
60+
environment,
61+
release,
62+
public_key: 'dogsarebadatkeepingsecrets',
63+
});
64+
65+
expect(transaction.tracestate).toEqual(b64Value);
66+
});
67+
68+
it('uses default environment if none given', () => {
69+
const release = 'off.leash.park';
70+
initSDK({
71+
dsn: 'https://[email protected]/12312012',
72+
release,
73+
});
74+
const transaction = getCurrentHub().startTransaction({ name: 'FETCH /ball' });
75+
76+
const b64Value = computeTracestateValue({
77+
trace_id: transaction.traceId,
78+
environment: 'production',
79+
release,
80+
public_key: 'dogsarebadatkeepingsecrets',
81+
});
82+
83+
expect(transaction.tracestate).toEqual(b64Value);
84+
});
85+
});
86+
3287
describe('getTransaction()', () => {
3388
it('should find a transaction which has been set on the scope if sampled = true', () => {
3489
const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }));

packages/types/src/transaction.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export interface TransactionContext extends SpanContext {
2222
* If this transaction has a parent, the parent's sampling decision
2323
*/
2424
parentSampled?: boolean;
25+
26+
/**
27+
* The tracestate header value associated with this transaction, potentially inherited from a parent transaction,
28+
* which will be propagated across services to all child transactions
29+
*/
30+
tracestate?: string;
2531
}
2632

2733
/**

0 commit comments

Comments
 (0)