@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
352 lines (295 loc) • 10.6 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
const utils = require('@sentry/utils');
const currentScopes = require('../currentScopes.js');
const debugBuild = require('../debug-build.js');
const envelope = require('../envelope.js');
const metricSummary = require('../metrics/metric-summary.js');
const semanticAttributes = require('../semanticAttributes.js');
const spanUtils = require('../utils/spanUtils.js');
const dynamicSamplingContext = require('./dynamicSamplingContext.js');
const logSpans = require('./logSpans.js');
const measurement = require('./measurement.js');
const utils$1 = require('./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 || utils.uuid4();
this._spanId = spanContext.spanId || utils.uuid4().substring(16);
this._startTime = spanContext.startTimestamp || utils.timestampInSeconds();
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 */
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;
}
}
/** @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 = spanUtils.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 = 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 utils.dropUndefinedKeys({
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] ,
_metrics_summary: metricSummary.getMetricSummaryJsonForSpan(this),
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,
});
}
/** @inheritdoc */
isRecording() {
return !this._endTime && !!this._sampled;
}
/**
* @inheritdoc
*/
addEvent(
name,
attributesOrStartTime,
startTime,
) {
debugBuild.DEBUG_BUILD && utils.logger.log('[Tracing] Adding an event to span:', name);
const time = isSpanTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime || utils.timestampInSeconds();
const attributes = isSpanTimeInput(attributesOrStartTime) ? {} : attributesOrStartTime || {};
const event = {
name,
time: spanUtils.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 = 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) {
sendSpanEnvelope(envelope.createSpanEnvelope([this], client));
return;
}
const transactionEvent = this._convertSpanToTransaction();
if (transactionEvent) {
const scope = utils$1.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 && utils.logger.warn('Transaction has no name, falling back to `<unlabeled transaction>`.');
this._name = '<unlabeled transaction>';
}
const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = utils$1.getCapturedScopesOnSpan(this);
const scope = capturedSpanScope || currentScopes.getCurrentScope();
const client = scope.getClient() || currentScopes.getClient();
if (this._sampled !== true) {
// At this point if `sampled !== true` we want to discard the transaction.
debugBuild.DEBUG_BUILD && utils.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 = 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] ;
const transaction = {
contexts: {
trace: spanUtils.spanToTransactionTraceContext(this),
},
spans,
start_timestamp: this._startTime,
timestamp: this._endTime,
transaction: this._name,
type: 'transaction',
sdkProcessingMetadata: {
capturedSpanScope,
capturedSpanIsolationScope,
...utils.dropUndefinedKeys({
dynamicSamplingContext: dynamicSamplingContext.getDynamicSamplingContextFromSpan(this),
}),
},
_metrics_summary: metricSummary.getMetricSummaryJsonForSpan(this),
...(source && {
transaction_info: {
source,
},
}),
};
const measurements = measurement.timedEventsToMeasurements(this._events);
const hasMeasurements = measurements && Object.keys(measurements).length;
if (hasMeasurements) {
debugBuild.DEBUG_BUILD &&
utils.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 = currentScopes.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 => {
debugBuild.DEBUG_BUILD && utils.logger.error('Error while sending span:', reason);
});
}
}
exports.SentrySpan = SentrySpan;
//# sourceMappingURL=sentrySpan.js.map