Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '124.1 KB',
limit: '125 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
_experiments: {
enableMetrics: true,
},
release: '1.0.0',
environment: 'test',
integrations: integrations => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ const client = Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0.0',
environment: 'test',
_experiments: {
enableMetrics: true,
},
transport: loggingTransport,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0.0',
environment: 'test',
_experiments: {
enableMetrics: true,
},
transport: loggingTransport,
});

Expand Down
19 changes: 16 additions & 3 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,22 @@ export class BrowserClient extends Client<BrowserClientOptions> {

super(opts);

const { sendDefaultPii, sendClientReports, enableLogs, _experiments } = this._options;
const {
sendDefaultPii,
sendClientReports,
enableLogs,
_experiments,
enableMetrics: enableMetricsOption,
} = this._options;

// todo(v11): Remove the experimental flag
// eslint-disable-next-line deprecation/deprecation
const enableMetrics = enableMetricsOption ?? _experiments?.enableMetrics ?? true;

// Flush logs and metrics when page becomes hidden (e.g., tab switch, navigation)
if (WINDOW.document && (sendClientReports || enableLogs || _experiments?.enableMetrics)) {
// todo(v11): Remove the experimental flag
// eslint-disable-next-line deprecation/deprecation
if (WINDOW.document && (sendClientReports || enableLogs || enableMetrics)) {
WINDOW.document.addEventListener('visibilitychange', () => {
if (WINDOW.document.visibilityState === 'hidden') {
if (sendClientReports) {
Expand All @@ -116,7 +128,8 @@ export class BrowserClient extends Client<BrowserClientOptions> {
if (enableLogs) {
_INTERNAL_flushLogsBuffer(this);
}
if (_experiments?.enableMetrics) {

if (enableMetrics) {
_INTERNAL_flushMetricsBuffer(this);
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
setupWeightBasedFlushing(this, 'afterCaptureLog', 'flushLogs', estimateLogSizeInBytes, _INTERNAL_flushLogsBuffer);
}

// todo(v11): Remove the experimental flag
// eslint-disable-next-line deprecation/deprecation
const enableMetrics = this._options.enableMetrics ?? this._options._experiments?.enableMetrics ?? true;

// Setup metric flushing with weight and timeout tracking
if (this._options._experiments?.enableMetrics) {
if (enableMetrics) {
setupWeightBasedFlushing(
this,
'afterCaptureMetric',
Expand Down
113 changes: 70 additions & 43 deletions packages/core/src/metrics/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,49 +116,33 @@ export interface InternalCaptureMetricOptions {
}

/**
* 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.
* Enriches metric with all contextual attributes (user, SDK metadata, replay, etc.)
*/
export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: InternalCaptureMetricOptions): void {
const currentScope = options?.scope ?? getCurrentScope();
const captureSerializedMetric = options?.captureSerializedMetric ?? _INTERNAL_captureSerializedMetric;
const client = currentScope?.getClient() ?? getClient();
if (!client) {
DEBUG_BUILD && debug.warn('No client available to capture metric.');
return;
}

const { release, environment, _experiments } = client.getOptions();
if (!_experiments?.enableMetrics) {
DEBUG_BUILD && debug.warn('metrics option not enabled, metric will not be captured.');
return;
}

const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
function _enrichMetricAttributes(beforeMetric: Metric, client: Client, currentScope: Scope): 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);

// 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<
Integration & {
getReplayId: (onlyIfSampled?: boolean) => string;
Expand All @@ -167,54 +151,97 @@ export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: Internal
>('Replay');

const replayId = replay?.getReplayId(true);

setMetricAttribute(processedMetricAttributes, '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
setMetricAttribute(processedMetricAttributes, 'sentry._internal.replay_is_buffering', true);
}

const metric: Metric = {
return {
...beforeMetric,
attributes: processedMetricAttributes,
};
}

// Run beforeSendMetric callback
const processedMetric = _experiments?.beforeSendMetric ? _experiments.beforeSendMetric(metric) : metric;

if (!processedMetric) {
DEBUG_BUILD && debug.log('`beforeSendMetric` returned `null`, will not send metric.');
return;
}

/**
* 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 processedMetric.attributes) {
if (processedMetric.attributes[key] !== undefined) {
serializedAttributes[key] = metricAttributeToSerializedMetricAttribute(processedMetric.attributes[key]);
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;

const serializedMetric: SerializedMetric = {
return {
timestamp: timestampInSeconds(),
trace_id: traceId,
span_id: spanId,
name: processedMetric.name,
type: processedMetric.type,
unit: processedMetric.unit,
value: processedMetric.value,
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.
*/
export function _INTERNAL_captureMetric(beforeMetric: Metric, options?: InternalCaptureMetricOptions): void {
const currentScope = options?.scope ?? getCurrentScope();
const captureSerializedMetric = options?.captureSerializedMetric ?? _INTERNAL_captureSerializedMetric;
const client = currentScope?.getClient() ?? getClient();
if (!client) {
DEBUG_BUILD && 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) {
DEBUG_BUILD && debug.warn('metrics option not enabled, metric will not be captured.');
return;
}

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

// 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) {
DEBUG_BUILD && debug.log('`beforeSendMetric` returned `null`, will not send metric.');
return;
}

const serializedMetric = _buildSerializedMetric(processedMetric, client, currentScope);

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

captureSerializedMetric(client, serializedMetric);

client.emit('afterCaptureMetric', metric);
client.emit('afterCaptureMetric', enrichedMetric);
}

/**
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/types-hoist/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
*
* @default false
* @experimental
* @deprecated Use the top level`enableMetrics` option instead.
*/
enableMetrics?: boolean;

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

/**
* If metrics support should be enabled.
*
* @default true
*/
enableMetrics?: boolean;

/**
* An event-processing callback for metrics, guaranteed to be invoked after all other metric
* processors. This allows a metric to be modified or dropped before it's sent.
*
* Note that you must return a valid metric from this callback. If you do not wish to modify the metric, simply return
* it at the end. Returning `null` will cause the metric to be dropped.
*
* @default undefined
*
* @param metric The metric generated by the SDK.
* @returns A new metric that will be sent.
*/
beforeSendMetric?: (metric: Metric) => Metric;

/**
* Function to compute tracing sample rate dynamically and filter unwanted traces.
*
Expand Down
3 changes: 0 additions & 3 deletions packages/core/test/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2753,7 +2753,6 @@ describe('Client', () => {
it('flushes metrics when weight exceeds 800KB', () => {
const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
_experiments: { enableMetrics: true },
});
const client = new TestClient(options);
const scope = new Scope();
Expand All @@ -2771,7 +2770,6 @@ describe('Client', () => {
it('accumulates metric weight without flushing when under threshold', () => {
const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
_experiments: { enableMetrics: true },
});
const client = new TestClient(options);
const scope = new Scope();
Expand All @@ -2788,7 +2786,6 @@ describe('Client', () => {
it('flushes metrics on flush event', () => {
const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
_experiments: { enableMetrics: true },
});
const client = new TestClient(options);
const scope = new Scope();
Expand Down
Loading