@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
350 lines (294 loc) • 10.4 kB
JavaScript
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