UNPKG

@sentry/core

Version:
350 lines (294 loc) 10.4 kB
import { uuid4, timestampInSeconds, dropUndefinedKeys, logger } from '@sentry/utils'; import { getClient, getCurrentScope } from '../currentScopes.js'; import { DEBUG_BUILD } from '../debug-build.js'; import { createSpanEnvelope } from '../envelope.js'; import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary.js'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_PROFILE_ID, SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes.js'; import { TRACE_FLAG_SAMPLED, TRACE_FLAG_NONE, spanTimeInputToSeconds, getStatusMessage, getRootSpan, spanToJSON, getSpanDescendants, spanToTransactionTraceContext } from '../utils/spanUtils.js'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext.js'; import { logSpanEnd } from './logSpans.js'; import { timedEventsToMeasurements } from './measurement.js'; import { getCapturedScopesOnSpan } from './utils.js'; /** * Span contains all data about a span */ class SentrySpan { /** Epoch timestamp in seconds when the span started. */ /** Epoch timestamp in seconds when the span ended. */ /** Internal keeper of the status */ /** The timed events added to this span. */ /** if true, treat span as a standalone span (not part of a transaction) */ /** * You should never call the constructor manually, always use `Sentry.startSpan()` * or other span methods. * @internal * @hideconstructor * @hidden */ constructor(spanContext = {}) { this._traceId = spanContext.traceId || uuid4(); this._spanId = spanContext.spanId || uuid4().substring(16); this._startTime = spanContext.startTimestamp || timestampInSeconds(); this._attributes = {}; this.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: spanContext.op, ...spanContext.attributes, }); this._name = spanContext.name; if (spanContext.parentSpanId) { this._parentSpanId = spanContext.parentSpanId; } // We want to include booleans as well here if ('sampled' in spanContext) { this._sampled = spanContext.sampled; } if (spanContext.endTimestamp) { this._endTime = spanContext.endTimestamp; } this._events = []; this._isStandaloneSpan = spanContext.isStandalone; // If the span is already ended, ensure we finalize the span immediately if (this._endTime) { this._onSpanEnded(); } } /** @inheritdoc */ spanContext() { const { _spanId: spanId, _traceId: traceId, _sampled: sampled } = this; return { spanId, traceId, traceFlags: sampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE, }; } /** @inheritdoc */ setAttribute(key, value) { if (value === undefined) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this._attributes[key]; } else { this._attributes[key] = value; } } /** @inheritdoc */ setAttributes(attributes) { Object.keys(attributes).forEach(key => this.setAttribute(key, attributes[key])); } /** * This should generally not be used, * but we need it for browser tracing where we want to adjust the start time afterwards. * USE THIS WITH CAUTION! * * @hidden * @internal */ updateStartTime(timeInput) { this._startTime = spanTimeInputToSeconds(timeInput); } /** * @inheritDoc */ setStatus(value) { this._status = value; return this; } /** * @inheritDoc */ updateName(name) { this._name = name; return this; } /** @inheritdoc */ end(endTimestamp) { // If already ended, skip if (this._endTime) { return; } this._endTime = spanTimeInputToSeconds(endTimestamp); logSpanEnd(this); this._onSpanEnded(); } /** * Get JSON representation of this span. * * @hidden * @internal This method is purely for internal purposes and should not be used outside * of SDK code. If you need to get a JSON representation of a span, * use `spanToJSON(span)` instead. */ getSpanJSON() { return dropUndefinedKeys({ data: this._attributes, description: this._name, op: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], parent_span_id: this._parentSpanId, span_id: this._spanId, start_timestamp: this._startTime, status: getStatusMessage(this._status), timestamp: this._endTime, trace_id: this._traceId, origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] , _metrics_summary: getMetricSummaryJsonForSpan(this), profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] , exclusive_time: this._attributes[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] , measurements: timedEventsToMeasurements(this._events), is_segment: (this._isStandaloneSpan && getRootSpan(this) === this) || undefined, segment_id: this._isStandaloneSpan ? getRootSpan(this).spanContext().spanId : undefined, }); } /** @inheritdoc */ isRecording() { return !this._endTime && !!this._sampled; } /** * @inheritdoc */ addEvent( name, attributesOrStartTime, startTime, ) { DEBUG_BUILD && logger.log('[Tracing] Adding an event to span:', name); const time = isSpanTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime || timestampInSeconds(); const attributes = isSpanTimeInput(attributesOrStartTime) ? {} : attributesOrStartTime || {}; const event = { name, time: spanTimeInputToSeconds(time), attributes, }; this._events.push(event); return this; } /** * This method should generally not be used, * but for now we need a way to publicly check if the `_isStandaloneSpan` flag is set. * USE THIS WITH CAUTION! * @internal * @hidden * @experimental */ isStandaloneSpan() { return !!this._isStandaloneSpan; } /** Emit `spanEnd` when the span is ended. */ _onSpanEnded() { const client = getClient(); if (client) { client.emit('spanEnd', this); } // A segment span is basically the root span of a local span tree. // So for now, this is either what we previously refer to as the root span, // or a standalone span. const isSegmentSpan = this._isStandaloneSpan || this === getRootSpan(this); if (!isSegmentSpan) { return; } // if this is a standalone span, we send it immediately if (this._isStandaloneSpan) { sendSpanEnvelope(createSpanEnvelope([this], client)); return; } const transactionEvent = this._convertSpanToTransaction(); if (transactionEvent) { const scope = getCapturedScopesOnSpan(this).scope || getCurrentScope(); scope.captureEvent(transactionEvent); } } /** * Finish the transaction & prepare the event to send to Sentry. */ _convertSpanToTransaction() { // We can only convert finished spans if (!isFullFinishedSpan(spanToJSON(this))) { return undefined; } if (!this._name) { DEBUG_BUILD && logger.warn('Transaction has no name, falling back to `<unlabeled transaction>`.'); this._name = '<unlabeled transaction>'; } const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this); const scope = capturedSpanScope || getCurrentScope(); const client = scope.getClient() || getClient(); if (this._sampled !== true) { // At this point if `sampled !== true` we want to discard the transaction. DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); if (client) { client.recordDroppedEvent('sample_rate', 'transaction'); } return undefined; } // The transaction span itself as well as any potential standalone spans should be filtered out const finishedSpans = getSpanDescendants(this).filter(span => span !== this && !isStandaloneSpan(span)); const spans = finishedSpans.map(span => spanToJSON(span)).filter(isFullFinishedSpan); const source = this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] ; const transaction = { contexts: { trace: spanToTransactionTraceContext(this), }, spans, start_timestamp: this._startTime, timestamp: this._endTime, transaction: this._name, type: 'transaction', sdkProcessingMetadata: { capturedSpanScope, capturedSpanIsolationScope, ...dropUndefinedKeys({ dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }), }, _metrics_summary: getMetricSummaryJsonForSpan(this), ...(source && { transaction_info: { source, }, }), }; const measurements = timedEventsToMeasurements(this._events); const hasMeasurements = measurements && Object.keys(measurements).length; if (hasMeasurements) { DEBUG_BUILD && logger.log('[Measurements] Adding measurements to transaction', JSON.stringify(measurements, undefined, 2)); transaction.measurements = measurements; } return transaction; } } function isSpanTimeInput(value) { return (value && typeof value === 'number') || value instanceof Date || Array.isArray(value); } // We want to filter out any incomplete SpanJSON objects function isFullFinishedSpan(input) { return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id; } /** `SentrySpan`s can be sent as a standalone span rather than belonging to a transaction */ function isStandaloneSpan(span) { return span instanceof SentrySpan && span.isStandaloneSpan(); } /** * Sends a `SpanEnvelope`. * * Note: If the envelope's spans are dropped, e.g. via `beforeSendSpan`, * the envelope will not be sent either. */ function sendSpanEnvelope(envelope) { const client = getClient(); if (!client) { return; } const spanItems = envelope[1]; if (!spanItems || spanItems.length === 0) { client.recordDroppedEvent('before_send', 'span'); return; } const transport = client.getTransport(); if (transport) { transport.send(envelope).then(null, reason => { DEBUG_BUILD && logger.error('Error while sending span:', reason); }); } } export { SentrySpan }; //# sourceMappingURL=sentrySpan.js.map