Skip to content

Commit c2530dd

Browse files
authored
feat(metrics): Add top level option enableMetrics and beforeSendMetric (#18088)
- Adds top level options for `enableMetrics` and `beforeSendMetric`. - Kept the experimental flags for now to stay backwards compatible, but added a deprecation note and todo comments for removing them in v11. - Updated all the tests to use the top-level flag. - Had to refactor the `_INTERNAL_captureMetric` function since it broke the complexity threshold of our linter > [!WARNING] > `enableMetrics` now defaults to `true`, so theoretically this PR is behaviourally breaking.
1 parent ad870cf commit c2530dd

File tree

11 files changed

+152
-107
lines changed

11 files changed

+152
-107
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ module.exports = [
190190
path: createCDNPath('bundle.tracing.min.js'),
191191
gzip: false,
192192
brotli: false,
193-
limit: '124.1 KB',
193+
limit: '125 KB',
194194
},
195195
{
196196
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',

dev-packages/browser-integration-tests/suites/public-api/metrics/init.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ window.Sentry = Sentry;
44

55
Sentry.init({
66
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7-
_experiments: {
8-
enableMetrics: true,
9-
},
107
release: '1.0.0',
118
environment: 'test',
129
integrations: integrations => {

dev-packages/node-core-integration-tests/suites/public-api/metrics/scenario.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ const client = Sentry.init({
66
dsn: 'https://public@dsn.ingest.sentry.io/1337',
77
release: '1.0.0',
88
environment: 'test',
9-
_experiments: {
10-
enableMetrics: true,
11-
},
129
transport: loggingTransport,
1310
});
1411

dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ Sentry.init({
55
dsn: 'https://public@dsn.ingest.sentry.io/1337',
66
release: '1.0.0',
77
environment: 'test',
8-
_experiments: {
9-
enableMetrics: true,
10-
},
118
transport: loggingTransport,
129
});
1310

packages/browser/src/client.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,22 @@ export class BrowserClient extends Client<BrowserClientOptions> {
104104

105105
super(opts);
106106

107-
const { sendDefaultPii, sendClientReports, enableLogs, _experiments } = this._options;
107+
const {
108+
sendDefaultPii,
109+
sendClientReports,
110+
enableLogs,
111+
_experiments,
112+
enableMetrics: enableMetricsOption,
113+
} = this._options;
114+
115+
// todo(v11): Remove the experimental flag
116+
// eslint-disable-next-line deprecation/deprecation
117+
const enableMetrics = enableMetricsOption ?? _experiments?.enableMetrics ?? true;
108118

109119
// Flush logs and metrics when page becomes hidden (e.g., tab switch, navigation)
110-
if (WINDOW.document && (sendClientReports || enableLogs || _experiments?.enableMetrics)) {
120+
// todo(v11): Remove the experimental flag
121+
// eslint-disable-next-line deprecation/deprecation
122+
if (WINDOW.document && (sendClientReports || enableLogs || enableMetrics)) {
111123
WINDOW.document.addEventListener('visibilitychange', () => {
112124
if (WINDOW.document.visibilityState === 'hidden') {
113125
if (sendClientReports) {
@@ -116,7 +128,8 @@ export class BrowserClient extends Client<BrowserClientOptions> {
116128
if (enableLogs) {
117129
_INTERNAL_flushLogsBuffer(this);
118130
}
119-
if (_experiments?.enableMetrics) {
131+
132+
if (enableMetrics) {
120133
_INTERNAL_flushMetricsBuffer(this);
121134
}
122135
}

packages/core/src/client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
232232
setupWeightBasedFlushing(this, 'afterCaptureLog', 'flushLogs', estimateLogSizeInBytes, _INTERNAL_flushLogsBuffer);
233233
}
234234

235+
// todo(v11): Remove the experimental flag
236+
// eslint-disable-next-line deprecation/deprecation
237+
const enableMetrics = this._options.enableMetrics ?? this._options._experiments?.enableMetrics ?? true;
238+
235239
// Setup metric flushing with weight and timeout tracking
236-
if (this._options._experiments?.enableMetrics) {
240+
if (enableMetrics) {
237241
setupWeightBasedFlushing(
238242
this,
239243
'afterCaptureMetric',

packages/core/src/metrics/internal.ts

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -116,49 +116,33 @@ export interface InternalCaptureMetricOptions {
116116
}
117117

118118
/**
119-
* Captures a metric event and sends it to Sentry.
120-
*
121-
* @param metric - The metric event to capture.
122-
* @param options - Options for capturing the metric.
123-
*
124-
* @experimental This method will experience breaking changes. This is not yet part of
125-
* the stable Sentry SDK API and can be changed or removed without warning.
119+
* Enriches metric with all contextual attributes (user, SDK metadata, replay, etc.)
126120
*/
127-
export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: InternalCaptureMetricOptions): void {
128-
const currentScope = options?.scope ?? getCurrentScope();
129-
const captureSerializedMetric = options?.captureSerializedMetric ?? _INTERNAL_captureSerializedMetric;
130-
const client = currentScope?.getClient() ?? getClient();
131-
if (!client) {
132-
DEBUG_BUILD && debug.warn('No client available to capture metric.');
133-
return;
134-
}
135-
136-
const { release, environment, _experiments } = client.getOptions();
137-
if (!_experiments?.enableMetrics) {
138-
DEBUG_BUILD && debug.warn('metrics option not enabled, metric will not be captured.');
139-
return;
140-
}
141-
142-
const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
121+
function _enrichMetricAttributes(beforeMetric: Metric, client: Client, currentScope: Scope): Metric {
122+
const { release, environment } = client.getOptions();
143123

144124
const processedMetricAttributes = {
145125
...beforeMetric.attributes,
146126
};
147127

128+
// Add user attributes
148129
const {
149130
user: { id, email, username },
150131
} = getMergedScopeData(currentScope);
151132
setMetricAttribute(processedMetricAttributes, 'user.id', id, false);
152133
setMetricAttribute(processedMetricAttributes, 'user.email', email, false);
153134
setMetricAttribute(processedMetricAttributes, 'user.name', username, false);
154135

136+
// Add Sentry metadata
155137
setMetricAttribute(processedMetricAttributes, 'sentry.release', release);
156138
setMetricAttribute(processedMetricAttributes, 'sentry.environment', environment);
157139

140+
// Add SDK metadata
158141
const { name, version } = client.getSdkMetadata()?.sdk ?? {};
159142
setMetricAttribute(processedMetricAttributes, 'sentry.sdk.name', name);
160143
setMetricAttribute(processedMetricAttributes, 'sentry.sdk.version', version);
161144

145+
// Add replay metadata
162146
const replay = client.getIntegrationByName<
163147
Integration & {
164148
getReplayId: (onlyIfSampled?: boolean) => string;
@@ -167,54 +151,97 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal
167151
>('Replay');
168152

169153
const replayId = replay?.getReplayId(true);
170-
171154
setMetricAttribute(processedMetricAttributes, 'sentry.replay_id', replayId);
172155

173156
if (replayId && replay?.getRecordingMode() === 'buffer') {
174-
// We send this so we can identify cases where the replayId is attached but the replay itself might not have been sent to Sentry
175157
setMetricAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true);
176158
}
177159

178-
const metric: Metric = {
160+
return {
179161
...beforeMetric,
180162
attributes: processedMetricAttributes,
181163
};
164+
}
182165

183-
// Run beforeSendMetric callback
184-
const processedMetric = _experiments?.beforeSendMetric ? _experiments.beforeSendMetric(metric) : metric;
185-
186-
if (!processedMetric) {
187-
DEBUG_BUILD && debug.log('`beforeSendMetric` returned `null`, will not send metric.');
188-
return;
189-
}
190-
166+
/**
167+
* Creates a serialized metric ready to be sent to Sentry.
168+
*/
169+
function _buildSerializedMetric(metric: Metric, client: Client, currentScope: Scope): SerializedMetric {
170+
// Serialize attributes
191171
const serializedAttributes: Record<string, SerializedMetricAttributeValue> = {};
192-
for (const key in processedMetric.attributes) {
193-
if (processedMetric.attributes[key] !== undefined) {
194-
serializedAttributes[key] = metricAttributeToSerializedMetricAttribute(processedMetric.attributes[key]);
172+
for (const key in metric.attributes) {
173+
if (metric.attributes[key] !== undefined) {
174+
serializedAttributes[key] = metricAttributeToSerializedMetricAttribute(metric.attributes[key]);
195175
}
196176
}
197177

178+
// Get trace context
179+
const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
198180
const span = _getSpanForScope(currentScope);
199181
const traceId = span ? span.spanContext().traceId : traceContext?.trace_id;
200182
const spanId = span ? span.spanContext().spanId : undefined;
201183

202-
const serializedMetric: SerializedMetric = {
184+
return {
203185
timestamp: timestampInSeconds(),
204186
trace_id: traceId ?? '',
205187
span_id: spanId,
206-
name: processedMetric.name,
207-
type: processedMetric.type,
208-
unit: processedMetric.unit,
209-
value: processedMetric.value,
188+
name: metric.name,
189+
type: metric.type,
190+
unit: metric.unit,
191+
value: metric.value,
210192
attributes: serializedAttributes,
211193
};
194+
}
195+
196+
/**
197+
* Captures a metric event and sends it to Sentry.
198+
*
199+
* @param metric - The metric event to capture.
200+
* @param options - Options for capturing the metric.
201+
*
202+
* @experimental This method will experience breaking changes. This is not yet part of
203+
* the stable Sentry SDK API and can be changed or removed without warning.
204+
*/
205+
export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: InternalCaptureMetricOptions): void {
206+
const currentScope = options?.scope ?? getCurrentScope();
207+
const captureSerializedMetric = options?.captureSerializedMetric ?? _INTERNAL_captureSerializedMetric;
208+
const client = currentScope?.getClient() ?? getClient();
209+
if (!client) {
210+
DEBUG_BUILD && debug.warn('No client available to capture metric.');
211+
return;
212+
}
213+
214+
const { _experiments, enableMetrics, beforeSendMetric } = client.getOptions();
215+
216+
// todo(v11): Remove the experimental flag
217+
// eslint-disable-next-line deprecation/deprecation
218+
const metricsEnabled = enableMetrics ?? _experiments?.enableMetrics ?? true;
219+
220+
if (!metricsEnabled) {
221+
DEBUG_BUILD && debug.warn('metrics option not enabled, metric will not be captured.');
222+
return;
223+
}
224+
225+
// Enrich metric with contextual attributes
226+
const enrichedMetric = _enrichMetricAttributes(beforeMetric, client, currentScope);
227+
228+
// todo(v11): Remove the experimental `beforeSendMetric`
229+
// eslint-disable-next-line deprecation/deprecation
230+
const beforeSendCallback = beforeSendMetric || _experiments?.beforeSendMetric;
231+
const processedMetric = beforeSendCallback ? beforeSendCallback(enrichedMetric) : enrichedMetric;
232+
233+
if (!processedMetric) {
234+
DEBUG_BUILD && debug.log('`beforeSendMetric` returned `null`, will not send metric.');
235+
return;
236+
}
237+
238+
const serializedMetric = _buildSerializedMetric(processedMetric, client, currentScope);
212239

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

215242
captureSerializedMetric(client, serializedMetric);
216243

217-
client.emit('afterCaptureMetric', metric);
244+
client.emit('afterCaptureMetric', enrichedMetric);
218245
}
219246

220247
/**

packages/core/src/types-hoist/options.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
287287
*
288288
* @default false
289289
* @experimental
290+
* @deprecated Use the top level`enableMetrics` option instead.
290291
*/
291292
enableMetrics?: boolean;
292293

@@ -302,6 +303,7 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
302303
*
303304
* @param metric The metric generated by the SDK.
304305
* @returns A new metric that will be sent | null.
306+
* @deprecated Use the top level`beforeSendMetric` option instead.
305307
*/
306308
beforeSendMetric?: (metric: Metric) => Metric | null;
307309
};
@@ -401,6 +403,27 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
401403
*/
402404
beforeSendLog?: (log: Log) => Log | null;
403405

406+
/**
407+
* If metrics support should be enabled.
408+
*
409+
* @default true
410+
*/
411+
enableMetrics?: boolean;
412+
413+
/**
414+
* An event-processing callback for metrics, guaranteed to be invoked after all other metric
415+
* processors. This allows a metric to be modified or dropped before it's sent.
416+
*
417+
* Note that you must return a valid metric from this callback. If you do not wish to modify the metric, simply return
418+
* it at the end. Returning `null` will cause the metric to be dropped.
419+
*
420+
* @default undefined
421+
*
422+
* @param metric The metric generated by the SDK.
423+
* @returns A new metric that will be sent.
424+
*/
425+
beforeSendMetric?: (metric: Metric) => Metric;
426+
404427
/**
405428
* Function to compute tracing sample rate dynamically and filter unwanted traces.
406429
*

packages/core/test/lib/client.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2753,7 +2753,6 @@ describe('Client', () => {
27532753
it('flushes metrics when weight exceeds 800KB', () => {
27542754
const options = getDefaultTestClientOptions({
27552755
dsn: PUBLIC_DSN,
2756-
_experiments: { enableMetrics: true },
27572756
});
27582757
const client = new TestClient(options);
27592758
const scope = new Scope();
@@ -2774,7 +2773,6 @@ describe('Client', () => {
27742773
it('accumulates metric weight without flushing when under threshold', () => {
27752774
const options = getDefaultTestClientOptions({
27762775
dsn: PUBLIC_DSN,
2777-
_experiments: { enableMetrics: true },
27782776
});
27792777
const client = new TestClient(options);
27802778
const scope = new Scope();
@@ -2791,7 +2789,6 @@ describe('Client', () => {
27912789
it('flushes metrics on flush event', () => {
27922790
const options = getDefaultTestClientOptions({
27932791
dsn: PUBLIC_DSN,
2794-
_experiments: { enableMetrics: true },
27952792
});
27962793
const client = new TestClient(options);
27972794
const scope = new Scope();

0 commit comments

Comments
 (0)