UNPKG

@sentry/core

Version:
400 lines (339 loc) 11.9 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const currentScopes = require('../currentScopes.js'); const debugBuild = require('../debug-build.js'); const envelope = require('../envelope.js'); const semanticAttributes = require('../semanticAttributes.js'); const logger = require('../utils-hoist/logger.js'); const propagationContext = require('../utils-hoist/propagationContext.js'); const time = require('../utils-hoist/time.js'); const spanUtils = require('../utils/spanUtils.js'); const dynamicSamplingContext = require('./dynamicSamplingContext.js'); const logSpans = require('./logSpans.js'); const measurement = require('./measurement.js'); const utils = require('./utils.js'); const MAX_SPAN_COUNT = 1000; /** * 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 || propagationContext.generateTraceId(); this._spanId = spanContext.spanId || propagationContext.generateSpanId(); this._startTime = spanContext.startTimestamp || time.timestampInSeconds(); this._links = spanContext.links; this._attributes = {}; this.setAttributes({ [semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [semanticAttributes.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 */ addLink(link) { if (this._links) { this._links.push(link); } else { this._links = [link]; } return this; } /** @inheritDoc */ addLinks(links) { if (this._links) { this._links.push(...links); } else { this._links = links; } return this; } /** * This should generally not be used, * but it is needed for being compliant with the OTEL Span interface. * * @hidden * @internal */ recordException(_exception, _time) { // noop } /** @inheritdoc */ spanContext() { const { _spanId: spanId, _traceId: traceId, _sampled: sampled } = this; return { spanId, traceId, traceFlags: sampled ? spanUtils.TRACE_FLAG_SAMPLED : spanUtils.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; } return this; } /** @inheritdoc */ setAttributes(attributes) { Object.keys(attributes).forEach(key => this.setAttribute(key, attributes[key])); return this; } /** * 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 = spanUtils.spanTimeInputToSeconds(timeInput); } /** * @inheritDoc */ setStatus(value) { this._status = value; return this; } /** * @inheritDoc */ updateName(name) { this._name = name; this.setAttribute(semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); return this; } /** @inheritdoc */ end(endTimestamp) { // If already ended, skip if (this._endTime) { return; } this._endTime = spanUtils.spanTimeInputToSeconds(endTimestamp); logSpans.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 { data: this._attributes, description: this._name, op: this._attributes[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_OP], parent_span_id: this._parentSpanId, span_id: this._spanId, start_timestamp: this._startTime, status: spanUtils.getStatusMessage(this._status), timestamp: this._endTime, trace_id: this._traceId, origin: this._attributes[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] , profile_id: this._attributes[semanticAttributes.SEMANTIC_ATTRIBUTE_PROFILE_ID] , exclusive_time: this._attributes[semanticAttributes.SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] , measurements: measurement.timedEventsToMeasurements(this._events), is_segment: (this._isStandaloneSpan && spanUtils.getRootSpan(this) === this) || undefined, segment_id: this._isStandaloneSpan ? spanUtils.getRootSpan(this).spanContext().spanId : undefined, links: spanUtils.convertSpanLinksForEnvelope(this._links), }; } /** @inheritdoc */ isRecording() { return !this._endTime && !!this._sampled; } /** * @inheritdoc */ addEvent( name, attributesOrStartTime, startTime, ) { debugBuild.DEBUG_BUILD && logger.logger.log('[Tracing] Adding an event to span:', name); const time$1 = isSpanTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime || time.timestampInSeconds(); const attributes = isSpanTimeInput(attributesOrStartTime) ? {} : attributesOrStartTime || {}; const event = { name, time: spanUtils.spanTimeInputToSeconds(time$1), 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 = currentScopes.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 === spanUtils.getRootSpan(this); if (!isSegmentSpan) { return; } // if this is a standalone span, we send it immediately if (this._isStandaloneSpan) { if (this._sampled) { sendSpanEnvelope(envelope.createSpanEnvelope([this], client)); } else { debugBuild.DEBUG_BUILD && logger.logger.log('[Tracing] Discarding standalone span because its trace was not chosen to be sampled.'); if (client) { client.recordDroppedEvent('sample_rate', 'span'); } } return; } const transactionEvent = this._convertSpanToTransaction(); if (transactionEvent) { const scope = utils.getCapturedScopesOnSpan(this).scope || currentScopes.getCurrentScope(); scope.captureEvent(transactionEvent); } } /** * Finish the transaction & prepare the event to send to Sentry. */ _convertSpanToTransaction() { // We can only convert finished spans if (!isFullFinishedSpan(spanUtils.spanToJSON(this))) { return undefined; } if (!this._name) { debugBuild.DEBUG_BUILD && logger.logger.warn('Transaction has no name, falling back to `<unlabeled transaction>`.'); this._name = '<unlabeled transaction>'; } const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = utils.getCapturedScopesOnSpan(this); if (this._sampled !== true) { return undefined; } // The transaction span itself as well as any potential standalone spans should be filtered out const finishedSpans = spanUtils.getSpanDescendants(this).filter(span => span !== this && !isStandaloneSpan(span)); const spans = finishedSpans.map(span => spanUtils.spanToJSON(span)).filter(isFullFinishedSpan); const source = this._attributes[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] ; // remove internal root span attributes we don't need to send. /* eslint-disable @typescript-eslint/no-dynamic-delete */ delete this._attributes[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; spans.forEach(span => { delete span.data[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; }); // eslint-enabled-next-line @typescript-eslint/no-dynamic-delete const transaction = { contexts: { trace: spanUtils.spanToTransactionTraceContext(this), }, spans: // spans.sort() mutates the array, but `spans` is already a copy so we can safely do this here // we do not use spans anymore after this point spans.length > MAX_SPAN_COUNT ? spans.sort((a, b) => a.start_timestamp - b.start_timestamp).slice(0, MAX_SPAN_COUNT) : spans, start_timestamp: this._startTime, timestamp: this._endTime, transaction: this._name, type: 'transaction', sdkProcessingMetadata: { capturedSpanScope, capturedSpanIsolationScope, dynamicSamplingContext: dynamicSamplingContext.getDynamicSamplingContextFromSpan(this), }, ...(source && { transaction_info: { source, }, }), }; const measurements = measurement.timedEventsToMeasurements(this._events); const hasMeasurements = measurements && Object.keys(measurements).length; if (hasMeasurements) { debugBuild.DEBUG_BUILD && logger.logger.log( '[Measurements] Adding measurements to transaction event', 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 = currentScopes.getClient(); if (!client) { return; } const spanItems = envelope[1]; if (!spanItems || spanItems.length === 0) { client.recordDroppedEvent('before_send', 'span'); return; } // sendEnvelope should not throw // eslint-disable-next-line @typescript-eslint/no-floating-promises client.sendEnvelope(envelope); } exports.SentrySpan = SentrySpan; //# sourceMappingURL=sentrySpan.js.map