Skip to content

feat(core): Extract sampleTransaction method out #9136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 5 additions & 125 deletions packages/core/src/tracing/hubextensions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { ClientOptions, CustomSamplingContext, Options, SamplingContext, TransactionContext } from '@sentry/types';
import { isNaN, logger } from '@sentry/utils';
import type { ClientOptions, CustomSamplingContext, TransactionContext } from '@sentry/types';
import { logger } from '@sentry/utils';

import type { Hub } from '../hub';
import { getMainCarrier } from '../hub';
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
import { registerErrorInstrumentation } from './errors';
import { IdleTransaction } from './idletransaction';
import { sampleTransaction } from './sampling';
import { Transaction } from './transaction';

/** Returns all trace headers that are currently on the top scope. */
Expand All @@ -20,126 +20,6 @@ function traceHeaders(this: Hub): { [key: string]: string } {
: {};
}

/**
* Makes a sampling decision for the given transaction and stores it on the transaction.
*
* Called every time a transaction is created. Only transactions which emerge with a `sampled` value of `true` will be
* sent to Sentry.
*
* @param transaction: The transaction needing a sampling decision
* @param options: The current client's options, so we can access `tracesSampleRate` and/or `tracesSampler`
* @param samplingContext: Default and user-provided data which may be used to help make the decision
*
* @returns The given transaction with its `sampled` value set
*/
function sample<T extends Transaction>(
transaction: T,
options: Pick<Options, 'tracesSampleRate' | 'tracesSampler' | 'enableTracing'>,
samplingContext: SamplingContext,
): T {
// nothing to do if tracing is not enabled
if (!hasTracingEnabled(options)) {
transaction.sampled = false;
return transaction;
}

// if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that
if (transaction.sampled !== undefined) {
transaction.setMetadata({
sampleRate: Number(transaction.sampled),
});
return transaction;
}

// we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` nor `enableTracing` were defined, so one of these should
// work; prefer the hook if so
let sampleRate;
if (typeof options.tracesSampler === 'function') {
sampleRate = options.tracesSampler(samplingContext);
transaction.setMetadata({
sampleRate: Number(sampleRate),
});
} else if (samplingContext.parentSampled !== undefined) {
sampleRate = samplingContext.parentSampled;
} else if (typeof options.tracesSampleRate !== 'undefined') {
sampleRate = options.tracesSampleRate;
transaction.setMetadata({
sampleRate: Number(sampleRate),
});
} else {
// When `enableTracing === true`, we use a sample rate of 100%
sampleRate = 1;
transaction.setMetadata({
sampleRate,
});
}

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(sampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Tracing] Discarding transaction because of invalid sample rate.');
transaction.sampled = false;
return transaction;
}

// if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped
if (!sampleRate) {
__DEBUG_BUILD__ &&
logger.log(
`[Tracing] Discarding transaction because ${
typeof options.tracesSampler === 'function'
? 'tracesSampler returned 0 or false'
: 'a negative sampling decision was inherited or tracesSampleRate is set to 0'
}`,
);
transaction.sampled = false;
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
transaction.sampled = Math.random() < (sampleRate as number | boolean);

// if we're not going to keep it, we're done
if (!transaction.sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Tracing] Discarding transaction because it's not included in the random sample (sampling rate = ${Number(
sampleRate,
)})`,
);
return transaction;
}

__DEBUG_BUILD__ && logger.log(`[Tracing] starting ${transaction.op} transaction - ${transaction.name}`);
return transaction;
}

/**
* Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
*/
function isValidSampleRate(rate: unknown): boolean {
// we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) {
__DEBUG_BUILD__ &&
logger.warn(
`[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify(
rate,
)} of type ${JSON.stringify(typeof rate)}.`,
);
return false;
}

// in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false
if (rate < 0 || rate > 1) {
__DEBUG_BUILD__ &&
logger.warn(`[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got ${rate}.`);
return false;
}
return true;
}

/**
* Creates a new transaction and adds a sampling decision if it doesn't yet have one.
*
Expand Down Expand Up @@ -177,7 +57,7 @@ The transaction will not be sampled. Please use the ${configInstrumenter} instru
}

let transaction = new Transaction(transactionContext, this);
transaction = sample(transaction, options, {
transaction = sampleTransaction(transaction, options, {
parentSampled: transactionContext.parentSampled,
transactionContext,
...customSamplingContext,
Expand Down Expand Up @@ -207,7 +87,7 @@ export function startIdleTransaction(
const options: Partial<ClientOptions> = (client && client.getOptions()) || {};

let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, heartbeatInterval, onScope);
transaction = sample(transaction, options, {
transaction = sampleTransaction(transaction, options, {
parentSampled: transactionContext.parentSampled,
transactionContext,
...customSamplingContext,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export type { SpanStatusType } from './span';
export { trace, getActiveSpan, startSpan, startInactiveSpan, startActiveSpan, startSpanManual } from './trace';
export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
export { setMeasurement } from './measurement';
export { sampleTransaction } from './sampling';
122 changes: 122 additions & 0 deletions packages/core/src/tracing/sampling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { Options, SamplingContext } from '@sentry/types';
import { isNaN, logger } from '@sentry/utils';

import { hasTracingEnabled } from '../utils/hasTracingEnabled';
import type { Transaction } from './transaction';

/**
* Makes a sampling decision for the given transaction and stores it on the transaction.
*
* Called every time a transaction is created. Only transactions which emerge with a `sampled` value of `true` will be
* sent to Sentry.
*
* This method muttes the given `transaction` and will set the `sampled` value on it.
* It returns the same transaction, for convenience.
*/
export function sampleTransaction<T extends Transaction>(
transaction: T,
options: Pick<Options, 'tracesSampleRate' | 'tracesSampler' | 'enableTracing'>,
samplingContext: SamplingContext,
): T {
// nothing to do if tracing is not enabled
if (!hasTracingEnabled(options)) {
transaction.sampled = false;
return transaction;
}

// if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that
if (transaction.sampled !== undefined) {
transaction.setMetadata({
sampleRate: Number(transaction.sampled),
});
return transaction;
}

// we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` nor `enableTracing` were defined, so one of these should
// work; prefer the hook if so
let sampleRate;
if (typeof options.tracesSampler === 'function') {
sampleRate = options.tracesSampler(samplingContext);
transaction.setMetadata({
sampleRate: Number(sampleRate),
});
} else if (samplingContext.parentSampled !== undefined) {
sampleRate = samplingContext.parentSampled;
} else if (typeof options.tracesSampleRate !== 'undefined') {
sampleRate = options.tracesSampleRate;
transaction.setMetadata({
sampleRate: Number(sampleRate),
});
} else {
// When `enableTracing === true`, we use a sample rate of 100%
sampleRate = 1;
transaction.setMetadata({
sampleRate,
});
}

// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
// only valid values are booleans or numbers between 0 and 1.)
if (!isValidSampleRate(sampleRate)) {
__DEBUG_BUILD__ && logger.warn('[Tracing] Discarding transaction because of invalid sample rate.');
transaction.sampled = false;
return transaction;
}

// if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped
if (!sampleRate) {
__DEBUG_BUILD__ &&
logger.log(
`[Tracing] Discarding transaction because ${
typeof options.tracesSampler === 'function'
? 'tracesSampler returned 0 or false'
: 'a negative sampling decision was inherited or tracesSampleRate is set to 0'
}`,
);
transaction.sampled = false;
return transaction;
}

// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
transaction.sampled = Math.random() < (sampleRate as number | boolean);

// if we're not going to keep it, we're done
if (!transaction.sampled) {
__DEBUG_BUILD__ &&
logger.log(
`[Tracing] Discarding transaction because it's not included in the random sample (sampling rate = ${Number(
sampleRate,
)})`,
);
return transaction;
}

__DEBUG_BUILD__ && logger.log(`[Tracing] starting ${transaction.op} transaction - ${transaction.name}`);
return transaction;
}

/**
* Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
*/
function isValidSampleRate(rate: unknown): boolean {
// we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) {
__DEBUG_BUILD__ &&
logger.warn(
`[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify(
rate,
)} of type ${JSON.stringify(typeof rate)}.`,
);
return false;
}

// in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false
if (rate < 0 || rate > 1) {
__DEBUG_BUILD__ &&
logger.warn(`[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got ${rate}.`);
return false;
}
return true;
}