diff --git a/packages/core/src/attributes.ts b/packages/core/src/attributes.ts index f33241be2a1e..a5d58f57870c 100644 --- a/packages/core/src/attributes.ts +++ b/packages/core/src/attributes.ts @@ -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, + key: string, + value: unknown, + setEvenIfPresent = true, +): void { + if (value && (setEvenIfPresent || !(key in attributes))) { + attributes[key] = value; + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e18ea294f182..b9825ff44796 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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'; diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index a39aa75d7074..114f2e27588b 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -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'; @@ -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, - 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. * @@ -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 & { @@ -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; @@ -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 }; @@ -212,20 +192,6 @@ export function _INTERNAL_getLogBuffer(client: Client): Array | 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> { // The reference to the Client <> LogBuffer map is stored on the carrier to ensure it's always the same return getGlobalSingleton('clientToLogBufferMap', () => new WeakMap>()); diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index 7ac1372d1285..e946ef6519c5 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -1,12 +1,14 @@ +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'; @@ -14,69 +16,6 @@ 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, - 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. * @@ -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< @@ -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 { @@ -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 = {}; - 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. * @@ -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); @@ -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); @@ -288,20 +214,6 @@ export function _INTERNAL_getMetricBuffer(client: Client): Array> { // The reference to the Client <> MetricBuffer map is stored on the carrier to ensure it's always the same return getGlobalSingleton('clientToMetricBufferMap', () => new WeakMap>()); diff --git a/packages/core/src/types-hoist/metric.ts b/packages/core/src/types-hoist/metric.ts index 6ac63da6032b..8f07fa3b7c83 100644 --- a/packages/core/src/types-hoist/metric.ts +++ b/packages/core/src/types-hoist/metric.ts @@ -1,3 +1,5 @@ +import type { Attributes } from '../attributes'; + export type MetricType = 'counter' | 'gauge' | 'distribution'; export interface Metric { @@ -27,11 +29,11 @@ export interface Metric { attributes?: Record; } -export type SerializedMetricAttributeValue = - | { value: string; type: 'string' } - | { value: number; type: 'integer' } - | { value: number; type: 'double' } - | { value: boolean; type: 'boolean' }; +/** + * @deprecated This type was not intended for public export and you shouldn't depend on it. + * If you absolutely need to use it, use `SerializedMetricAttributeValue['attributes'] instead. + */ +export type SerializedMetricAttributeValue = Attributes; export interface SerializedMetric { /** @@ -72,7 +74,7 @@ export interface SerializedMetric { /** * Arbitrary structured data that stores information about the metric. */ - attributes?: Record; + attributes?: Attributes; } export type SerializedMetricContainer = { diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index fd1cb62440f4..111ed9e74525 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -7,7 +7,7 @@ import { Scope } from '../scope'; import type { Event, EventHint } from '../types-hoist/event'; import type { ClientOptions } from '../types-hoist/options'; import type { StackParser } from '../types-hoist/stacktrace'; -import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent'; +import { applyScopeDataToEvent, mergeScopeData } from './scope-utils'; import { getFilenameToDebugIdMap } from './debug-ids'; import { addExceptionMechanism, uuid4 } from './misc'; import { normalize } from './normalize'; diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/scope-utils.ts similarity index 91% rename from packages/core/src/utils/applyScopeDataToEvent.ts rename to packages/core/src/utils/scope-utils.ts index 3770c41977dc..2235e05ac8d2 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/scope-utils.ts @@ -1,4 +1,5 @@ -import type { ScopeData } from '../scope'; +import { getGlobalScope, getIsolationScope } from '../currentScopes'; +import type { Scope, ScopeData } from '../scope'; import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext'; import type { Breadcrumb } from '../types-hoist/breadcrumb'; import type { Event } from '../types-hoist/event'; @@ -194,3 +195,17 @@ function applyFingerprintToEvent(event: Event, fingerprint: ScopeData['fingerpri delete event.fingerprint; } } + +/** + * 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. + */ +export function getFinalScopeData(currentScope: Scope): ScopeData { + const scopeData = getGlobalScope().getScopeData(); + mergeScopeData(scopeData, getIsolationScope().getScopeData()); + mergeScopeData(scopeData, currentScope.getScopeData()); + return scopeData; +} diff --git a/packages/core/test/lib/metrics/internal.test.ts b/packages/core/test/lib/metrics/internal.test.ts index 3e479e282a0c..55753082d7ff 100644 --- a/packages/core/test/lib/metrics/internal.test.ts +++ b/packages/core/test/lib/metrics/internal.test.ts @@ -4,7 +4,6 @@ import { _INTERNAL_captureMetric, _INTERNAL_flushMetricsBuffer, _INTERNAL_getMetricBuffer, - metricAttributeToSerializedMetricAttribute, } from '../../../src/metrics/internal'; import type { Metric } from '../../../src/types-hoist/metric'; import * as loggerModule from '../../../src/utils/debug-logger'; @@ -12,74 +11,6 @@ import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const PUBLIC_DSN = 'https://username@domain/123'; -describe('metricAttributeToSerializedMetricAttribute', () => { - it('serializes integer values', () => { - const result = metricAttributeToSerializedMetricAttribute(42); - expect(result).toEqual({ - value: 42, - type: 'integer', - }); - }); - - it('serializes double values', () => { - const result = metricAttributeToSerializedMetricAttribute(42.34); - expect(result).toEqual({ - value: 42.34, - type: 'double', - }); - }); - - it('serializes boolean values', () => { - const result = metricAttributeToSerializedMetricAttribute(true); - expect(result).toEqual({ - value: true, - type: 'boolean', - }); - }); - - it('serializes string values', () => { - const result = metricAttributeToSerializedMetricAttribute('endpoint'); - expect(result).toEqual({ - value: 'endpoint', - type: 'string', - }); - }); - - it('serializes object values as JSON strings', () => { - const obj = { name: 'John', age: 30 }; - const result = metricAttributeToSerializedMetricAttribute(obj); - expect(result).toEqual({ - value: JSON.stringify(obj), - type: 'string', - }); - }); - - it('serializes array values as JSON strings', () => { - const array = [1, 2, 3, 'test']; - const result = metricAttributeToSerializedMetricAttribute(array); - expect(result).toEqual({ - value: JSON.stringify(array), - type: 'string', - }); - }); - - it('serializes undefined values as empty strings', () => { - const result = metricAttributeToSerializedMetricAttribute(undefined); - expect(result).toEqual({ - value: '', - type: 'string', - }); - }); - - it('serializes null values as JSON strings', () => { - const result = metricAttributeToSerializedMetricAttribute(null); - expect(result).toEqual({ - value: 'null', - type: 'string', - }); - }); -}); - describe('_INTERNAL_captureMetric', () => { it('captures and sends metrics', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 339a57828e5b..83c66116c96a 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -10,7 +10,7 @@ import { import { Scope } from '../../src/scope'; import type { Breadcrumb } from '../../src/types-hoist/breadcrumb'; import type { Event } from '../../src/types-hoist/event'; -import { applyScopeDataToEvent } from '../../src/utils/applyScopeDataToEvent'; +import { applyScopeDataToEvent } from '../../src/utils/scope-utils'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { clearGlobalScope } from '../testutils'; diff --git a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts index a23404eaf70f..758ac95ee9c0 100644 --- a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts +++ b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts @@ -10,7 +10,7 @@ import { mergeAndOverwriteScopeData, mergeArray, mergeScopeData, -} from '../../../src/utils/applyScopeDataToEvent'; +} from '../../../src/utils/scope-utils'; describe('mergeArray', () => { it.each([