Skip to content

Commit c8bc80b

Browse files
authored
feat(core): Update metric normalization (#11518)
Closes #11433 https://develop.sentry.dev/sdk/metrics/#normalization
1 parent 481debb commit c8bc80b

File tree

5 files changed

+94
-34
lines changed

5 files changed

+94
-34
lines changed

packages/core/src/metrics/aggregator.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { Client, MeasurementUnit, MetricsAggregator as MetricsAggregatorBase, Primitive } from '@sentry/types';
22
import { timestampInSeconds } from '@sentry/utils';
33
import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils';
4-
import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
4+
import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, SET_METRIC_TYPE } from './constants';
55
import { captureAggregateMetrics } from './envelope';
66
import { METRIC_MAP } from './instance';
77
import type { MetricBucket, MetricType } from './types';
8-
import { getBucketKey, sanitizeTags } from './utils';
8+
import { getBucketKey, sanitizeMetricKey, sanitizeTags, sanitizeUnit } from './utils';
99

1010
/**
1111
* A metrics aggregator that aggregates metrics in memory and flushes them periodically.
@@ -46,6 +46,7 @@ export class MetricsAggregator implements MetricsAggregatorBase {
4646
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
4747
this._interval.unref();
4848
}
49+
4950
this._flushShift = Math.floor((Math.random() * DEFAULT_FLUSH_INTERVAL) / 1000);
5051
this._forceFlush = false;
5152
}
@@ -57,13 +58,14 @@ export class MetricsAggregator implements MetricsAggregatorBase {
5758
metricType: MetricType,
5859
unsanitizedName: string,
5960
value: number | string,
60-
unit: MeasurementUnit = 'none',
61+
unsanitizedUnit: MeasurementUnit = 'none',
6162
unsanitizedTags: Record<string, Primitive> = {},
6263
maybeFloatTimestamp = timestampInSeconds(),
6364
): void {
6465
const timestamp = Math.floor(maybeFloatTimestamp);
65-
const name = unsanitizedName.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_');
66+
const name = sanitizeMetricKey(unsanitizedName);
6667
const tags = sanitizeTags(unsanitizedTags);
68+
const unit = sanitizeUnit(unsanitizedUnit as string);
6769

6870
const bucketKey = getBucketKey(metricType, name, unit, tags);
6971

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { Client, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types';
22
import { timestampInSeconds } from '@sentry/utils';
33
import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils';
4-
import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
4+
import { DEFAULT_BROWSER_FLUSH_INTERVAL, SET_METRIC_TYPE } from './constants';
55
import { captureAggregateMetrics } from './envelope';
66
import { METRIC_MAP } from './instance';
77
import type { MetricBucket, MetricType } from './types';
8-
import { getBucketKey, sanitizeTags } from './utils';
8+
import { getBucketKey, sanitizeMetricKey, sanitizeTags, sanitizeUnit } from './utils';
99

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

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

@@ -79,8 +80,7 @@ export class BrowserMetricsAggregator implements MetricsAggregator {
7980
return;
8081
}
8182

82-
// TODO(@anonrig): Use Object.values() when we support ES6+
83-
const metricBuckets = Array.from(this._buckets).map(([, bucketItem]) => bucketItem);
83+
const metricBuckets = Array.from(this._buckets.values());
8484
captureAggregateMetrics(this._client, metricBuckets);
8585

8686
this._buckets.clear();

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: 59 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,72 @@ export function serializeMetricBuckets(metricBucketItems: MetricBucketItem[]): s
5453
return out;
5554
}
5655

56+
/**
57+
* Sanitizes units
58+
*
59+
* These Regex's are straight from the normalisation docs:
60+
* https://develop.sentry.dev/sdk/metrics/#normalization
61+
*/
62+
export function sanitizeUnit(unit: string): string {
63+
return unit.replace(/[^\w]+/gi, '_');
64+
}
65+
66+
/**
67+
* Sanitizes metric keys
68+
*
69+
* These Regex's are straight from the normalisation docs:
70+
* https://develop.sentry.dev/sdk/metrics/#normalization
71+
*/
72+
export function sanitizeMetricKey(key: string): string {
73+
return key.replace(/[^\w\-.]+/gi, '_');
74+
}
75+
76+
/**
77+
* Sanitizes metric keys
78+
*
79+
* These Regex's are straight from the normalisation docs:
80+
* https://develop.sentry.dev/sdk/metrics/#normalization
81+
*/
82+
function sanitizeTagKey(key: string): string {
83+
return key.replace(/[^\w\-./]+/gi, '');
84+
}
85+
86+
/**
87+
* These Regex's are straight from the normalisation docs:
88+
* https://develop.sentry.dev/sdk/metrics/#normalization
89+
*/
90+
const tagValueReplacements: [string, string][] = [
91+
['\n', '\\n'],
92+
['\r', '\\r'],
93+
['\t', '\\t'],
94+
['\\', '\\\\'],
95+
['|', '\\u{7c}'],
96+
[',', '\\u{2c}'],
97+
];
98+
99+
function getCharOrReplacement(input: string): string {
100+
for (const [search, replacement] of tagValueReplacements) {
101+
if (input === search) {
102+
return replacement;
103+
}
104+
}
105+
106+
return input;
107+
}
108+
109+
function sanitizeTagValue(value: string): string {
110+
return [...value].reduce((acc, char) => acc + getCharOrReplacement(char), '');
111+
}
112+
57113
/**
58114
* Sanitizes tags.
59115
*/
60116
export function sanitizeTags(unsanitizedTags: Record<string, Primitive>): Record<string, string> {
61117
const tags: Record<string, string> = {};
62118
for (const key in unsanitizedTags) {
63119
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, '');
120+
const sanitizedKey = sanitizeTagKey(key);
121+
tags[sanitizedKey] = sanitizeTagValue(String(unsanitizedTags[key]));
66122
}
67123
}
68124
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)