@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
300 lines (258 loc) • 10 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const carrier = require('../carrier.js');
const currentScopes = require('../currentScopes.js');
const debugBuild = require('../debug-build.js');
const applyScopeDataToEvent = require('../utils/applyScopeDataToEvent.js');
const debugLogger = require('../utils/debug-logger.js');
const spanOnScope = require('../utils/spanOnScope.js');
const time = require('../utils/time.js');
const traceInfo = require('../utils/trace-info.js');
const envelope = require('./envelope.js');
const MAX_METRIC_BUFFER_SIZE = 1000;
/**
* Converts a metric attribute to a serialized metric attribute.
*
* @param value - The value of the metric attribute.
* @returns The serialized metric attribute.
*/
function metricAttributeToSerializedMetricAttribute(value) {
switch (typeof value) {
case 'number':
if (Number.isInteger(value)) {
return {
value,
type: 'integer',
};
}
return {
value,
type: 'double',
};
case 'boolean':
return {
value,
type: 'boolean',
};
case 'string':
return {
value,
type: 'string',
};
default: {
let stringValue = '';
try {
stringValue = JSON.stringify(value) ?? '';
} catch {
// Do nothing
}
return {
value: stringValue,
type: 'string',
};
}
}
}
/**
* Sets a metric attribute if the value exists and the attribute key is not already present.
*
* @param metricAttributes - The metric attributes object to modify.
* @param key - The attribute key to set.
* @param value - The value to set (only sets if truthy and key not present).
* @param setEvenIfPresent - Whether to set the attribute if it is present. Defaults to true.
*/
function setMetricAttribute(
metricAttributes,
key,
value,
setEvenIfPresent = true,
) {
if (value && (setEvenIfPresent || !(key in metricAttributes))) {
metricAttributes[key] = value;
}
}
/**
* Captures a serialized metric event and adds it to the metric buffer for the given client.
*
* @param client - A client. Uses the current client if not provided.
* @param serializedMetric - The serialized metric event to capture.
*
* @experimental This method will experience breaking changes. This is not yet part of
* the stable Sentry SDK API and can be changed or removed without warning.
*/
function _INTERNAL_captureSerializedMetric(client, serializedMetric) {
const bufferMap = _getBufferMap();
const metricBuffer = _INTERNAL_getMetricBuffer(client);
if (metricBuffer === undefined) {
bufferMap.set(client, [serializedMetric]);
} else {
if (metricBuffer.length >= MAX_METRIC_BUFFER_SIZE) {
_INTERNAL_flushMetricsBuffer(client, metricBuffer);
bufferMap.set(client, [serializedMetric]);
} else {
bufferMap.set(client, [...metricBuffer, serializedMetric]);
}
}
}
/**
* Options for capturing a metric internally.
*/
/**
* Enriches metric with all contextual attributes (user, SDK metadata, replay, etc.)
*/
function _enrichMetricAttributes(beforeMetric, client, currentScope) {
const { release, environment } = client.getOptions();
const processedMetricAttributes = {
...beforeMetric.attributes,
};
// Add user attributes
const {
user: { id, email, username },
} = getMergedScopeData(currentScope);
setMetricAttribute(processedMetricAttributes, 'user.id', id, false);
setMetricAttribute(processedMetricAttributes, 'user.email', email, false);
setMetricAttribute(processedMetricAttributes, 'user.name', username, false);
// Add Sentry metadata
setMetricAttribute(processedMetricAttributes, 'sentry.release', release);
setMetricAttribute(processedMetricAttributes, 'sentry.environment', environment);
// Add SDK metadata
const { name, version } = client.getSdkMetadata()?.sdk ?? {};
setMetricAttribute(processedMetricAttributes, 'sentry.sdk.name', name);
setMetricAttribute(processedMetricAttributes, 'sentry.sdk.version', version);
// Add replay metadata
const replay = client.getIntegrationByName
('Replay');
const replayId = replay?.getReplayId(true);
setMetricAttribute(processedMetricAttributes, 'sentry.replay_id', replayId);
if (replayId && replay?.getRecordingMode() === 'buffer') {
setMetricAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true);
}
return {
...beforeMetric,
attributes: processedMetricAttributes,
};
}
/**
* Creates a serialized metric ready to be sent to Sentry.
*/
function _buildSerializedMetric(metric, client, currentScope) {
// Serialize attributes
const serializedAttributes = {};
for (const key in metric.attributes) {
if (metric.attributes[key] !== undefined) {
serializedAttributes[key] = metricAttributeToSerializedMetricAttribute(metric.attributes[key]);
}
}
// Get trace context
const [, traceContext] = traceInfo._getTraceInfoFromScope(client, currentScope);
const span = spanOnScope._getSpanForScope(currentScope);
const traceId = span ? span.spanContext().traceId : traceContext?.trace_id;
const spanId = span ? span.spanContext().spanId : undefined;
return {
timestamp: time.timestampInSeconds(),
trace_id: traceId ?? '',
span_id: spanId,
name: metric.name,
type: metric.type,
unit: metric.unit,
value: metric.value,
attributes: serializedAttributes,
};
}
/**
* Captures a metric event and sends it to Sentry.
*
* @param metric - The metric event to capture.
* @param options - Options for capturing the metric.
*
* @experimental This method will experience breaking changes. This is not yet part of
* the stable Sentry SDK API and can be changed or removed without warning.
*/
function _INTERNAL_captureMetric(beforeMetric, options) {
const currentScope = options?.scope ?? currentScopes.getCurrentScope();
const captureSerializedMetric = options?.captureSerializedMetric ?? _INTERNAL_captureSerializedMetric;
const client = currentScope?.getClient() ?? currentScopes.getClient();
if (!client) {
debugBuild.DEBUG_BUILD && debugLogger.debug.warn('No client available to capture metric.');
return;
}
const { _experiments, enableMetrics, beforeSendMetric } = client.getOptions();
// todo(v11): Remove the experimental flag
// eslint-disable-next-line deprecation/deprecation
const metricsEnabled = enableMetrics ?? _experiments?.enableMetrics ?? true;
if (!metricsEnabled) {
debugBuild.DEBUG_BUILD && debugLogger.debug.warn('metrics option not enabled, metric will not be captured.');
return;
}
// Enrich metric with contextual attributes
const enrichedMetric = _enrichMetricAttributes(beforeMetric, client, currentScope);
client.emit('processMetric', enrichedMetric);
// todo(v11): Remove the experimental `beforeSendMetric`
// eslint-disable-next-line deprecation/deprecation
const beforeSendCallback = beforeSendMetric || _experiments?.beforeSendMetric;
const processedMetric = beforeSendCallback ? beforeSendCallback(enrichedMetric) : enrichedMetric;
if (!processedMetric) {
debugBuild.DEBUG_BUILD && debugLogger.debug.log('`beforeSendMetric` returned `null`, will not send metric.');
return;
}
const serializedMetric = _buildSerializedMetric(processedMetric, client, currentScope);
debugBuild.DEBUG_BUILD && debugLogger.debug.log('[Metric]', serializedMetric);
captureSerializedMetric(client, serializedMetric);
client.emit('afterCaptureMetric', processedMetric);
}
/**
* Flushes the metrics buffer to Sentry.
*
* @param client - A client.
* @param maybeMetricBuffer - A metric buffer. Uses the metric buffer for the given client if not provided.
*
* @experimental This method will experience breaking changes. This is not yet part of
* the stable Sentry SDK API and can be changed or removed without warning.
*/
function _INTERNAL_flushMetricsBuffer(client, maybeMetricBuffer) {
const metricBuffer = maybeMetricBuffer ?? _INTERNAL_getMetricBuffer(client) ?? [];
if (metricBuffer.length === 0) {
return;
}
const clientOptions = client.getOptions();
const envelope$1 = envelope.createMetricEnvelope(metricBuffer, clientOptions._metadata, clientOptions.tunnel, client.getDsn());
// Clear the metric buffer after envelopes have been constructed.
_getBufferMap().set(client, []);
client.emit('flushMetrics');
// sendEnvelope should not throw
// eslint-disable-next-line @typescript-eslint/no-floating-promises
client.sendEnvelope(envelope$1);
}
/**
* Returns the metric buffer for a given client.
*
* Exported for testing purposes.
*
* @param client - The client to get the metric buffer for.
* @returns The metric buffer for the given client.
*/
function _INTERNAL_getMetricBuffer(client) {
return _getBufferMap().get(client);
}
/**
* Get the scope data for the current scope after merging with the
* global scope and isolation scope.
*
* @param currentScope - The current scope.
* @returns The scope data.
*/
function getMergedScopeData(currentScope) {
const scopeData = currentScopes.getGlobalScope().getScopeData();
applyScopeDataToEvent.mergeScopeData(scopeData, currentScopes.getIsolationScope().getScopeData());
applyScopeDataToEvent.mergeScopeData(scopeData, currentScope.getScopeData());
return scopeData;
}
function _getBufferMap() {
// The reference to the Client <> MetricBuffer map is stored on the carrier to ensure it's always the same
return carrier.getGlobalSingleton('clientToMetricBufferMap', () => new WeakMap());
}
exports._INTERNAL_captureMetric = _INTERNAL_captureMetric;
exports._INTERNAL_captureSerializedMetric = _INTERNAL_captureSerializedMetric;
exports._INTERNAL_flushMetricsBuffer = _INTERNAL_flushMetricsBuffer;
exports._INTERNAL_getMetricBuffer = _INTERNAL_getMetricBuffer;
exports.metricAttributeToSerializedMetricAttribute = metricAttributeToSerializedMetricAttribute;
//# sourceMappingURL=internal.js.map