Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/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://public@dsn.ingest.sentry.io/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://public@dsn.ingest.sentry.io/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
Loading