Skip to content

Commit 7c14283

Browse files
authored
feat(tracing): Add hook for trace sampling function to SDK options (#2820)
Currently, tracing is either on, with a static sample rate for all transactions, or off. This doesn't let users either a) sample different transactions at different rates or b) filter out certain transactions all together. This PR introduces a new SDK option, `tracesSampler`, whose value is a callback with the signature `tracesSampler(samplingContext: SamplingContext): number | boolean`, where `samplingContext` is a combination of automatically-provided data (varying by SDK/integration) and data passed by the user to `startTransaction()` as an optional third parameter. Given that data, the `tracesSampler` function can then compute the appropriate sample rate and return it to the sampling function. Example use cases for variable sample rate: - Collect traces for all enterprise customers but only a fraction of regular customers - Collect a higher percentage of traces on rarely-visited pages, to ensure a decent sample size - Collect more traces on a page you recently revamped compared to one whose code you already have well-profiled Example use cases for filtering: - Don't trace pre-flight CORS requests - Don't trace health check requests - Don't collect traces from users with a certain browser extension enabled, because it's known to break things - Don't trace navigation to deprecated parts of your web app (A user can, of course, combine these ideas as well, returning a variable sample rate for some traces and 0 to filter out others.) In order to support this addition, a few other changes needed to be made: - Move method for extracting node request data to `@sentry/utils` - Break up `misc.ts` to eliminate circular dependencies within `@sentry/utils` - Move method for extracting `sentry-trace` data into `@sentry/tracing` utils - Tweak some types related to domains and request data, and polyfill `WorkerLocation` type
1 parent f398514 commit 7c14283

32 files changed

+1134
-436
lines changed

packages/gatsby/README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ To automatically capture the `release` value on Vercel you will need to register
4040

4141
## Sentry Performance
4242

43-
To enable Tracing support, supply the `tracesSampleRate` to the options and make sure you have installed the `@sentry/tracing` package. This will also turn on the `BrowserTracing` integration for automatic instrumentation of the browser.
43+
To enable tracing, supply either `tracesSampleRate` or `tracesSampler` to the options and make sure you have installed the `@sentry/tracing` package. This will also turn on the `BrowserTracing` integration for automatic instrumentation of pageloads and navigations.
4444

4545
```javascript
4646
{
@@ -49,8 +49,31 @@ To enable Tracing support, supply the `tracesSampleRate` to the options and make
4949
{
5050
resolve: "@sentry/gatsby",
5151
options: {
52-
dsn: process.env.SENTRY_DSN, // this is the default
53-
tracesSampleRate: 1, // this is just to test, you should lower this in production
52+
dsn: process.env.SENTRY_DSN, // this is the default
53+
54+
// A rate of 1 means all traces will be sent, so it's good for testing.
55+
// In production, you'll likely want to either choose a lower rate or use `tracesSampler` instead (see below).
56+
tracesSampleRate: 1,
57+
58+
// Alternatively:
59+
tracesSampler: samplingContext => {
60+
// Examine provided context data (along with anything in the global namespace) to decide the sample rate
61+
// for this transaction.
62+
// Can return 0 to drop the transaction entirely.
63+
64+
if ("...") {
65+
return 0.5 // These are important - take a big sample
66+
}
67+
else if ("...") {
68+
return 0.01 // These are less important or happen much more frequently - only take 1% of them
69+
}
70+
else if ("...") {
71+
return 0 // These aren't something worth tracking - drop all transactions like this
72+
}
73+
else {
74+
return 0.1 // Default sample rate
75+
}
76+
}
5477
}
5578
},
5679
// ...
@@ -68,7 +91,7 @@ If you want to supply options to the `BrowserTracing` integration, use the `brow
6891
resolve: "@sentry/gatsby",
6992
options: {
7093
dsn: process.env.SENTRY_DSN, // this is the default
71-
tracesSampleRate: 1, // this is just to test, you should lower this in production
94+
tracesSampleRate: 1, // or tracesSampler (see above)
7295
browserTracingOptions: {
7396
// disable creating spans for XHR requests
7497
traceXHR: false,

packages/gatsby/gatsby-browser.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ exports.onClientEntry = function(_, pluginParams) {
66
return;
77
}
88

9-
const tracesSampleRate = pluginParams.tracesSampleRate !== undefined ? pluginParams.tracesSampleRate : 0;
109
const integrations = [...(pluginParams.integrations || [])];
1110

12-
if (tracesSampleRate && !integrations.some(ele => ele.name === 'BrowserTracing')) {
11+
if (Tracing.hasTracingEnabled(pluginParams) && !integrations.some(ele => ele.name === 'BrowserTracing')) {
1312
integrations.push(new Tracing.Integrations.BrowserTracing(pluginParams.browserTracingOptions));
1413
}
1514

@@ -22,7 +21,6 @@ exports.onClientEntry = function(_, pluginParams) {
2221
// eslint-disable-next-line no-undef
2322
dsn: __SENTRY_DSN__,
2423
...pluginParams,
25-
tracesSampleRate,
2624
integrations,
2725
});
2826

packages/gatsby/test/gatsby-browser.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ describe('onClientEntry', () => {
5353
environment: process.env.NODE_ENV,
5454
integrations: [],
5555
release: (global as any).__SENTRY_RELEASE__,
56-
tracesSampleRate: 0,
5756
});
5857
});
5958

@@ -100,6 +99,16 @@ describe('onClientEntry', () => {
10099
);
101100
});
102101

102+
it('sets a tracesSampler if defined as option', () => {
103+
const tracesSampler = jest.fn();
104+
onClientEntry(undefined, { tracesSampler });
105+
expect(sentryInit).toHaveBeenLastCalledWith(
106+
expect.objectContaining({
107+
tracesSampler,
108+
}),
109+
);
110+
});
111+
103112
it('adds `BrowserTracing` integration if tracesSampleRate is defined', () => {
104113
onClientEntry(undefined, { tracesSampleRate: 0.5 });
105114
expect(sentryInit).toHaveBeenLastCalledWith(
@@ -109,6 +118,16 @@ describe('onClientEntry', () => {
109118
);
110119
});
111120

121+
it('adds `BrowserTracing` integration if tracesSampler is defined', () => {
122+
const tracesSampler = jest.fn();
123+
onClientEntry(undefined, { tracesSampler });
124+
expect(sentryInit).toHaveBeenLastCalledWith(
125+
expect.objectContaining({
126+
integrations: [expect.objectContaining({ name: 'BrowserTracing' })],
127+
}),
128+
);
129+
});
130+
112131
it('only defines a single `BrowserTracing` integration', () => {
113132
const Tracing = jest.requireActual('@sentry/tracing');
114133
const integrations = [new Tracing.Integrations.BrowserTracing()];

packages/hub/src/hub.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Breadcrumb,
44
BreadcrumbHint,
55
Client,
6+
CustomSamplingContext,
67
Event,
78
EventHint,
89
Extra,
@@ -19,7 +20,7 @@ import {
1920
} from '@sentry/types';
2021
import { consoleSandbox, getGlobalObject, isNodeEnv, logger, timestampWithMs, uuid4 } from '@sentry/utils';
2122

22-
import { Carrier, Layer } from './interfaces';
23+
import { Carrier, DomainAsCarrier, Layer } from './interfaces';
2324
import { Scope } from './scope';
2425

2526
/**
@@ -369,8 +370,8 @@ export class Hub implements HubInterface {
369370
/**
370371
* @inheritDoc
371372
*/
372-
public startTransaction(context: TransactionContext): Transaction {
373-
return this._callExtensionMethod('startTransaction', context);
373+
public startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction {
374+
return this._callExtensionMethod('startTransaction', context, customSamplingContext);
374375
}
375376

376377
/**
@@ -456,22 +457,24 @@ export function getCurrentHub(): Hub {
456457
return getHubFromCarrier(registry);
457458
}
458459

460+
/**
461+
* Returns the active domain, if one exists
462+
*
463+
* @returns The domain, or undefined if there is no active domain
464+
*/
465+
export function getActiveDomain(): DomainAsCarrier | undefined {
466+
const sentry = getMainCarrier().__SENTRY__;
467+
468+
return sentry && sentry.extensions && sentry.extensions.domain && sentry.extensions.domain.active;
469+
}
470+
459471
/**
460472
* Try to read the hub from an active domain, and fallback to the registry if one doesn't exist
461473
* @returns discovered hub
462474
*/
463475
function getHubFromActiveDomain(registry: Carrier): Hub {
464476
try {
465-
const property = 'domain';
466-
const carrier = getMainCarrier();
467-
const sentry = carrier.__SENTRY__;
468-
if (!sentry || !sentry.extensions || !sentry.extensions[property]) {
469-
return getHubFromCarrier(registry);
470-
}
471-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
472-
const domain = sentry.extensions[property] as any;
473-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
474-
const activeDomain = domain.active;
477+
const activeDomain = getActiveDomain();
475478

476479
// If there's no active domain, just return global hub
477480
if (!activeDomain) {

packages/hub/src/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1-
export { Carrier, Layer } from './interfaces';
1+
export { Carrier, DomainAsCarrier, Layer } from './interfaces';
22
export { addGlobalEventProcessor, Scope } from './scope';
3-
export { getCurrentHub, getHubFromCarrier, getMainCarrier, Hub, makeMain, setHubOnCarrier } from './hub';
3+
export {
4+
getActiveDomain,
5+
getCurrentHub,
6+
getHubFromCarrier,
7+
getMainCarrier,
8+
Hub,
9+
makeMain,
10+
setHubOnCarrier,
11+
} from './hub';

packages/hub/src/interfaces.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Client } from '@sentry/types';
2+
import * as domain from 'domain';
23

34
import { Hub } from './hub';
45
import { Scope } from './scope';
@@ -20,9 +21,23 @@ export interface Carrier {
2021
__SENTRY__?: {
2122
hub?: Hub;
2223
/**
23-
* These are extension methods for the hub, the current instance of the hub will be bound to it
24+
* Extra Hub properties injected by various SDKs
2425
*/
25-
// eslint-disable-next-line @typescript-eslint/ban-types
26-
extensions?: { [key: string]: Function };
26+
extensions?: {
27+
/** Hack to prevent bundlers from breaking our usage of the domain package in the cross-platform Hub package */
28+
domain?: typeof domain & {
29+
/**
30+
* The currently active domain. This is part of the domain package, but for some reason not declared in the
31+
* package's typedef.
32+
*/
33+
active?: domain.Domain;
34+
};
35+
} & {
36+
/** Extension methods for the hub, which are bound to the current Hub instance */
37+
// eslint-disable-next-line @typescript-eslint/ban-types
38+
[key: string]: Function;
39+
};
2740
};
2841
}
42+
43+
export interface DomainAsCarrier extends domain.Domain, Carrier {}

packages/minimal/src/index.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getCurrentHub, Hub, Scope } from '@sentry/hub';
22
import {
33
Breadcrumb,
44
CaptureContext,
5+
CustomSamplingContext,
56
Event,
67
Extra,
78
Extras,
@@ -190,22 +191,25 @@ export function _callOnClient(method: string, ...args: any[]): void {
190191
}
191192

192193
/**
193-
* Starts a new `Transaction` and returns it. This is the entry point to manual
194-
* tracing instrumentation.
194+
* Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation.
195195
*
196-
* A tree structure can be built by adding child spans to the transaction, and
197-
* child spans to other spans. To start a new child span within the transaction
198-
* or any span, call the respective `.startChild()` method.
196+
* A tree structure can be built by adding child spans to the transaction, and child spans to other spans. To start a
197+
* new child span within the transaction or any span, call the respective `.startChild()` method.
199198
*
200-
* Every child span must be finished before the transaction is finished,
201-
* otherwise the unfinished spans are discarded.
199+
* Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded.
202200
*
203-
* The transaction must be finished with a call to its `.finish()` method, at
204-
* which point the transaction with all its finished child spans will be sent to
205-
* Sentry.
201+
* The transaction must be finished with a call to its `.finish()` method, at which point the transaction with all its
202+
* finished child spans will be sent to Sentry.
206203
*
207204
* @param context Properties of the new `Transaction`.
205+
* @param customSamplingContext Information given to the transaction sampling function (along with context-dependent
206+
* default values). See {@link Options.tracesSampler}.
207+
*
208+
* @returns The transaction which was just started
208209
*/
209-
export function startTransaction(context: TransactionContext): Transaction {
210-
return callOnHub('startTransaction', { ...context });
210+
export function startTransaction(
211+
context: TransactionContext,
212+
customSamplingContext?: CustomSamplingContext,
213+
): Transaction {
214+
return callOnHub('startTransaction', { ...context }, customSamplingContext);
211215
}

0 commit comments

Comments
 (0)