UNPKG

@sentry/core

Version:
417 lines (358 loc) 13 kB
import { propagationContextFromHeaders, generatePropagationContext, logger } from '@sentry/utils'; import { getMainCarrier } from '../carrier.js'; import { withScope, getCurrentScope, getIsolationScope, getClient } from '../currentScopes.js'; import { getAsyncContextStrategy } from '../asyncContext/index.js'; import { DEBUG_BUILD } from '../debug-build.js'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes.js'; import { handleCallbackErrors } from '../utils/handleCallbackErrors.js'; import { hasTracingEnabled } from '../utils/hasTracingEnabled.js'; import { _setSpanForScope, _getSpanForScope } from '../utils/spanOnScope.js'; import { spanToJSON, addChildSpanToSpan, spanIsSampled, spanTimeInputToSeconds, getRootSpan } from '../utils/spanUtils.js'; import { getDynamicSamplingContextFromSpan, freezeDscOnSpan } from './dynamicSamplingContext.js'; import { logSpanStart } from './logSpans.js'; import { sampleSpan } from './sampling.js'; import { SentryNonRecordingSpan } from './sentryNonRecordingSpan.js'; import { SentrySpan } from './sentrySpan.js'; import { SPAN_STATUS_ERROR } from './spanstatus.js'; import { setCapturedScopesOnSpan } from './utils.js'; const SUPPRESS_TRACING_KEY = '__SENTRY_SUPPRESS_TRACING__'; /** * Wraps a function with a transaction/span and finishes the span after the function is done. * The created span is the active span and will be used as parent by other spans created inside the function * and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active. * * If you want to create a span that is not set as active, use {@link startInactiveSpan}. * * You'll always get a span passed to the callback, * it may just be a non-recording span if the span is not sampled or if tracing is disabled. */ function startSpan(context, callback) { const acs = getAcs(); if (acs.startSpan) { return acs.startSpan(context, callback); } const spanContext = normalizeContext(context); return withScope(context.scope, scope => { const parentSpan = getParentSpan(scope); const shouldSkipSpan = context.onlyIfParent && !parentSpan; const activeSpan = shouldSkipSpan ? new SentryNonRecordingSpan() : createChildOrRootSpan({ parentSpan, spanContext, forceTransaction: context.forceTransaction, scope, }); _setSpanForScope(scope, activeSpan); return handleCallbackErrors( () => callback(activeSpan), () => { // Only update the span status if it hasn't been changed yet, and the span is not yet finished const { status } = spanToJSON(activeSpan); if (activeSpan.isRecording() && (!status || status === 'ok')) { activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } }, () => activeSpan.end(), ); }); } /** * Similar to `Sentry.startSpan`. Wraps a function with a transaction/span, but does not finish the span * after the function is done automatically. You'll have to call `span.end()` manually. * * The created span is the active span and will be used as parent by other spans created inside the function * and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active. * * You'll always get a span passed to the callback, * it may just be a non-recording span if the span is not sampled or if tracing is disabled. */ function startSpanManual(context, callback) { const acs = getAcs(); if (acs.startSpanManual) { return acs.startSpanManual(context, callback); } const spanContext = normalizeContext(context); return withScope(context.scope, scope => { const parentSpan = getParentSpan(scope); const shouldSkipSpan = context.onlyIfParent && !parentSpan; const activeSpan = shouldSkipSpan ? new SentryNonRecordingSpan() : createChildOrRootSpan({ parentSpan, spanContext, forceTransaction: context.forceTransaction, scope, }); _setSpanForScope(scope, activeSpan); function finishAndSetSpan() { activeSpan.end(); } return handleCallbackErrors( () => callback(activeSpan, finishAndSetSpan), () => { // Only update the span status if it hasn't been changed yet, and the span is not yet finished const { status } = spanToJSON(activeSpan); if (activeSpan.isRecording() && (!status || status === 'ok')) { activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); } }, ); }); } /** * Creates a span. This span is not set as active, so will not get automatic instrumentation spans * as children or be able to be accessed via `Sentry.getActiveSpan()`. * * If you want to create a span that is set as active, use {@link startSpan}. * * This function will always return a span, * it may just be a non-recording span if the span is not sampled or if tracing is disabled. */ function startInactiveSpan(context) { const acs = getAcs(); if (acs.startInactiveSpan) { return acs.startInactiveSpan(context); } const spanContext = normalizeContext(context); const scope = context.scope || getCurrentScope(); const parentSpan = getParentSpan(scope); const shouldSkipSpan = context.onlyIfParent && !parentSpan; if (shouldSkipSpan) { return new SentryNonRecordingSpan(); } return createChildOrRootSpan({ parentSpan, spanContext, forceTransaction: context.forceTransaction, scope, }); } /** * Continue a trace from `sentry-trace` and `baggage` values. * These values can be obtained from incoming request headers, or in the browser from `<meta name="sentry-trace">` * and `<meta name="baggage">` HTML tags. * * Spans started with `startSpan`, `startSpanManual` and `startInactiveSpan`, within the callback will automatically * be attached to the incoming trace. */ const continueTrace = ( { sentryTrace, baggage, } , callback, ) => { return withScope(scope => { const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); scope.setPropagationContext(propagationContext); return callback(); }); }; /** * Forks the current scope and sets the provided span as active span in the context of the provided callback. Can be * passed `null` to start an entirely new span tree. * * @param span Spans started in the context of the provided callback will be children of this span. If `null` is passed, * spans started within the callback will not be attached to a parent span. * @param callback Execution context in which the provided span will be active. Is passed the newly forked scope. * @returns the value returned from the provided callback function. */ function withActiveSpan(span, callback) { const acs = getAcs(); if (acs.withActiveSpan) { return acs.withActiveSpan(span, callback); } return withScope(scope => { _setSpanForScope(scope, span || undefined); return callback(scope); }); } /** Suppress tracing in the given callback, ensuring no spans are generated inside of it. */ function suppressTracing(callback) { const acs = getAcs(); if (acs.suppressTracing) { return acs.suppressTracing(callback); } return withScope(scope => { scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: true }); return callback(); }); } /** * Starts a new trace for the duration of the provided callback. Spans started within the * callback will be part of the new trace instead of a potentially previously started trace. * * Important: Only use this function if you want to override the default trace lifetime and * propagation mechanism of the SDK for the duration and scope of the provided callback. * The newly created trace will also be the root of a new distributed trace, for example if * you make http requests within the callback. * This function might be useful if the operation you want to instrument should not be part * of a potentially ongoing trace. * * Default behavior: * - Server-side: A new trace is started for each incoming request. * - Browser: A new trace is started for each page our route. Navigating to a new route * or page will automatically create a new trace. */ function startNewTrace(callback) { return withScope(scope => { scope.setPropagationContext(generatePropagationContext()); DEBUG_BUILD && logger.info(`Starting a new trace with id ${scope.getPropagationContext().traceId}`); return withActiveSpan(null, callback); }); } function createChildOrRootSpan({ parentSpan, spanContext, forceTransaction, scope, } ) { if (!hasTracingEnabled()) { return new SentryNonRecordingSpan(); } const isolationScope = getIsolationScope(); let span; if (parentSpan && !forceTransaction) { span = _startChildSpan(parentSpan, scope, spanContext); addChildSpanToSpan(parentSpan, span); } else if (parentSpan) { // If we forced a transaction but have a parent span, make sure to continue from the parent span, not the scope const dsc = getDynamicSamplingContextFromSpan(parentSpan); const { traceId, spanId: parentSpanId } = parentSpan.spanContext(); const parentSampled = spanIsSampled(parentSpan); span = _startRootSpan( { traceId, parentSpanId, ...spanContext, }, scope, parentSampled, ); freezeDscOnSpan(span, dsc); } else { const { traceId, dsc, parentSpanId, sampled: parentSampled, } = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext(), }; span = _startRootSpan( { traceId, parentSpanId, ...spanContext, }, scope, parentSampled, ); if (dsc) { freezeDscOnSpan(span, dsc); } } logSpanStart(span); setCapturedScopesOnSpan(span, scope, isolationScope); return span; } /** * This converts StartSpanOptions to SentrySpanArguments. * For the most part (for now) we accept the same options, * but some of them need to be transformed. * * Eventually the StartSpanOptions will be more aligned with OpenTelemetry. */ function normalizeContext(context) { const exp = context.experimental || {}; const initialCtx = { isStandalone: exp.standalone, ...context, }; if (context.startTime) { const ctx = { ...initialCtx }; ctx.startTimestamp = spanTimeInputToSeconds(context.startTime); delete ctx.startTime; return ctx; } return initialCtx; } function getAcs() { const carrier = getMainCarrier(); return getAsyncContextStrategy(carrier); } function _startRootSpan(spanArguments, scope, parentSampled) { const client = getClient(); const options = (client && client.getOptions()) || {}; const { name = '', attributes } = spanArguments; const [sampled, sampleRate] = scope.getScopeData().sdkProcessingMetadata[SUPPRESS_TRACING_KEY] ? [false] : sampleSpan(options, { name, parentSampled, attributes, transactionContext: { name, parentSampled, }, }); const rootSpan = new SentrySpan({ ...spanArguments, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', ...spanArguments.attributes, }, sampled, }); if (sampleRate !== undefined) { rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); } if (client) { client.emit('spanStart', rootSpan); } return rootSpan; } /** * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * This inherits the sampling decision from the parent span. */ function _startChildSpan(parentSpan, scope, spanArguments) { const { spanId, traceId } = parentSpan.spanContext(); const sampled = scope.getScopeData().sdkProcessingMetadata[SUPPRESS_TRACING_KEY] ? false : spanIsSampled(parentSpan); const childSpan = sampled ? new SentrySpan({ ...spanArguments, parentSpanId: spanId, traceId, sampled, }) : new SentryNonRecordingSpan({ traceId }); addChildSpanToSpan(parentSpan, childSpan); const client = getClient(); if (client) { client.emit('spanStart', childSpan); // If it has an endTimestamp, it's already ended if (spanArguments.endTimestamp) { client.emit('spanEnd', childSpan); } } return childSpan; } function getParentSpan(scope) { const span = _getSpanForScope(scope) ; if (!span) { return undefined; } const client = getClient(); const options = client ? client.getOptions() : {}; if (options.parentSpanIsAlwaysRootSpan) { return getRootSpan(span) ; } return span; } export { continueTrace, startInactiveSpan, startNewTrace, startSpan, startSpanManual, suppressTracing, withActiveSpan }; //# sourceMappingURL=trace.js.map