Skip to content

Commit 1c0194a

Browse files
authored
feat(core): Update metric normalization (v7) (#11519)
Backport of #11518 for v7
1 parent 5c35031 commit 1c0194a

File tree

5 files changed

+75
-32
lines changed

5 files changed

+75
-32
lines changed

packages/core/src/metrics/aggregator.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ 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, SET_METRIC_TYPE } from './constants';
9+
import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, SET_METRIC_TYPE } from './constants';
1010
import { METRIC_MAP } from './instance';
1111
import { updateMetricSummaryOnActiveSpan } from './metric-summary';
1212
import type { MetricBucket, MetricType } from './types';
13-
import { getBucketKey, sanitizeTags } from './utils';
13+
import { getBucketKey, sanitizeMetricKey, sanitizeTags, sanitizeUnit } from './utils';
1414

1515
/**
1616
* A metrics aggregator that aggregates metrics in memory and flushes them periodically.
@@ -51,6 +51,7 @@ export class MetricsAggregator implements MetricsAggregatorBase {
5151
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
5252
this._interval.unref();
5353
}
54+
5455
this._flushShift = Math.floor((Math.random() * DEFAULT_FLUSH_INTERVAL) / 1000);
5556
this._forceFlush = false;
5657
}
@@ -62,13 +63,14 @@ export class MetricsAggregator implements MetricsAggregatorBase {
6263
metricType: MetricType,
6364
unsanitizedName: string,
6465
value: number | string,
65-
unit: MeasurementUnit = 'none',
66+
unsanitizedUnit: MeasurementUnit = 'none',
6667
unsanitizedTags: Record<string, Primitive> = {},
6768
maybeFloatTimestamp = timestampInSeconds(),
6869
): void {
6970
const timestamp = Math.floor(maybeFloatTimestamp);
70-
const name = unsanitizedName.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_');
71+
const name = sanitizeMetricKey(unsanitizedName);
7172
const tags = sanitizeTags(unsanitizedTags);
73+
const unit = sanitizeUnit(unsanitizedUnit as string);
7274

7375
const bucketKey = getBucketKey(metricType, name, unit, tags);
7476

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Client, ClientOptions, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types';
22
import { timestampInSeconds } from '@sentry/utils';
3-
import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
3+
import { DEFAULT_BROWSER_FLUSH_INTERVAL, SET_METRIC_TYPE } from './constants';
44
import { METRIC_MAP } from './instance';
55
import { updateMetricSummaryOnActiveSpan } from './metric-summary';
66
import type { MetricBucket, MetricType } from './types';
7-
import { getBucketKey, sanitizeTags } from './utils';
7+
import { getBucketKey, sanitizeMetricKey, sanitizeTags, sanitizeUnit } from './utils';
88

99
/**
1010
* A simple metrics aggregator that aggregates metrics in memory and flushes them periodically.
@@ -31,13 +31,14 @@ export class BrowserMetricsAggregator implements MetricsAggregator {
3131
metricType: MetricType,
3232
unsanitizedName: string,
3333
value: number | string,
34-
unit: MeasurementUnit | undefined = 'none',
34+
unsanitizedUnit: MeasurementUnit | undefined = 'none',
3535
unsanitizedTags: Record<string, Primitive> | undefined = {},
3636
maybeFloatTimestamp: number | undefined = timestampInSeconds(),
3737
): void {
3838
const timestamp = Math.floor(maybeFloatTimestamp);
39-
const name = unsanitizedName.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_');
39+
const name = sanitizeMetricKey(unsanitizedName);
4040
const tags = sanitizeTags(unsanitizedTags);
41+
const unit = sanitizeUnit(unsanitizedUnit as string);
4142

4243
const bucketKey = getBucketKey(metricType, name, unit, tags);
4344

@@ -77,11 +78,13 @@ export class BrowserMetricsAggregator implements MetricsAggregator {
7778
if (this._buckets.size === 0) {
7879
return;
7980
}
81+
8082
if (this._client.captureAggregateMetrics) {
8183
// TODO(@anonrig): Use Object.values() when we support ES6+
8284
const metricBuckets = Array.from(this._buckets).map(([, bucketItem]) => bucketItem);
8385
this._client.captureAggregateMetrics(metricBuckets);
8486
}
87+
8588
this._buckets.clear();
8689
}
8790

packages/core/src/metrics/constants.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,6 @@ export const GAUGE_METRIC_TYPE = 'g' as const;
33
export const SET_METRIC_TYPE = 's' as const;
44
export const DISTRIBUTION_METRIC_TYPE = 'd' as const;
55

6-
/**
7-
* Normalization regex for metric names and metric tag names.
8-
*
9-
* This enforces that names and tag keys only contain alphanumeric characters,
10-
* underscores, forward slashes, periods, and dashes.
11-
*
12-
* See: https://develop.sentry.dev/sdk/metrics/#normalization
13-
*/
14-
export const NAME_AND_TAG_KEY_NORMALIZATION_REGEX = /[^a-zA-Z0-9_/.-]+/g;
15-
16-
/**
17-
* Normalization regex for metric tag values.
18-
*
19-
* This enforces that values only contain words, digits, or the following
20-
* special characters: _:/@.{}[\]$-
21-
*
22-
* See: https://develop.sentry.dev/sdk/metrics/#normalization
23-
*/
24-
export const TAG_VALUE_NORMALIZATION_REGEX = /[^\w\d\s_:/@.{}[\]$-]+/g;
25-
266
/**
277
* This does not match spec in https://develop.sentry.dev/sdk/metrics
288
* but was chosen to optimize for the most common case in browser environments.

packages/core/src/metrics/utils.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { MeasurementUnit, MetricBucketItem, Primitive } from '@sentry/types';
22
import { dropUndefinedKeys } from '@sentry/utils';
3-
import { NAME_AND_TAG_KEY_NORMALIZATION_REGEX, TAG_VALUE_NORMALIZATION_REGEX } from './constants';
43
import type { MetricType } from './types';
54

65
/**
@@ -54,15 +53,52 @@ export function serializeMetricBuckets(metricBucketItems: MetricBucketItem[]): s
5453
return out;
5554
}
5655

56+
/** Sanitizes units */
57+
export function sanitizeUnit(unit: string): string {
58+
return unit.replace(/[^\w]+/gi, '_');
59+
}
60+
61+
/** Sanitizes metric keys */
62+
export function sanitizeMetricKey(key: string): string {
63+
return key.replace(/[^\w\-.]+/gi, '_');
64+
}
65+
66+
function sanitizeTagKey(key: string): string {
67+
return key.replace(/[^\w\-./]+/gi, '');
68+
}
69+
70+
const tagValueReplacements: [string, string][] = [
71+
['\n', '\\n'],
72+
['\r', '\\r'],
73+
['\t', '\\t'],
74+
['\\', '\\\\'],
75+
['|', '\\u{7c}'],
76+
[',', '\\u{2c}'],
77+
];
78+
79+
function getCharOrReplacement(input: string): string {
80+
for (const [search, replacement] of tagValueReplacements) {
81+
if (input === search) {
82+
return replacement;
83+
}
84+
}
85+
86+
return input;
87+
}
88+
89+
function sanitizeTagValue(value: string): string {
90+
return [...value].reduce((acc, char) => acc + getCharOrReplacement(char), '');
91+
}
92+
5793
/**
5894
* Sanitizes tags.
5995
*/
6096
export function sanitizeTags(unsanitizedTags: Record<string, Primitive>): Record<string, string> {
6197
const tags: Record<string, string> = {};
6298
for (const key in unsanitizedTags) {
6399
if (Object.prototype.hasOwnProperty.call(unsanitizedTags, key)) {
64-
const sanitizedKey = key.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_');
65-
tags[sanitizedKey] = String(unsanitizedTags[key]).replace(TAG_VALUE_NORMALIZATION_REGEX, '');
100+
const sanitizedKey = sanitizeTagKey(key);
101+
tags[sanitizedKey] = sanitizeTagValue(String(unsanitizedTags[key]));
66102
}
67103
}
68104
return tags;

packages/core/test/lib/metrics/utils.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
GAUGE_METRIC_TYPE,
55
SET_METRIC_TYPE,
66
} from '../../../src/metrics/constants';
7-
import { getBucketKey } from '../../../src/metrics/utils';
7+
import { getBucketKey, sanitizeTags } from '../../../src/metrics/utils';
88

99
describe('getBucketKey', () => {
1010
it.each([
@@ -18,4 +18,26 @@ describe('getBucketKey', () => {
1818
])('should return', (metricType, name, unit, tags, expected) => {
1919
expect(getBucketKey(metricType, name, unit, tags)).toEqual(expected);
2020
});
21+
22+
it('should sanitize tags', () => {
23+
const inputTags = {
24+
'f-oo|bar': '%$foo/',
25+
'foo$.$.$bar': 'blah{}',
26+
'foö-bar': 'snöwmän',
27+
route: 'GET /foo',
28+
__bar__: 'this | or , that',
29+
'foo/': 'hello!\n\r\t\\',
30+
};
31+
32+
const outputTags = {
33+
'f-oobar': '%$foo/',
34+
'foo..bar': 'blah{}',
35+
'fo-bar': 'snöwmän',
36+
route: 'GET /foo',
37+
__bar__: 'this \\u{7c} or \\u{2c} that',
38+
'foo/': 'hello!\\n\\r\\t\\\\',
39+
};
40+
41+
expect(sanitizeTags(inputTags)).toEqual(outputTags);
42+
});
2143
});

0 commit comments

Comments
 (0)