diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index ad6858d41e4a..007612d8fb34 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -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. */ @@ -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( - transaction: T, - options: Pick, - 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. * @@ -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, @@ -207,7 +87,7 @@ export function startIdleTransaction( const options: Partial = (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, diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index c5be88f8c350..40d667c67ff0 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -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'; diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts new file mode 100644 index 000000000000..4b357b7bf1be --- /dev/null +++ b/packages/core/src/tracing/sampling.ts @@ -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( + transaction: T, + options: Pick, + 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; +}