Skip to content

Commit

Permalink
feat(core): Extract sampleTransaction method out
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Oct 2, 2023
1 parent c356073 commit 9fcee38
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 125 deletions.
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;
}

0 comments on commit 9fcee38

Please sign in to comment.