Skip to content

Commit 7710945

Browse files
authored
feat(tracing): Send sample rate and type in transaction item header in envelope (#3068)
MVP of this feature for now, as it's only being used internally at the moment.
1 parent 928e96a commit 7710945

File tree

6 files changed

+136
-21
lines changed

6 files changed

+136
-21
lines changed

packages/core/src/request.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export function sessionToSentryRequest(session: Session, api: API): SentryReques
2020

2121
/** Creates a SentryRequest from an event. */
2222
export function eventToSentryRequest(event: Event, api: API): SentryRequest {
23+
// since JS has no Object.prototype.pop()
24+
const { __sentry_samplingMethod: samplingMethod, __sentry_sampleRate: sampleRate, ...otherTags } = event.tags || {};
25+
event.tags = otherTags;
26+
2327
const useEnvelope = event.type === 'transaction';
2428

2529
const req: SentryRequest = {
@@ -41,6 +45,11 @@ export function eventToSentryRequest(event: Event, api: API): SentryRequest {
4145
});
4246
const itemHeaders = JSON.stringify({
4347
type: event.type,
48+
49+
// TODO: Right now, sampleRate may or may not be defined (it won't be in the cases of inheritance and
50+
// explicitly-set sampling decisions). Are we good with that?
51+
sample_rates: [{ id: samplingMethod, rate: sampleRate }],
52+
4453
// The content-type is assumed to be 'application/json' and not part of
4554
// the current spec for transaction items, so we don't bloat the request
4655
// body with it.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Event, TransactionSamplingMethod } from '@sentry/types';
2+
3+
import { API } from '../../src/api';
4+
import { eventToSentryRequest } from '../../src/request';
5+
6+
describe('eventToSentryRequest', () => {
7+
const api = new API('https://[email protected]/12312012');
8+
const event: Event = {
9+
contexts: { trace: { trace_id: '1231201211212012', span_id: '12261980', op: 'pageload' } },
10+
environment: 'dogpark',
11+
event_id: '0908201304152013',
12+
release: 'off.leash.park',
13+
spans: [],
14+
transaction: '/dogs/are/great/',
15+
type: 'transaction',
16+
user: { id: '1121', username: 'CharlieDog', ip_address: '11.21.20.12' },
17+
};
18+
19+
[
20+
{ method: TransactionSamplingMethod.Rate, rate: '0.1121', dog: 'Charlie' },
21+
{ method: TransactionSamplingMethod.Sampler, rate: '0.1231', dog: 'Maisey' },
22+
{ method: TransactionSamplingMethod.Inheritance, dog: 'Cory' },
23+
{ method: TransactionSamplingMethod.Explicit, dog: 'Bodhi' },
24+
25+
// this shouldn't ever happen (at least the method should always be included in tags), but good to know that things
26+
// won't blow up if it does
27+
{ dog: 'Lucy' },
28+
].forEach(({ method, rate, dog }) => {
29+
it(`adds transaction sampling information to item header - ${method}, ${rate}, ${dog}`, () => {
30+
// TODO kmclb - once tag types are loosened, don't need to cast to string here
31+
event.tags = { __sentry_samplingMethod: String(method), __sentry_sampleRate: String(rate), dog };
32+
33+
const result = eventToSentryRequest(event as Event, api);
34+
35+
const [envelopeHeaderString, itemHeaderString, eventString] = result.body.split('\n');
36+
37+
const envelope = {
38+
envelopeHeader: JSON.parse(envelopeHeaderString),
39+
itemHeader: JSON.parse(itemHeaderString),
40+
event: JSON.parse(eventString),
41+
};
42+
43+
// the right stuff is added to the item header
44+
expect(envelope.itemHeader).toEqual({
45+
type: 'transaction',
46+
// TODO kmclb - once tag types are loosened, don't need to cast to string here
47+
sample_rates: [{ id: String(method), rate: String(rate) }],
48+
});
49+
50+
// show that it pops the right tags and leaves the rest alone
51+
expect('__sentry_samplingMethod' in envelope.event.tags).toBe(false);
52+
expect('__sentry_sampleRate' in envelope.event.tags).toBe(false);
53+
expect('dog' in envelope.event.tags).toBe(true);
54+
});
55+
});
56+
});

packages/tracing/src/hubextensions.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getActiveDomain, getMainCarrier, Hub } from '@sentry/hub';
2-
import { CustomSamplingContext, SamplingContext, TransactionContext } from '@sentry/types';
2+
import { CustomSamplingContext, SamplingContext, TransactionContext, TransactionSamplingMethod } from '@sentry/types';
33
import {
44
dynamicRequire,
55
extractNodeRequestData,
@@ -28,18 +28,6 @@ function traceHeaders(this: Hub): { [key: string]: string } {
2828
return {};
2929
}
3030

31-
/**
32-
* Implements sampling inheritance and falls back to user-provided static rate if no parent decision is available.
33-
*
34-
* @param parentSampled: The parent transaction's sampling decision, if any.
35-
* @param givenRate: The rate to use if no parental decision is available.
36-
*
37-
* @returns The parent's sampling decision (if one exists), or the provided static rate
38-
*/
39-
function _inheritOrUseGivenRate(parentSampled: boolean | undefined, givenRate: unknown): boolean | unknown {
40-
return parentSampled !== undefined ? parentSampled : givenRate;
41-
}
42-
4331
/**
4432
* Makes a sampling decision for the given transaction and stores it on the transaction.
4533
*
@@ -64,15 +52,35 @@ function sample<T extends Transaction>(hub: Hub, transaction: T, samplingContext
6452

6553
// if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that
6654
if (transaction.sampled !== undefined) {
55+
transaction.tags = { ...transaction.tags, __sentry_samplingMethod: TransactionSamplingMethod.Explicit };
6756
return transaction;
6857
}
6958

7059
// we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` were defined, so one of these should
7160
// work; prefer the hook if so
72-
const sampleRate =
73-
typeof options.tracesSampler === 'function'
74-
? options.tracesSampler(samplingContext)
75-
: _inheritOrUseGivenRate(samplingContext.parentSampled, options.tracesSampleRate);
61+
let sampleRate;
62+
if (typeof options.tracesSampler === 'function') {
63+
sampleRate = options.tracesSampler(samplingContext);
64+
// cast the rate to a number first in case it's a boolean
65+
transaction.tags = {
66+
...transaction.tags,
67+
__sentry_samplingMethod: TransactionSamplingMethod.Sampler,
68+
// TODO kmclb - once tag types are loosened, don't need to cast to string here
69+
__sentry_sampleRate: String(Number(sampleRate)),
70+
};
71+
} else if (samplingContext.parentSampled !== undefined) {
72+
sampleRate = samplingContext.parentSampled;
73+
transaction.tags = { ...transaction.tags, __sentry_samplingMethod: TransactionSamplingMethod.Inheritance };
74+
} else {
75+
sampleRate = options.tracesSampleRate;
76+
// cast the rate to a number first in case it's a boolean
77+
transaction.tags = {
78+
...transaction.tags,
79+
__sentry_samplingMethod: TransactionSamplingMethod.Rate,
80+
// TODO kmclb - once tag types are loosened, don't need to cast to string here
81+
__sentry_sampleRate: String(Number(sampleRate)),
82+
};
83+
}
7684

7785
// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
7886
// only valid values are booleans or numbers between 0 and 1.)
@@ -88,7 +96,7 @@ function sample<T extends Transaction>(hub: Hub, transaction: T, samplingContext
8896
`[Tracing] Discarding transaction because ${
8997
typeof options.tracesSampler === 'function'
9098
? 'tracesSampler returned 0 or false'
91-
: 'tracesSampleRate is set to 0'
99+
: 'a negative sampling decision was inherited or tracesSampleRate is set to 0'
92100
}`,
93101
);
94102
transaction.sampled = false;

packages/tracing/test/hub.test.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ describe('Hub', () => {
171171
});
172172

173173
describe('sample()', () => {
174-
it('should not sample transactions when tracing is disabled', () => {
174+
it('should set sampled = false when tracing is disabled', () => {
175175
// neither tracesSampleRate nor tracesSampler is defined -> tracing disabled
176176
const hub = new Hub(new BrowserClient({}));
177177
const transaction = hub.startTransaction({ name: 'dogpark' });
@@ -219,7 +219,7 @@ describe('Hub', () => {
219219
expect(transaction.sampled).toBe(true);
220220
});
221221

222-
it('should not try to override positive sampling decision provided in transaction context', () => {
222+
it('should not try to override explicitly set positive sampling decision', () => {
223223
// so that the decision otherwise would be false
224224
const tracesSampler = jest.fn().mockReturnValue(0);
225225
const hub = new Hub(new BrowserClient({ tracesSampler }));
@@ -228,7 +228,7 @@ describe('Hub', () => {
228228
expect(transaction.sampled).toBe(true);
229229
});
230230

231-
it('should not try to override negative sampling decision provided in transaction context', () => {
231+
it('should not try to override explicitly set negative sampling decision', () => {
232232
// so that the decision otherwise would be true
233233
const tracesSampler = jest.fn().mockReturnValue(1);
234234
const hub = new Hub(new BrowserClient({ tracesSampler }));
@@ -255,6 +255,40 @@ describe('Hub', () => {
255255
expect(tracesSampler).toHaveBeenCalled();
256256
expect(transaction.sampled).toBe(true);
257257
});
258+
259+
it('should record sampling method when sampling decision is explicitly set', () => {
260+
const tracesSampler = jest.fn().mockReturnValue(0.1121);
261+
const hub = new Hub(new BrowserClient({ tracesSampler }));
262+
const transaction = hub.startTransaction({ name: 'dogpark', sampled: true });
263+
264+
expect(transaction.tags).toEqual(expect.objectContaining({ __sentry_samplingMethod: 'explicitly_set' }));
265+
});
266+
267+
it('should record sampling method and rate when sampling decision comes from tracesSampler', () => {
268+
const tracesSampler = jest.fn().mockReturnValue(0.1121);
269+
const hub = new Hub(new BrowserClient({ tracesSampler }));
270+
const transaction = hub.startTransaction({ name: 'dogpark' });
271+
272+
expect(transaction.tags).toEqual(
273+
expect.objectContaining({ __sentry_samplingMethod: 'client_sampler', __sentry_sampleRate: '0.1121' }),
274+
);
275+
});
276+
277+
it('should record sampling method when sampling decision is inherited', () => {
278+
const hub = new Hub(new BrowserClient({ tracesSampleRate: 0.1121 }));
279+
const transaction = hub.startTransaction({ name: 'dogpark', parentSampled: true });
280+
281+
expect(transaction.tags).toEqual(expect.objectContaining({ __sentry_samplingMethod: 'inheritance' }));
282+
});
283+
284+
it('should record sampling method and rate when sampling decision comes from traceSampleRate', () => {
285+
const hub = new Hub(new BrowserClient({ tracesSampleRate: 0.1121 }));
286+
const transaction = hub.startTransaction({ name: 'dogpark' });
287+
288+
expect(transaction.tags).toEqual(
289+
expect.objectContaining({ __sentry_samplingMethod: 'client_rate', __sentry_sampleRate: '0.1121' }),
290+
);
291+
});
258292
});
259293

260294
describe('isValidSampleRate()', () => {

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export {
3232
TraceparentData,
3333
Transaction,
3434
TransactionContext,
35+
TransactionSamplingMethod,
3536
} from './transaction';
3637
export { Thread } from './thread';
3738
export { Transport, TransportOptions, TransportClass } from './transport';

packages/types/src/transaction.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,10 @@ export interface SamplingContext extends CustomSamplingContext {
100100
}
101101

102102
export type Measurements = Record<string, { value: number }>;
103+
104+
export enum TransactionSamplingMethod {
105+
Explicit = 'explicitly_set',
106+
Sampler = 'client_sampler',
107+
Rate = 'client_rate',
108+
Inheritance = 'inheritance',
109+
}

0 commit comments

Comments
 (0)