Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 5 additions & 0 deletions packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export {
withScope,
FunctionToString,
InboundFilters,
incr,
distribution,
set,
gauge,
Metrics,
} from '@sentry/core';

export { WINDOW } from './helpers';
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ 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 { MetricsAggregator } from './metrics/types';
import type { Scope } from './scope';
import { updateSession } from './session';
import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext';
Expand Down Expand Up @@ -88,6 +90,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 +273,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) {
this.metricsAggregator.flush();
}
return this._isClientDoneProcessing(timeout).then(clientFinished => {
return transport.flush(timeout).then(transportFlushed => clientFinished && transportFlushed);
});
Expand All @@ -278,6 +290,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();
}
return result;
});
}
Expand Down Expand Up @@ -383,6 +398,19 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
}
}

/**
* @inheritDoc
*/
public captureSerializedMetrics(serializedMetrics: string): void {
const metricsEnvelope = createMetricEnvelope(
serializedMetrics,
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
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,12 @@ export { DEFAULT_ENVIRONMENT } from './constants';
export { ModuleMetadata } from './integrations/metadata';
export { RequestData } from './integrations/requestdata';
import * as Integrations from './integrations';
export {
incr,
distribution,
set,
gauge,
} from './metrics/exports';
export { Metrics } from './metrics/integration';

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';
export const GAUGE_METRIC_TYPE = 'g';
export const SET_METRIC_TYPE = 's';
export const DISTRIBUTION_METRIC_TYPE = 'd';
Copy link
Contributor

Choose a reason for hiding this comment

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

m: Since we are not using these anywhere as actual values, should we just define them as types instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

ended up using the constants and caught a bug f5d6333


/**
* 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 balues.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Normalization regex for metric tag balues.
* 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;
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import type { DsnComponents, DynamicSamplingContext, SdkMetadata, StatsdEnvelope, StatsdItem } from '@sentry/types';
import { createEnvelope, dropUndefinedKeys, dsnToString } from '@sentry/utils';
import type { DsnComponents, SdkMetadata, StatsdEnvelope, StatsdItem } from '@sentry/types';
import { createEnvelope, dsnToString } from '@sentry/utils';

/**
* Create envelope from a metric aggregate.
*/
export function createMetricEnvelope(
// TODO(abhi): Add type for this
metricAggregate: string,
dynamicSamplingContext?: Partial<DynamicSamplingContext>,
dsn?: DsnComponents,
metadata?: SdkMetadata,
tunnel?: string,
dsn?: DsnComponents,
): StatsdEnvelope {
const headers: StatsdEnvelope[0] = {
sent_at: new Date().toISOString(),
Expand All @@ -27,17 +25,15 @@ export function createMetricEnvelope(
headers.dsn = dsnToString(dsn);
}

if (dynamicSamplingContext) {
headers.trace = dropUndefinedKeys(dynamicSamplingContext) as DynamicSamplingContext;
}

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

function createMetricEnvelopeItem(metricAggregate: string): StatsdItem {
const metricHeaders: StatsdItem[0] = {
type: 'statsd',
content_type: 'application/octet-stream',
length: metricAggregate.length,
};
return [metricHeaders, metricAggregate];
}
81 changes: 81 additions & 0 deletions packages/core/src/metrics/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 type { MetricType } from './types';

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

function addToMetricsAggregator(metricType: MetricType, name: string, value: number, 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 Metrics integration to use metrics APIs');
return;
}
const { unit, tags, timestamp } = data;
const { release, environment } = client.getOptions();
const transaction = scope.getTransaction();
const metricTags = {
...tags,
};
if (release) {
metricTags.release = release;
}
if (environment) {
metricTags.environment = environment;
}
if (transaction) {
metricTags.transaction = transaction.name;
}
Comment on lines +35 to +43
Copy link
Contributor

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, timestamp);
}
}

/**
* Adds a value to a counter metric
*
* @experimental This API is experimental and might having breaking changes in the future.
*/
export function incr(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, incomingValue: number | string, data?: MetricData): void {
const value = typeof incomingValue === 'string' ? parseInt(incomingValue) : Math.floor(incomingValue);
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);
}
114 changes: 114 additions & 0 deletions packages/core/src/metrics/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants';

interface MetricInstance {
/**
* Adds a value to a metric.
*/
add(value: number): void;
/**
* Serializes the metric into a statsd format string.
*/
toString(): string;
}

/**
* A metric instance representing a counter.
*/
export class CounterMetric implements MetricInstance {
public constructor(private _value: number) {}

/** @inheritdoc */
public add(value: number): void {
this._value += value;
}

/** @inheritdoc */
public toString(): string {
return `${this._value}`;
}
}

/**
* A metric instance representing a gauge.
*/
export class GaugeMetric implements MetricInstance {
private _last: number;
private _min: number;
private _max: number;
private _sum: number;
private _count: number;

public constructor(private _value: number) {
this._last = _value;
this._min = _value;
this._max = _value;
this._sum = _value;
this._count = 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be moved to line 30

Copy link
Member Author

Choose a reason for hiding this comment

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

For now it can't see #8700

}

/** @inheritdoc */
public add(value: number): void {
this._value = value;
this._value = value;
this._min = Math.min(this._min, value);
this._max = Math.max(this._max, value);
this._sum += value;
this._count += 1;
}

/** @inheritdoc */
public toString(): string {
return `${this._last}:${this._min}:${this._max}:${this._sum}:${this._count}`;
}
}

/**
* A metric instance representing a distribution.
*/
export class DistributionMetric implements MetricInstance {
private _value: number[];

public constructor(first: number) {
this._value = [first];
}

/** @inheritdoc */
public add(value: number): void {
this._value.push(value);
}

/** @inheritdoc */
public toString(): string {
return this._value.join(':');
}
}

/**
* A metric instance representing a set.
*/
export class SetMetric implements MetricInstance {
private _value: Set<number>;

public constructor(public first: number) {
this._value = new Set([first]);
}

/** @inheritdoc */
public add(value: number): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

h: this should also be able to take a string.

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 changed the set public method to accept a string | number and then changed the internals to only take a number. I feel like this is more correct behavour.

Let me chat with Armin about this. This means that if we get something like new Set(['1', 1]), we'll flush 1 out twice when we serialize the set.

this._value.add(value);
}

/** @inheritdoc */
public toString(): string {
return `${Array.from(this._value).join(':')}`;
}
}

export type Metric = CounterMetric | GaugeMetric | DistributionMetric | SetMetric;

export const METRIC_MAP = {
[COUNTER_METRIC_TYPE]: CounterMetric,
[GAUGE_METRIC_TYPE]: GaugeMetric,
[DISTRIBUTION_METRIC_TYPE]: DistributionMetric,
[SET_METRIC_TYPE]: SetMetric,
};
38 changes: 38 additions & 0 deletions packages/core/src/metrics/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ClientOptions, Integration } from '@sentry/types';
import type { BaseClient } from '../baseclient';
import { SimpleMetricsAggregator } from './simpleaggregator';

/**
* Enables Sentry metrics monitoring.
*
* @experimental This API is experimental and might having breaking changes in the future.
*/
export class Metrics implements Integration {
/**
* @inheritDoc
*/
public static id: string = 'Metrics';

/**
* @inheritDoc
*/
public name: string;

public constructor() {
this.name = Metrics.id;
}

/**
* @inheritDoc
*/
public setupOnce(): void {
// Do nothing
}

/**
* @inheritDoc
*/
public setup(client: BaseClient<ClientOptions>): void {
client.metricsAggregator = new SimpleMetricsAggregator(client);
}
}
Loading