Skip to content

Commit 739f6ba

Browse files
committed
feat(core): Apply scope attributes to metrics
1 parent d1dd308 commit 739f6ba

File tree

8 files changed

+98
-253
lines changed

8 files changed

+98
-253
lines changed

packages/core/src/attributes.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,22 @@ function getTypedAttributeValue(value: unknown): TypedAttributeValue | void {
156156
return { value, type: primitiveType };
157157
}
158158
}
159+
160+
/**
161+
* Sets a raw attribute if the value exists and the attribute key is not already present.
162+
*
163+
* @param attributes - The attributes object to modify.
164+
* @param key - The attribute key to set.
165+
* @param value - The value to set (only sets if truthy and key not present).
166+
* @param setEvenIfPresent - Whether to set the attribute if it is present. Defaults to true.
167+
*/
168+
export function safeSetAttribute(
169+
attributes: Record<string, unknown>,
170+
key: string,
171+
value: unknown,
172+
setEvenIfPresent = true,
173+
): void {
174+
if (value && (setEvenIfPresent || !(key in attributes))) {
175+
attributes[key] = value;
176+
}
177+
}

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export {
6161
_INTERNAL_shouldSkipAiProviderWrapping,
6262
_INTERNAL_clearAiProviderSkips,
6363
} from './utils/ai/providerSkip';
64-
export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent';
64+
export { applyScopeDataToEvent, mergeScopeData } from './utils/scope-utils';
6565
export { prepareEvent } from './utils/prepareEvent';
6666
export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
6767
export { createCheckInEnvelope } from './checkin';

packages/core/src/logs/internal.ts

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { serializeAttributes } from '../attributes';
1+
import { safeSetAttribute, serializeAttributes } from '../attributes';
22
import { getGlobalSingleton } from '../carrier';
33
import type { Client } from '../client';
4-
import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '../currentScopes';
4+
import { getClient, getCurrentScope } from '../currentScopes';
55
import { DEBUG_BUILD } from '../debug-build';
6-
import type { Scope, ScopeData } from '../scope';
76
import type { Integration } from '../types-hoist/integration';
87
import type { Log, SerializedLog } from '../types-hoist/log';
9-
import { mergeScopeData } from '../utils/applyScopeDataToEvent';
108
import { consoleSandbox, debug } from '../utils/debug-logger';
119
import { isParameterizedString } from '../utils/is';
10+
import { getFinalScopeData } from '../utils/scope-utils';
1211
import { _getSpanForScope } from '../utils/spanOnScope';
1312
import { timestampInSeconds } from '../utils/time';
1413
import { _getTraceInfoFromScope } from '../utils/trace-info';
@@ -17,25 +16,6 @@ import { createLogEnvelope } from './envelope';
1716

1817
const MAX_LOG_BUFFER_SIZE = 100;
1918

20-
/**
21-
* Sets a log attribute if the value exists and the attribute key is not already present.
22-
*
23-
* @param logAttributes - The log attributes object to modify.
24-
* @param key - The attribute key to set.
25-
* @param value - The value to set (only sets if truthy and key not present).
26-
* @param setEvenIfPresent - Whether to set the attribute if it is present. Defaults to true.
27-
*/
28-
function setLogAttribute(
29-
logAttributes: Record<string, unknown>,
30-
key: string,
31-
value: unknown,
32-
setEvenIfPresent = true,
33-
): void {
34-
if (value && (!logAttributes[key] || setEvenIfPresent)) {
35-
logAttributes[key] = value;
36-
}
37-
}
38-
3919
/**
4020
* Captures a serialized log event and adds it to the log buffer for the given client.
4121
*
@@ -98,18 +78,18 @@ export function _INTERNAL_captureLog(
9878
const {
9979
user: { id, email, username },
10080
attributes: scopeAttributes = {},
101-
} = getMergedScopeData(currentScope);
81+
} = getFinalScopeData(currentScope);
10282

103-
setLogAttribute(processedLogAttributes, 'user.id', id, false);
104-
setLogAttribute(processedLogAttributes, 'user.email', email, false);
105-
setLogAttribute(processedLogAttributes, 'user.name', username, false);
83+
safeSetAttribute(processedLogAttributes, 'user.id', id, false);
84+
safeSetAttribute(processedLogAttributes, 'user.email', email, false);
85+
safeSetAttribute(processedLogAttributes, 'user.name', username, false);
10686

107-
setLogAttribute(processedLogAttributes, 'sentry.release', release);
108-
setLogAttribute(processedLogAttributes, 'sentry.environment', environment);
87+
safeSetAttribute(processedLogAttributes, 'sentry.release', release);
88+
safeSetAttribute(processedLogAttributes, 'sentry.environment', environment);
10989

11090
const { name, version } = client.getSdkMetadata()?.sdk ?? {};
111-
setLogAttribute(processedLogAttributes, 'sentry.sdk.name', name);
112-
setLogAttribute(processedLogAttributes, 'sentry.sdk.version', version);
91+
safeSetAttribute(processedLogAttributes, 'sentry.sdk.name', name);
92+
safeSetAttribute(processedLogAttributes, 'sentry.sdk.version', version);
11393

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

121101
const replayId = replay?.getReplayId(true);
122-
setLogAttribute(processedLogAttributes, 'sentry.replay_id', replayId);
102+
safeSetAttribute(processedLogAttributes, 'sentry.replay_id', replayId);
123103

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

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

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

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

@@ -212,20 +192,6 @@ export function _INTERNAL_getLogBuffer(client: Client): Array<SerializedLog> | u
212192
return _getBufferMap().get(client);
213193
}
214194

215-
/**
216-
* Get the scope data for the current scope after merging with the
217-
* global scope and isolation scope.
218-
*
219-
* @param currentScope - The current scope.
220-
* @returns The scope data.
221-
*/
222-
function getMergedScopeData(currentScope: Scope): ScopeData {
223-
const scopeData = getGlobalScope().getScopeData();
224-
mergeScopeData(scopeData, getIsolationScope().getScopeData());
225-
mergeScopeData(scopeData, currentScope.getScopeData());
226-
return scopeData;
227-
}
228-
229195
function _getBufferMap(): WeakMap<Client, Array<SerializedLog>> {
230196
// The reference to the Client <> LogBuffer map is stored on the carrier to ensure it's always the same
231197
return getGlobalSingleton('clientToLogBufferMap', () => new WeakMap<Client, Array<SerializedLog>>());

packages/core/src/metrics/internal.ts

Lines changed: 39 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,21 @@
1+
import { safeSetAttribute, serializeAttributes } from '../attributes';
12
import { getGlobalSingleton } from '../carrier';
23
import type { Client } from '../client';
3-
import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '../currentScopes';
4+
import { getClient, getCurrentScope } from '../currentScopes';
45
import { DEBUG_BUILD } from '../debug-build';
5-
import type { Scope, ScopeData } from '../scope';
6+
import type { Scope } from '../scope';
67
import type { Integration } from '../types-hoist/integration';
7-
import type { Metric, SerializedMetric, SerializedMetricAttributeValue } from '../types-hoist/metric';
8-
import { mergeScopeData } from '../utils/applyScopeDataToEvent';
8+
import type { Metric, SerializedMetric } from '../types-hoist/metric';
9+
import type { User } from '../types-hoist/user';
910
import { debug } from '../utils/debug-logger';
11+
import { getFinalScopeData } from '../utils/scope-utils';
1012
import { _getSpanForScope } from '../utils/spanOnScope';
1113
import { timestampInSeconds } from '../utils/time';
1214
import { _getTraceInfoFromScope } from '../utils/trace-info';
1315
import { createMetricEnvelope } from './envelope';
1416

1517
const MAX_METRIC_BUFFER_SIZE = 1000;
1618

17-
/**
18-
* Converts a metric attribute to a serialized metric attribute.
19-
*
20-
* @param value - The value of the metric attribute.
21-
* @returns The serialized metric attribute.
22-
*/
23-
export function metricAttributeToSerializedMetricAttribute(value: unknown): SerializedMetricAttributeValue {
24-
switch (typeof value) {
25-
case 'number':
26-
if (Number.isInteger(value)) {
27-
return {
28-
value,
29-
type: 'integer',
30-
};
31-
}
32-
return {
33-
value,
34-
type: 'double',
35-
};
36-
case 'boolean':
37-
return {
38-
value,
39-
type: 'boolean',
40-
};
41-
case 'string':
42-
return {
43-
value,
44-
type: 'string',
45-
};
46-
default: {
47-
let stringValue = '';
48-
try {
49-
stringValue = JSON.stringify(value) ?? '';
50-
} catch {
51-
// Do nothing
52-
}
53-
return {
54-
value: stringValue,
55-
type: 'string',
56-
};
57-
}
58-
}
59-
}
60-
61-
/**
62-
* Sets a metric attribute if the value exists and the attribute key is not already present.
63-
*
64-
* @param metricAttributes - The metric attributes object to modify.
65-
* @param key - The attribute key to set.
66-
* @param value - The value to set (only sets if truthy and key not present).
67-
* @param setEvenIfPresent - Whether to set the attribute if it is present. Defaults to true.
68-
*/
69-
function setMetricAttribute(
70-
metricAttributes: Record<string, unknown>,
71-
key: string,
72-
value: unknown,
73-
setEvenIfPresent = true,
74-
): void {
75-
if (value && (setEvenIfPresent || !(key in metricAttributes))) {
76-
metricAttributes[key] = value;
77-
}
78-
}
79-
8019
/**
8120
* Captures a serialized metric event and adds it to the metric buffer for the given client.
8221
*
@@ -120,29 +59,26 @@ export interface InternalCaptureMetricOptions {
12059
/**
12160
* Enriches metric with all contextual attributes (user, SDK metadata, replay, etc.)
12261
*/
123-
function _enrichMetricAttributes(beforeMetric: Metric, client: Client, currentScope: Scope): Metric {
62+
function _enrichMetricAttributes(beforeMetric: Metric, client: Client, user: User): Metric {
12463
const { release, environment } = client.getOptions();
12564

12665
const processedMetricAttributes = {
12766
...beforeMetric.attributes,
12867
};
12968

130-
// Add user attributes
131-
const {
132-
user: { id, email, username },
133-
} = getMergedScopeData(currentScope);
134-
setMetricAttribute(processedMetricAttributes, 'user.id', id, false);
135-
setMetricAttribute(processedMetricAttributes, 'user.email', email, false);
136-
setMetricAttribute(processedMetricAttributes, 'user.name', username, false);
69+
const { id, email, username } = user;
70+
safeSetAttribute(processedMetricAttributes, 'user.id', id, false);
71+
safeSetAttribute(processedMetricAttributes, 'user.email', email, false);
72+
safeSetAttribute(processedMetricAttributes, 'user.name', username, false);
13773

13874
// Add Sentry metadata
139-
setMetricAttribute(processedMetricAttributes, 'sentry.release', release);
140-
setMetricAttribute(processedMetricAttributes, 'sentry.environment', environment);
75+
safeSetAttribute(processedMetricAttributes, 'sentry.release', release);
76+
safeSetAttribute(processedMetricAttributes, 'sentry.environment', environment);
14177

14278
// Add SDK metadata
14379
const { name, version } = client.getSdkMetadata()?.sdk ?? {};
144-
setMetricAttribute(processedMetricAttributes, 'sentry.sdk.name', name);
145-
setMetricAttribute(processedMetricAttributes, 'sentry.sdk.version', version);
80+
safeSetAttribute(processedMetricAttributes, 'sentry.sdk.name', name);
81+
safeSetAttribute(processedMetricAttributes, 'sentry.sdk.version', version);
14682

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

15591
const replayId = replay?.getReplayId(true);
156-
setMetricAttribute(processedMetricAttributes, 'sentry.replay_id', replayId);
92+
safeSetAttribute(processedMetricAttributes, 'sentry.replay_id', replayId);
15793

15894
if (replayId && replay?.getRecordingMode() === 'buffer') {
159-
setMetricAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true);
95+
safeSetAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true);
16096
}
16197

16298
return {
@@ -165,36 +101,6 @@ function _enrichMetricAttributes(beforeMetric: Metric, client: Client, currentSc
165101
};
166102
}
167103

168-
/**
169-
* Creates a serialized metric ready to be sent to Sentry.
170-
*/
171-
function _buildSerializedMetric(metric: Metric, client: Client, currentScope: Scope): SerializedMetric {
172-
// Serialize attributes
173-
const serializedAttributes: Record<string, SerializedMetricAttributeValue> = {};
174-
for (const key in metric.attributes) {
175-
if (metric.attributes[key] !== undefined) {
176-
serializedAttributes[key] = metricAttributeToSerializedMetricAttribute(metric.attributes[key]);
177-
}
178-
}
179-
180-
// Get trace context
181-
const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
182-
const span = _getSpanForScope(currentScope);
183-
const traceId = span ? span.spanContext().traceId : traceContext?.trace_id;
184-
const spanId = span ? span.spanContext().spanId : undefined;
185-
186-
return {
187-
timestamp: timestampInSeconds(),
188-
trace_id: traceId ?? '',
189-
span_id: spanId,
190-
name: metric.name,
191-
type: metric.type,
192-
unit: metric.unit,
193-
value: metric.value,
194-
attributes: serializedAttributes,
195-
};
196-
}
197-
198104
/**
199105
* Captures a metric event and sends it to Sentry.
200106
*
@@ -224,8 +130,10 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal
224130
return;
225131
}
226132

133+
const { user, attributes: scopeAttributes } = getFinalScopeData(currentScope);
134+
227135
// Enrich metric with contextual attributes
228-
const enrichedMetric = _enrichMetricAttributes(beforeMetric, client, currentScope);
136+
const enrichedMetric = _enrichMetricAttributes(beforeMetric, client, user);
229137

230138
client.emit('processMetric', enrichedMetric);
231139

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

242-
const serializedMetric = _buildSerializedMetric(processedMetric, client, currentScope);
150+
const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
151+
const span = _getSpanForScope(currentScope);
152+
const traceId = span ? span.spanContext().traceId : traceContext?.trace_id;
153+
const spanId = span ? span.spanContext().spanId : undefined;
154+
155+
const { name, type, unit, value, attributes: metricAttributes } = processedMetric;
156+
const serializedMetric = {
157+
timestamp: timestampInSeconds(),
158+
trace_id: traceId ?? '',
159+
span_id: spanId,
160+
name,
161+
type,
162+
unit,
163+
value,
164+
attributes: {
165+
...serializeAttributes(metricAttributes, true),
166+
...serializeAttributes(scopeAttributes),
167+
},
168+
};
243169

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

@@ -288,20 +214,6 @@ export function _INTERNAL_getMetricBuffer(client: Client): Array<SerializedMetri
288214
return _getBufferMap().get(client);
289215
}
290216

291-
/**
292-
* Get the scope data for the current scope after merging with the
293-
* global scope and isolation scope.
294-
*
295-
* @param currentScope - The current scope.
296-
* @returns The scope data.
297-
*/
298-
function getMergedScopeData(currentScope: Scope): ScopeData {
299-
const scopeData = getGlobalScope().getScopeData();
300-
mergeScopeData(scopeData, getIsolationScope().getScopeData());
301-
mergeScopeData(scopeData, currentScope.getScopeData());
302-
return scopeData;
303-
}
304-
305217
function _getBufferMap(): WeakMap<Client, Array<SerializedMetric>> {
306218
// The reference to the Client <> MetricBuffer map is stored on the carrier to ensure it's always the same
307219
return getGlobalSingleton('clientToMetricBufferMap', () => new WeakMap<Client, Array<SerializedMetric>>());

0 commit comments

Comments
 (0)