Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/core/src/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,22 @@ function getTypedAttributeValue(value: unknown): TypedAttributeValue | void {
return { value, type: primitiveType };
}
}

/**
* Sets a raw attribute if the value exists and the attribute key is not already present.
*
* @param attributes - The 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.
*/
export function safeSetAttribute(
attributes: Record<string, unknown>,
key: string,
value: unknown,
setEvenIfPresent = true,
): void {
if (value && (setEvenIfPresent || !(key in attributes))) {
attributes[key] = value;
}
}
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export {
_INTERNAL_shouldSkipAiProviderWrapping,
_INTERNAL_clearAiProviderSkips,
} from './utils/ai/providerSkip';
export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent';
export { applyScopeDataToEvent, mergeScopeData } from './utils/scope-utils';
export { prepareEvent } from './utils/prepareEvent';
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
export { createCheckInEnvelope } from './checkin';
Expand Down
62 changes: 14 additions & 48 deletions packages/core/src/logs/internal.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { serializeAttributes } from '../attributes';
import { safeSetAttribute, serializeAttributes } from '../attributes';
import { getGlobalSingleton } from '../carrier';
import type { Client } from '../client';
import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '../currentScopes';
import { getClient, getCurrentScope } from '../currentScopes';
import { DEBUG_BUILD } from '../debug-build';
import type { Scope, ScopeData } from '../scope';
import type { Integration } from '../types-hoist/integration';
import type { Log, SerializedLog } from '../types-hoist/log';
import { mergeScopeData } from '../utils/applyScopeDataToEvent';
import { consoleSandbox, debug } from '../utils/debug-logger';
import { isParameterizedString } from '../utils/is';
import { getFinalScopeData } from '../utils/scope-utils';
import { _getSpanForScope } from '../utils/spanOnScope';
import { timestampInSeconds } from '../utils/time';
import { _getTraceInfoFromScope } from '../utils/trace-info';
Expand All @@ -17,25 +16,6 @@ import { createLogEnvelope } from './envelope';

const MAX_LOG_BUFFER_SIZE = 100;

/**
* Sets a log attribute if the value exists and the attribute key is not already present.
*
* @param logAttributes - The log 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 setLogAttribute(
logAttributes: Record<string, unknown>,
key: string,
value: unknown,
setEvenIfPresent = true,
): void {
if (value && (!logAttributes[key] || setEvenIfPresent)) {
logAttributes[key] = value;
}
}

/**
* Captures a serialized log event and adds it to the log buffer for the given client.
*
Expand Down Expand Up @@ -98,18 +78,18 @@ export function _INTERNAL_captureLog(
const {
user: { id, email, username },
attributes: scopeAttributes = {},
} = getMergedScopeData(currentScope);
} = getFinalScopeData(currentScope);

setLogAttribute(processedLogAttributes, 'user.id', id, false);
setLogAttribute(processedLogAttributes, 'user.email', email, false);
setLogAttribute(processedLogAttributes, 'user.name', username, false);
safeSetAttribute(processedLogAttributes, 'user.id', id, false);
safeSetAttribute(processedLogAttributes, 'user.email', email, false);
safeSetAttribute(processedLogAttributes, 'user.name', username, false);

setLogAttribute(processedLogAttributes, 'sentry.release', release);
setLogAttribute(processedLogAttributes, 'sentry.environment', environment);
safeSetAttribute(processedLogAttributes, 'sentry.release', release);
safeSetAttribute(processedLogAttributes, 'sentry.environment', environment);

const { name, version } = client.getSdkMetadata()?.sdk ?? {};
setLogAttribute(processedLogAttributes, 'sentry.sdk.name', name);
setLogAttribute(processedLogAttributes, 'sentry.sdk.version', version);
safeSetAttribute(processedLogAttributes, 'sentry.sdk.name', name);
safeSetAttribute(processedLogAttributes, 'sentry.sdk.version', version);

const replay = client.getIntegrationByName<
Integration & {
Expand All @@ -119,11 +99,11 @@ export function _INTERNAL_captureLog(
>('Replay');

const replayId = replay?.getReplayId(true);
setLogAttribute(processedLogAttributes, 'sentry.replay_id', replayId);
safeSetAttribute(processedLogAttributes, 'sentry.replay_id', replayId);

if (replayId && replay?.getRecordingMode() === 'buffer') {
// We send this so we can identify cases where the replayId is attached but the replay itself might not have been sent to Sentry
setLogAttribute(processedLogAttributes, 'sentry._internal.replay_is_buffering', true);
safeSetAttribute(processedLogAttributes, 'sentry._internal.replay_is_buffering', true);
}

const beforeLogMessage = beforeLog.message;
Expand All @@ -139,7 +119,7 @@ export function _INTERNAL_captureLog(

const span = _getSpanForScope(currentScope);
// Add the parent span ID to the log attributes for trace context
setLogAttribute(processedLogAttributes, 'sentry.trace.parent_span_id', span?.spanContext().spanId);
safeSetAttribute(processedLogAttributes, 'sentry.trace.parent_span_id', span?.spanContext().spanId);

const processedLog = { ...beforeLog, attributes: processedLogAttributes };

Expand Down Expand Up @@ -212,20 +192,6 @@ export function _INTERNAL_getLogBuffer(client: Client): Array<SerializedLog> | u
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: Scope): ScopeData {
const scopeData = getGlobalScope().getScopeData();
mergeScopeData(scopeData, getIsolationScope().getScopeData());
mergeScopeData(scopeData, currentScope.getScopeData());
return scopeData;
}

function _getBufferMap(): WeakMap<Client, Array<SerializedLog>> {
// The reference to the Client <> LogBuffer map is stored on the carrier to ensure it's always the same
return getGlobalSingleton('clientToLogBufferMap', () => new WeakMap<Client, Array<SerializedLog>>());
Expand Down
166 changes: 39 additions & 127 deletions packages/core/src/metrics/internal.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,21 @@
import { safeSetAttribute, serializeAttributes } from '../attributes';
import { getGlobalSingleton } from '../carrier';
import type { Client } from '../client';
import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '../currentScopes';
import { getClient, getCurrentScope } from '../currentScopes';
import { DEBUG_BUILD } from '../debug-build';
import type { Scope, ScopeData } from '../scope';
import type { Scope } from '../scope';
import type { Integration } from '../types-hoist/integration';
import type { Metric, SerializedMetric, SerializedMetricAttributeValue } from '../types-hoist/metric';
import { mergeScopeData } from '../utils/applyScopeDataToEvent';
import type { Metric, SerializedMetric } from '../types-hoist/metric';
import type { User } from '../types-hoist/user';
import { debug } from '../utils/debug-logger';
import { getFinalScopeData } from '../utils/scope-utils';
import { _getSpanForScope } from '../utils/spanOnScope';
import { timestampInSeconds } from '../utils/time';
import { _getTraceInfoFromScope } from '../utils/trace-info';
import { createMetricEnvelope } from './envelope';

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.
*/
export function metricAttributeToSerializedMetricAttribute(value: unknown): SerializedMetricAttributeValue {
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: Record<string, unknown>,
key: string,
value: unknown,
setEvenIfPresent = true,
): void {
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.
*
Expand Down Expand Up @@ -120,29 +59,26 @@ export interface InternalCaptureMetricOptions {
/**
* Enriches metric with all contextual attributes (user, SDK metadata, replay, etc.)
*/
function _enrichMetricAttributes(beforeMetric: Metric, client: Client, currentScope: Scope): Metric {
function _enrichMetricAttributes(beforeMetric: Metric, client: Client, user: User): Metric {
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);
const { id, email, username } = user;
safeSetAttribute(processedMetricAttributes, 'user.id', id, false);
safeSetAttribute(processedMetricAttributes, 'user.email', email, false);
safeSetAttribute(processedMetricAttributes, 'user.name', username, false);

// Add Sentry metadata
setMetricAttribute(processedMetricAttributes, 'sentry.release', release);
setMetricAttribute(processedMetricAttributes, 'sentry.environment', environment);
safeSetAttribute(processedMetricAttributes, 'sentry.release', release);
safeSetAttribute(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);
safeSetAttribute(processedMetricAttributes, 'sentry.sdk.name', name);
safeSetAttribute(processedMetricAttributes, 'sentry.sdk.version', version);

// Add replay metadata
const replay = client.getIntegrationByName<
Expand All @@ -153,10 +89,10 @@ function _enrichMetricAttributes(beforeMetric: Metric, client: Client, currentSc
>('Replay');

const replayId = replay?.getReplayId(true);
setMetricAttribute(processedMetricAttributes, 'sentry.replay_id', replayId);
safeSetAttribute(processedMetricAttributes, 'sentry.replay_id', replayId);

if (replayId && replay?.getRecordingMode() === 'buffer') {
setMetricAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true);
safeSetAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true);
}

return {
Expand All @@ -165,36 +101,6 @@ function _enrichMetricAttributes(beforeMetric: Metric, client: Client, currentSc
};
}

/**
* Creates a serialized metric ready to be sent to Sentry.
*/
function _buildSerializedMetric(metric: Metric, client: Client, currentScope: Scope): SerializedMetric {
// Serialize attributes
const serializedAttributes: Record<string, SerializedMetricAttributeValue> = {};
for (const key in metric.attributes) {
if (metric.attributes[key] !== undefined) {
serializedAttributes[key] = metricAttributeToSerializedMetricAttribute(metric.attributes[key]);
}
}

// Get trace context
const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
const span = _getSpanForScope(currentScope);
const traceId = span ? span.spanContext().traceId : traceContext?.trace_id;
const spanId = span ? span.spanContext().spanId : undefined;

return {
timestamp: 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.
*
Expand Down Expand Up @@ -224,8 +130,10 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal
return;
}

const { user, attributes: scopeAttributes } = getFinalScopeData(currentScope);

// Enrich metric with contextual attributes
const enrichedMetric = _enrichMetricAttributes(beforeMetric, client, currentScope);
const enrichedMetric = _enrichMetricAttributes(beforeMetric, client, user);

client.emit('processMetric', enrichedMetric);

Expand All @@ -239,7 +147,25 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal
return;
}

const serializedMetric = _buildSerializedMetric(processedMetric, client, currentScope);
const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
const span = _getSpanForScope(currentScope);
const traceId = span ? span.spanContext().traceId : traceContext?.trace_id;
const spanId = span ? span.spanContext().spanId : undefined;

const { name, type, unit, value, attributes: metricAttributes } = processedMetric;
const serializedMetric = {
timestamp: timestampInSeconds(),
trace_id: traceId ?? '',
span_id: spanId,
name,
type,
unit,
value,
attributes: {
...serializeAttributes(metricAttributes, true),
...serializeAttributes(scopeAttributes),
},
};

DEBUG_BUILD && debug.log('[Metric]', serializedMetric);

Expand Down Expand Up @@ -288,20 +214,6 @@ export function _INTERNAL_getMetricBuffer(client: Client): Array<SerializedMetri
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: Scope): ScopeData {
const scopeData = getGlobalScope().getScopeData();
mergeScopeData(scopeData, getIsolationScope().getScopeData());
mergeScopeData(scopeData, currentScope.getScopeData());
return scopeData;
}

function _getBufferMap(): WeakMap<Client, Array<SerializedMetric>> {
// The reference to the Client <> MetricBuffer map is stored on the carrier to ensure it's always the same
return getGlobalSingleton('clientToMetricBufferMap', () => new WeakMap<Client, Array<SerializedMetric>>());
Expand Down
Loading
Loading