package.build.esm.tracing.sentrySpan.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core Show documentation
Show all versions of core Show documentation
Base implementation for all Sentry JavaScript SDKs
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';
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 || 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();
}
}
/**
* This should generally not be used,
* but it is needed for being compliant with the OTEL Span interface.
*
* @hidden
* @internal
*/
addLink(_link) {
return this;
}
/**
* This should generally not be used,
* but it is needed for being compliant with the OTEL Span interface.
*
* @hidden
* @internal
*/
addLinks(_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 ? 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;
}
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 = 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) {
if (this._sampled) {
sendSpanEnvelope(createSpanEnvelope([this], client));
} else {
DEBUG_BUILD &&
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 = 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 ``.');
this._name = '';
}
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:
// 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,
...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 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 = 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