Skip to content

Commit 5d95eca

Browse files
committed
feat(browser): Add brower metrics sdk
1 parent b3f8d9b commit 5d95eca

File tree

16 files changed

+502
-42
lines changed

16 files changed

+502
-42
lines changed

packages/browser/src/exports.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export {
5656
withScope,
5757
FunctionToString,
5858
InboundFilters,
59+
incr,
60+
distribution,
61+
set,
62+
gauge,
63+
Metrics,
5964
} from '@sentry/core';
6065

6166
export { WINDOW } from './helpers';

packages/core/src/baseclient.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { createEventEnvelope, createSessionEnvelope } from './envelope';
4949
import { getCurrentHub } from './hub';
5050
import type { IntegrationIndex } from './integration';
5151
import { setupIntegration, setupIntegrations } from './integration';
52+
import type { MetricsAggregator } from './metrics/types';
5253
import type { Scope } from './scope';
5354
import { updateSession } from './session';
5455
import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext';
@@ -88,6 +89,13 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca
8889
* }
8990
*/
9091
export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
92+
/**
93+
* A reference to a metrics aggregator
94+
*
95+
* @experimental Note this is alpha API. It may experience breaking changes in the future.
96+
*/
97+
public metricsAggregator: MetricsAggregator | undefined;
98+
9199
/** Options passed to the SDK. */
92100
protected readonly _options: O;
93101

@@ -264,6 +272,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
264272
public flush(timeout?: number): PromiseLike<boolean> {
265273
const transport = this._transport;
266274
if (transport) {
275+
if (this.metricsAggregator) {
276+
this.metricsAggregator.flush();
277+
}
267278
return this._isClientDoneProcessing(timeout).then(clientFinished => {
268279
return transport.flush(timeout).then(transportFlushed => clientFinished && transportFlushed);
269280
});

packages/core/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,12 @@ export { DEFAULT_ENVIRONMENT } from './constants';
6363
export { ModuleMetadata } from './integrations/metadata';
6464
export { RequestData } from './integrations/requestdata';
6565
import * as Integrations from './integrations';
66+
export {
67+
incr,
68+
distribution,
69+
set,
70+
gauge,
71+
} from './metrics/exports';
72+
export { Metrics } from './metrics/integration';
6673

6774
export { Integrations };
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const COUNTER_METRIC_TYPE = 'c';
2+
export const GAUGE_METRIC_TYPE = 'g';
3+
export const SET_METRIC_TYPE = 's';
4+
export const DISTRIBUTION_METRIC_TYPE = 'd';
5+
6+
export const NAME_AND_TAG_KEY_REGEX = /[^a-zA-Z0-9_/.-]+"/g;
7+
export const TAG_VALUE_REGEX = /[^\w\d_:/@.{}[\]$-]+/g;

packages/core/src/metrics/index.ts renamed to packages/core/src/metrics/envelope.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import type { DsnComponents, DynamicSamplingContext, SdkMetadata, StatsdEnvelope, StatsdItem } from '@sentry/types';
2-
import { createEnvelope, dropUndefinedKeys, dsnToString } from '@sentry/utils';
1+
import type { DsnComponents, SdkMetadata, StatsdEnvelope, StatsdItem } from '@sentry/types';
2+
import { createEnvelope, dsnToString } from '@sentry/utils';
33

44
/**
55
* Create envelope from a metric aggregate.
66
*/
77
export function createMetricEnvelope(
8-
// TODO(abhi): Add type for this
98
metricAggregate: string,
10-
dynamicSamplingContext?: Partial<DynamicSamplingContext>,
119
metadata?: SdkMetadata,
1210
tunnel?: string,
1311
dsn?: DsnComponents,
@@ -27,17 +25,15 @@ export function createMetricEnvelope(
2725
headers.dsn = dsnToString(dsn);
2826
}
2927

30-
if (dynamicSamplingContext) {
31-
headers.trace = dropUndefinedKeys(dynamicSamplingContext) as DynamicSamplingContext;
32-
}
33-
3428
const item = createMetricEnvelopeItem(metricAggregate);
3529
return createEnvelope<StatsdEnvelope>(headers, [item]);
3630
}
3731

3832
function createMetricEnvelopeItem(metricAggregate: string): StatsdItem {
3933
const metricHeaders: StatsdItem[0] = {
4034
type: 'statsd',
35+
content_type: 'application/octet-stream',
36+
length: metricAggregate.length,
4137
};
4238
return [metricHeaders, metricAggregate];
4339
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { ClientOptions, MeasurementUnit, Primitive } from '@sentry/types';
2+
import { logger } from '@sentry/utils';
3+
import type { BaseClient } from '../baseclient';
4+
import { DEBUG_BUILD } from '../debug-build';
5+
import { getCurrentHub } from '../hub';
6+
import type { MetricType } from './types';
7+
8+
interface MetricData {
9+
unit?: MeasurementUnit;
10+
tags?: { [key: string]: Primitive };
11+
timestamp?: number;
12+
}
13+
14+
function addToMetricsAggregator(metricType: MetricType, name: string, value: number, data: MetricData = {}): void {
15+
const hub = getCurrentHub();
16+
const client = hub.getClient() as BaseClient<ClientOptions>;
17+
const scope = hub.getScope();
18+
if (client) {
19+
if (!client.metricsAggregator) {
20+
DEBUG_BUILD &&
21+
logger.warn('No metrics aggregator enabled. Please add the Metrics integration to use metrics APIs');
22+
return;
23+
}
24+
const { unit, tags, timestamp } = data;
25+
const { release, environment } = client.getOptions();
26+
const transaction = scope.getTransaction();
27+
const metricTags = {
28+
...tags,
29+
};
30+
if (release) {
31+
metricTags.release = release;
32+
}
33+
if (environment) {
34+
metricTags.environment = environment;
35+
}
36+
if (transaction) {
37+
metricTags.transaction = transaction.name;
38+
}
39+
40+
DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`);
41+
client.metricsAggregator.add(metricType, name, value, unit, metricTags, timestamp);
42+
}
43+
}
44+
45+
/**
46+
* Adds a value to a counter metric
47+
*
48+
* @experimental This API is experimental and might having breaking changes in the future.
49+
*/
50+
export function incr(name: string, value: number, data?: MetricData): void {
51+
addToMetricsAggregator('c', name, value, data);
52+
}
53+
54+
/**
55+
* Adds a value to a distribution metric
56+
*
57+
* @experimental This API is experimental and might having breaking changes in the future.
58+
*/
59+
export function distribution(name: string, value: number, data?: MetricData): void {
60+
addToMetricsAggregator('d', name, value, data);
61+
}
62+
63+
/**
64+
* Adds a value to a set metric
65+
*
66+
* @experimental This API is experimental and might having breaking changes in the future.
67+
*/
68+
export function set(name: string, value: number, data?: MetricData): void {
69+
addToMetricsAggregator('s', name, value, data);
70+
}
71+
72+
/**
73+
* Adds a value to a gauge metric
74+
*
75+
* @experimental This API is experimental and might having breaking changes in the future.
76+
*/
77+
export function gauge(name: string, value: number, data?: MetricData): void {
78+
addToMetricsAggregator('s', name, value, data);
79+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
interface MetricInstance {
2+
add(value: number): void;
3+
toString(): string;
4+
}
5+
6+
/**
7+
* A metric instance representing a counter.
8+
*/
9+
export class CounterMetric implements MetricInstance {
10+
public constructor(public value: number) {}
11+
12+
/** JSDoc */
13+
public add(value: number): void {
14+
this.value += value;
15+
}
16+
17+
/** JSDoc */
18+
public toString(): string {
19+
return `${this.value}`;
20+
}
21+
}
22+
23+
/**
24+
* A metric instance representing a gauge.
25+
*/
26+
export class GaugeMetric implements MetricInstance {
27+
public last: number;
28+
public min: number;
29+
public max: number;
30+
public sum: number;
31+
public count: number;
32+
33+
public constructor(public value: number) {
34+
this.last = value;
35+
this.min = value;
36+
this.max = value;
37+
this.sum = value;
38+
this.count = 1;
39+
}
40+
41+
/** JSDoc */
42+
public add(value: number): void {
43+
this.value = value;
44+
this.last = value;
45+
this.min = Math.min(this.min, value);
46+
this.max = Math.max(this.max, value);
47+
this.sum += value;
48+
this.count += 1;
49+
}
50+
51+
/** JSDoc */
52+
public toString(): string {
53+
return `${this.last}:${this.min}:${this.max}:${this.sum}:${this.count}`;
54+
}
55+
}
56+
57+
/**
58+
* A metric instance representing a distribution.
59+
*/
60+
export class DistributionMetric implements MetricInstance {
61+
public value: number[];
62+
63+
public constructor(first: number) {
64+
this.value = [first];
65+
}
66+
67+
/** JSDoc */
68+
public add(value: number): void {
69+
this.value.push(value);
70+
}
71+
72+
/** JSDoc */
73+
public toString(): string {
74+
return this.value.join(':');
75+
}
76+
}
77+
78+
/**
79+
* A metric instance representing a set.
80+
*/
81+
export class SetMetric implements MetricInstance {
82+
public value: Set<number>;
83+
84+
public constructor(public first: number) {
85+
this.value = new Set([first]);
86+
}
87+
88+
/** JSDoc */
89+
public add(value: number): void {
90+
this.value.add(value);
91+
}
92+
93+
/** JSDoc */
94+
public toString(): string {
95+
return `${Array.from(this.value).join(':')}`;
96+
}
97+
}
98+
99+
export type Metric = CounterMetric | GaugeMetric | DistributionMetric | SetMetric;
100+
101+
export const METRIC_MAP = {
102+
c: CounterMetric,
103+
g: GaugeMetric,
104+
d: DistributionMetric,
105+
s: SetMetric,
106+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ClientOptions, Integration } from '@sentry/types';
2+
import type { BaseClient } from '../baseclient';
3+
import { SimpleMetricsAggregator } from './simpleaggregator';
4+
5+
/**
6+
* Enables Sentry metrics monitoring.
7+
*
8+
* @experimental This API is experimental and might having breaking changes in the future.
9+
*/
10+
export class Metrics implements Integration {
11+
/**
12+
* @inheritDoc
13+
*/
14+
public static id: string = 'Metrics';
15+
16+
/**
17+
* @inheritDoc
18+
*/
19+
public name: string;
20+
21+
public constructor() {
22+
this.name = Metrics.id;
23+
}
24+
25+
/**
26+
* @inheritDoc
27+
*/
28+
public setupOnce(): void {
29+
// Do nothing
30+
}
31+
32+
/**
33+
* @inheritDoc
34+
*/
35+
public setup(client: BaseClient<ClientOptions>): void {
36+
client.metricsAggregator = new SimpleMetricsAggregator(client);
37+
}
38+
}

0 commit comments

Comments
 (0)