Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(browser): Add browser metrics sdk #9794

Merged
merged 21 commits into from
Dec 14, 2023
Merged
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
1 change: 1 addition & 0 deletions packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export {
withScope,
FunctionToString,
InboundFilters,
metrics,
} from '@sentry/core';

export { WINDOW } from './helpers';
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type {
FeedbackEvent,
Integration,
IntegrationClass,
MetricBucketItem,
MetricsAggregator,
Outcome,
PropagationContext,
SdkMetadata,
Expand Down Expand Up @@ -49,6 +51,7 @@ import { createEventEnvelope, createSessionEnvelope } from './envelope';
import { getCurrentHub } from './hub';
import type { IntegrationIndex } from './integration';
import { setupIntegration, setupIntegrations } from './integration';
import { createMetricEnvelope } from './metrics/envelope';
import type { Scope } from './scope';
import { updateSession } from './session';
import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext';
Expand Down Expand Up @@ -88,6 +91,13 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca
* }
*/
export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
/**
* A reference to a metrics aggregator
*
* @experimental Note this is alpha API. It may experience breaking changes in the future.
*/
public metricsAggregator?: MetricsAggregator;

/** Options passed to the SDK. */
protected readonly _options: O;

Expand Down Expand Up @@ -264,6 +274,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
public flush(timeout?: number): PromiseLike<boolean> {
const transport = this._transport;
if (transport) {
if (this.metricsAggregator) {
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
this.metricsAggregator.flush();
}
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
return this._isClientDoneProcessing(timeout).then(clientFinished => {
return transport.flush(timeout).then(transportFlushed => clientFinished && transportFlushed);
});
Expand All @@ -278,6 +291,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
public close(timeout?: number): PromiseLike<boolean> {
return this.flush(timeout).then(result => {
this.getOptions().enabled = false;
if (this.metricsAggregator) {
this.metricsAggregator.close();
}
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
return result;
});
}
Expand Down Expand Up @@ -383,6 +399,19 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
}
}

/**
* @inheritDoc
*/
public captureAggregateMetrics(metricBucketItems: Array<MetricBucketItem>): void {
const metricsEnvelope = createMetricEnvelope(
metricBucketItems,
this._dsn,
this._options._metadata,
this._options.tunnel,
);
void this._sendEnvelope(metricsEnvelope);
}

// Keep on() & emit() signatures in sync with types' client.ts interface
/* eslint-disable @typescript-eslint/unified-signatures */

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,6 @@ export { DEFAULT_ENVIRONMENT } from './constants';
export { ModuleMetadata } from './integrations/metadata';
export { RequestData } from './integrations/requestdata';
import * as Integrations from './integrations';
export { metrics } from './metrics/exports';

export { Integrations };
30 changes: 30 additions & 0 deletions packages/core/src/metrics/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const COUNTER_METRIC_TYPE = 'c' as const;
export const GAUGE_METRIC_TYPE = 'g' as const;
export const SET_METRIC_TYPE = 's' as const;
export const DISTRIBUTION_METRIC_TYPE = 'd' as const;

/**
* Normalization regex for metric names and metric tag names.
*
* This enforces that names and tag keys only contain alphanumeric characters,
* underscores, forward slashes, periods, and dashes.
*
* See: https://develop.sentry.dev/sdk/metrics/#normalization
*/
export const NAME_AND_TAG_KEY_NORMALIZATION_REGEX = /[^a-zA-Z0-9_/.-]+/g;

/**
* Normalization regex for metric tag values.
*
* This enforces that values only contain words, digits, or the following
* special characters: _:/@.{}[\]$-
*
* See: https://develop.sentry.dev/sdk/metrics/#normalization
*/
export const TAG_VALUE_NORMALIZATION_REGEX = /[^\w\d_:/@.{}[\]$-]+/g;

/**
* This does not match spec in https://develop.sentry.dev/sdk/metrics
* but was chosen to optimize for the most common case in browser environments.
*/
export const DEFAULT_FLUSH_INTERVAL = 5000;
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
40 changes: 40 additions & 0 deletions packages/core/src/metrics/envelope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { DsnComponents, MetricBucketItem, SdkMetadata, StatsdEnvelope, StatsdItem } from '@sentry/types';
import { createEnvelope, dsnToString } from '@sentry/utils';
import { serializeMetricBuckets } from './utils';

/**
* Create envelope from a metric aggregate.
*/
export function createMetricEnvelope(
metricBucketItems: Array<MetricBucketItem>,
dsn?: DsnComponents,
metadata?: SdkMetadata,
tunnel?: string,
): StatsdEnvelope {
const headers: StatsdEnvelope[0] = {
sent_at: new Date().toISOString(),
};

if (metadata && metadata.sdk) {
headers.sdk = {
name: metadata.sdk.name,
version: metadata.sdk.version,
};
}

if (!!tunnel && dsn) {
headers.dsn = dsnToString(dsn);
}

const item = createMetricEnvelopeItem(metricBucketItems);
return createEnvelope<StatsdEnvelope>(headers, [item]);
}

function createMetricEnvelopeItem(metricBucketItems: Array<MetricBucketItem>): StatsdItem {
const payload = serializeMetricBuckets(metricBucketItems);
const metricHeaders: StatsdItem[0] = {
type: 'statsd',
length: payload.length,
};
return [metricHeaders, payload];
}
92 changes: 92 additions & 0 deletions packages/core/src/metrics/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { ClientOptions, MeasurementUnit, Primitive } from '@sentry/types';
import { logger } from '@sentry/utils';
import type { BaseClient } from '../baseclient';
import { DEBUG_BUILD } from '../debug-build';
import { getCurrentHub } from '../hub';
import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants';
import { MetricsAggregator } from './integration';
import type { MetricType } from './types';

interface MetricData {
unit?: MeasurementUnit;
tags?: Record<string, Primitive>;
timestamp?: number;
}

function addToMetricsAggregator(
metricType: MetricType,
name: string,
value: number | string,
data: MetricData = {},
): void {
const hub = getCurrentHub();
const client = hub.getClient() as BaseClient<ClientOptions>;
const scope = hub.getScope();
if (client) {
if (!client.metricsAggregator) {
DEBUG_BUILD &&
logger.warn('No metrics aggregator enabled. Please add the MetricsAggregator integration to use metrics APIs');
return;
}
const { unit, tags, timestamp } = data;
const { release, environment } = client.getOptions();
const transaction = scope.getTransaction();
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
const metricTags: Record<string, string> = {};
if (release) {
metricTags.release = release;
}
if (environment) {
metricTags.environment = environment;
}
if (transaction) {
metricTags.transaction = transaction.name;
}
Comment on lines +35 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l/m: Should we think about applying these before we spread the tags so that people can override them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we want users to override them. Let me start a slack thread.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that this is also somewhat like this for general events 🤔 e.g.

// in scope applyToEvent
  if (this._level) {
      event.level = this._level;
    }
    if (this._transactionName) {
      event.transaction = this._transactionName;
    }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... but only for some things, for others (e.g. tags etc.) event data takes precedence.


DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`);
client.metricsAggregator.add(metricType, name, value, unit, { ...metricTags, ...tags }, timestamp);
}
}

/**
* Adds a value to a counter metric
*
* @experimental This API is experimental and might having breaking changes in the future.
*/
export function increment(name: string, value: number = 1, data?: MetricData): void {
addToMetricsAggregator(COUNTER_METRIC_TYPE, name, value, data);
}

/**
* Adds a value to a distribution metric
*
* @experimental This API is experimental and might having breaking changes in the future.
*/
export function distribution(name: string, value: number, data?: MetricData): void {
addToMetricsAggregator(DISTRIBUTION_METRIC_TYPE, name, value, data);
}

/**
* Adds a value to a set metric. Value must be a string or integer.
*
* @experimental This API is experimental and might having breaking changes in the future.
*/
export function set(name: string, value: number | string, data?: MetricData): void {
addToMetricsAggregator(SET_METRIC_TYPE, name, value, data);
}

/**
* Adds a value to a gauge metric
*
* @experimental This API is experimental and might having breaking changes in the future.
*/
export function gauge(name: string, value: number, data?: MetricData): void {
addToMetricsAggregator(GAUGE_METRIC_TYPE, name, value, data);
}

export const metrics = {
increment,
distribution,
set,
gauge,
MetricsAggregator,
};
43 changes: 0 additions & 43 deletions packages/core/src/metrics/index.ts

This file was deleted.

Loading