Skip to content

Commit 61e9a77

Browse files
committed
Revert "Revert "feat(core): Add metric summaries to spans (#10432)" (#10495)"
This reverts commit 60a7d65. # Conflicts: # packages/types/src/event.ts
1 parent 737fb0e commit 61e9a77

File tree

10 files changed

+250
-15
lines changed

10 files changed

+250
-15
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
_experiments: {
10+
metricsAggregator: true,
11+
},
12+
});
13+
14+
// Stop the process from exiting before the transaction is sent
15+
setInterval(() => {}, 1000);
16+
17+
Sentry.startSpan(
18+
{
19+
name: 'Test Transaction',
20+
op: 'transaction',
21+
},
22+
() => {
23+
Sentry.metrics.increment('root-counter');
24+
Sentry.metrics.increment('root-counter');
25+
26+
Sentry.startSpan(
27+
{
28+
name: 'Some other span',
29+
op: 'transaction',
30+
},
31+
() => {
32+
Sentry.metrics.increment('root-counter');
33+
Sentry.metrics.increment('root-counter');
34+
Sentry.metrics.increment('root-counter', 2);
35+
36+
Sentry.metrics.set('root-set', 'some-value');
37+
Sentry.metrics.set('root-set', 'another-value');
38+
Sentry.metrics.set('root-set', 'another-value');
39+
40+
Sentry.metrics.gauge('root-gauge', 42);
41+
Sentry.metrics.gauge('root-gauge', 20);
42+
43+
Sentry.metrics.distribution('root-distribution', 42);
44+
Sentry.metrics.distribution('root-distribution', 20);
45+
},
46+
);
47+
},
48+
);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { createRunner } from '../../../utils/runner';
2+
3+
const EXPECTED_TRANSACTION = {
4+
transaction: 'Test Transaction',
5+
_metrics_summary: {
6+
'c:root-counter@none': {
7+
min: 1,
8+
max: 1,
9+
count: 2,
10+
sum: 2,
11+
tags: {
12+
release: '1.0',
13+
transaction: 'Test Transaction',
14+
},
15+
},
16+
},
17+
spans: expect.arrayContaining([
18+
expect.objectContaining({
19+
description: 'Some other span',
20+
op: 'transaction',
21+
_metrics_summary: {
22+
'c:root-counter@none': {
23+
min: 1,
24+
max: 2,
25+
count: 3,
26+
sum: 4,
27+
tags: {
28+
release: '1.0',
29+
transaction: 'Test Transaction',
30+
},
31+
},
32+
's:root-set@none': {
33+
min: 0,
34+
max: 1,
35+
count: 3,
36+
sum: 2,
37+
tags: {
38+
release: '1.0',
39+
transaction: 'Test Transaction',
40+
},
41+
},
42+
'g:root-gauge@none': {
43+
min: 20,
44+
max: 42,
45+
count: 2,
46+
sum: 62,
47+
tags: {
48+
release: '1.0',
49+
transaction: 'Test Transaction',
50+
},
51+
},
52+
'd:root-distribution@none': {
53+
min: 20,
54+
max: 42,
55+
count: 2,
56+
sum: 62,
57+
tags: {
58+
release: '1.0',
59+
transaction: 'Test Transaction',
60+
},
61+
},
62+
},
63+
}),
64+
]),
65+
};
66+
67+
test('Should add metric summaries to spans', done => {
68+
createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done);
69+
});

packages/core/src/metrics/aggregator.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type {
66
Primitive,
77
} from '@sentry/types';
88
import { timestampInSeconds } from '@sentry/utils';
9-
import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants';
9+
import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
1010
import { METRIC_MAP } from './instance';
11+
import { updateMetricSummaryOnActiveSpan } from './metric-summary';
1112
import type { MetricBucket, MetricType } from './types';
1213
import { getBucketKey, sanitizeTags } from './utils';
1314

@@ -62,7 +63,11 @@ export class MetricsAggregator implements MetricsAggregatorBase {
6263
const tags = sanitizeTags(unsanitizedTags);
6364

6465
const bucketKey = getBucketKey(metricType, name, unit, tags);
66+
6567
let bucketItem = this._buckets.get(bucketKey);
68+
// If this is a set metric, we need to calculate the delta from the previous weight.
69+
const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0;
70+
6671
if (bucketItem) {
6772
bucketItem.metric.add(value);
6873
// TODO(abhi): Do we need this check?
@@ -82,6 +87,10 @@ export class MetricsAggregator implements MetricsAggregatorBase {
8287
this._buckets.set(bucketKey, bucketItem);
8388
}
8489

90+
// If value is a string, it's a set metric so calculate the delta from the previous weight.
91+
const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value;
92+
updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey);
93+
8594
// We need to keep track of the total weight of the buckets so that we can
8695
// flush them when we exceed the max weight.
8796
this._bucketsTotalWeight += bucketItem.metric.weight;

packages/core/src/metrics/browser-aggregator.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import type {
2-
Client,
3-
ClientOptions,
4-
MeasurementUnit,
5-
MetricBucketItem,
6-
MetricsAggregator,
7-
Primitive,
8-
} from '@sentry/types';
1+
import type { Client, ClientOptions, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types';
92
import { timestampInSeconds } from '@sentry/utils';
10-
import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants';
3+
import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
114
import { METRIC_MAP } from './instance';
5+
import { updateMetricSummaryOnActiveSpan } from './metric-summary';
126
import type { MetricBucket, MetricType } from './types';
137
import { getBucketKey, sanitizeTags } from './utils';
148

@@ -46,24 +40,33 @@ export class BrowserMetricsAggregator implements MetricsAggregator {
4640
const tags = sanitizeTags(unsanitizedTags);
4741

4842
const bucketKey = getBucketKey(metricType, name, unit, tags);
49-
const bucketItem: MetricBucketItem | undefined = this._buckets.get(bucketKey);
43+
44+
let bucketItem = this._buckets.get(bucketKey);
45+
// If this is a set metric, we need to calculate the delta from the previous weight.
46+
const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0;
47+
5048
if (bucketItem) {
5149
bucketItem.metric.add(value);
5250
// TODO(abhi): Do we need this check?
5351
if (bucketItem.timestamp < timestamp) {
5452
bucketItem.timestamp = timestamp;
5553
}
5654
} else {
57-
this._buckets.set(bucketKey, {
55+
bucketItem = {
5856
// @ts-expect-error we don't need to narrow down the type of value here, saves bundle size.
5957
metric: new METRIC_MAP[metricType](value),
6058
timestamp,
6159
metricType,
6260
name,
6361
unit,
6462
tags,
65-
});
63+
};
64+
this._buckets.set(bucketKey, bucketItem);
6665
}
66+
67+
// If value is a string, it's a set metric so calculate the delta from the previous weight.
68+
const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value;
69+
updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey);
6770
}
6871

6972
/**
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { MeasurementUnit, Span } from '@sentry/types';
2+
import type { MetricSummary } from '@sentry/types';
3+
import type { Primitive } from '@sentry/types';
4+
import { dropUndefinedKeys } from '@sentry/utils';
5+
import { getActiveSpan } from '../tracing';
6+
import type { MetricType } from './types';
7+
8+
/**
9+
* key: bucketKey
10+
* value: [exportKey, MetricSummary]
11+
*/
12+
type MetricSummaryStorage = Map<string, [string, MetricSummary]>;
13+
14+
let SPAN_METRIC_SUMMARY: WeakMap<Span, MetricSummaryStorage> | undefined;
15+
16+
function getMetricStorageForSpan(span: Span): MetricSummaryStorage | undefined {
17+
return SPAN_METRIC_SUMMARY ? SPAN_METRIC_SUMMARY.get(span) : undefined;
18+
}
19+
20+
/**
21+
* Fetches the metric summary if it exists for the passed span
22+
*/
23+
export function getMetricSummaryJsonForSpan(span: Span): Record<string, MetricSummary> | undefined {
24+
const storage = getMetricStorageForSpan(span);
25+
26+
if (!storage) {
27+
return undefined;
28+
}
29+
const output: Record<string, MetricSummary> = {};
30+
31+
for (const [, [exportKey, summary]] of storage) {
32+
output[exportKey] = dropUndefinedKeys(summary);
33+
}
34+
35+
return output;
36+
}
37+
38+
/**
39+
* Updates the metric summary on the currently active span
40+
*/
41+
export function updateMetricSummaryOnActiveSpan(
42+
metricType: MetricType,
43+
sanitizedName: string,
44+
value: number,
45+
unit: MeasurementUnit,
46+
tags: Record<string, Primitive>,
47+
bucketKey: string,
48+
): void {
49+
const span = getActiveSpan();
50+
if (span) {
51+
const storage = getMetricStorageForSpan(span) || new Map<string, [string, MetricSummary]>();
52+
53+
const exportKey = `${metricType}:${sanitizedName}@${unit}`;
54+
const bucketItem = storage.get(bucketKey);
55+
56+
if (bucketItem) {
57+
const [, summary] = bucketItem;
58+
storage.set(bucketKey, [
59+
exportKey,
60+
{
61+
min: Math.min(summary.min, value),
62+
max: Math.max(summary.max, value),
63+
count: (summary.count += 1),
64+
sum: (summary.sum += value),
65+
tags: summary.tags,
66+
},
67+
]);
68+
} else {
69+
storage.set(bucketKey, [
70+
exportKey,
71+
{
72+
min: value,
73+
max: value,
74+
count: 1,
75+
sum: value,
76+
tags,
77+
},
78+
]);
79+
}
80+
81+
if (!SPAN_METRIC_SUMMARY) {
82+
SPAN_METRIC_SUMMARY = new WeakMap();
83+
}
84+
85+
SPAN_METRIC_SUMMARY.set(span, storage);
86+
}
87+
}

packages/core/src/tracing/span.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
1717

1818
import { DEBUG_BUILD } from '../debug-build';
19+
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
1920
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
2021
import { getRootSpan } from '../utils/getRootSpan';
2122
import {
@@ -624,6 +625,7 @@ export class Span implements SpanInterface {
624625
timestamp: this._endTime,
625626
trace_id: this._traceId,
626627
origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined,
628+
_metrics_summary: getMetricSummaryJsonForSpan(this),
627629
});
628630
}
629631

packages/core/src/tracing/transaction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { dropUndefinedKeys, logger } from '@sentry/utils';
1515
import { DEBUG_BUILD } from '../debug-build';
1616
import type { Hub } from '../hub';
1717
import { getCurrentHub } from '../hub';
18+
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
1819
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
1920
import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils';
2021
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
@@ -331,6 +332,7 @@ export class Transaction extends SpanClass implements TransactionInterface {
331332
capturedSpanIsolationScope,
332333
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
333334
},
335+
_metrics_summary: getMetricSummaryJsonForSpan(this),
334336
...(source && {
335337
transaction_info: {
336338
source,

packages/types/src/event.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { Request } from './request';
1111
import type { CaptureContext } from './scope';
1212
import type { SdkInfo } from './sdkinfo';
1313
import type { SeverityLevel } from './severity';
14-
import type { Span, SpanJSON } from './span';
14+
import type { MetricSummary, Span, SpanJSON } from './span';
1515
import type { Thread } from './thread';
1616
import type { TransactionSource } from './transaction';
1717
import type { User } from './user';
@@ -72,6 +72,7 @@ export interface ErrorEvent extends Event {
7272
}
7373
export interface TransactionEvent extends Event {
7474
type: 'transaction';
75+
_metrics_summary?: Record<string, MetricSummary>;
7576
}
7677

7778
/** JSDoc */

packages/types/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export type {
9898
SpanJSON,
9999
SpanContextData,
100100
TraceFlag,
101+
MetricSummary,
101102
} from './span';
102103
export type { StackFrame } from './stackframe';
103104
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace';
@@ -149,5 +150,9 @@ export type {
149150

150151
export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './browseroptions';
151152
export type { CheckIn, MonitorConfig, FinishedCheckIn, InProgressCheckIn, SerializedCheckIn } from './checkin';
152-
export type { MetricsAggregator, MetricBucketItem, MetricInstance } from './metrics';
153+
export type {
154+
MetricsAggregator,
155+
MetricBucketItem,
156+
MetricInstance,
157+
} from './metrics';
153158
export type { ParameterizedString } from './parameterize';

packages/types/src/span.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ export type SpanAttributes = Partial<{
3131
}> &
3232
Record<string, SpanAttributeValue | undefined>;
3333

34+
export type MetricSummary = {
35+
min: number;
36+
max: number;
37+
count: number;
38+
sum: number;
39+
tags?: Record<string, Primitive> | undefined;
40+
};
41+
3442
/** This type is aligned with the OpenTelemetry TimeInput type. */
3543
export type SpanTimeInput = HrTime | number | Date;
3644

@@ -47,6 +55,7 @@ export interface SpanJSON {
4755
timestamp?: number;
4856
trace_id: string;
4957
origin?: SpanOrigin;
58+
_metrics_summary?: Record<string, MetricSummary>;
5059
}
5160

5261
// These are aligned with OpenTelemetry trace flags

0 commit comments

Comments
 (0)