Skip to content

Commit 158b7a0

Browse files
committed
Introduce the labeled metric type
This supports string, boolean and counter labeled types and ports over tests from the Kotlin implementation in the SDK and the glean-core internals.
1 parent 9c9a8bd commit 158b7a0

File tree

2 files changed

+558
-0
lines changed

2 files changed

+558
-0
lines changed
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { CommonMetricData, MetricType } from "..";
6+
import Glean from "../../glean";
7+
import CounterMetricType from "./counter";
8+
import BooleanMetricType from "./boolean";
9+
import StringMetricType from "./string";
10+
11+
const MAX_LABELS = 16;
12+
const OTHER_LABEL = "__other__";
13+
const MAX_LABEL_LENGTH = 61;
14+
15+
// ** IMPORTANT **
16+
// When changing this documentation or the regex, be sure to change the same code
17+
// in the Glean SDK repository as well.
18+
//
19+
// This regex is used for matching against labels and should allow for dots,
20+
// underscores, and/or hyphens. Labels are also limited to starting with either
21+
// a letter or an underscore character.
22+
//
23+
// Some examples of good and bad labels:
24+
//
25+
// Good:
26+
// * `this.is.fine`
27+
// * `this_is_fine_too`
28+
// * `this.is_still_fine`
29+
// * `thisisfine`
30+
// * `_.is_fine`
31+
// * `this.is-fine`
32+
// * `this-is-fine`
33+
// Bad:
34+
// * `this.is.not_fine_due_tu_the_length_being_too_long_i_thing.i.guess`
35+
// * `1.not_fine`
36+
// * `this.$isnotfine`
37+
// * `-.not_fine`
38+
const LABEL_REGEX = /^[a-z_][a-z0-9_-]{0,29}(\.[a-z_][a-z0-9_-]{0,29})*$/;
39+
40+
type SupportedLabeledTypes = CounterMetricType | BooleanMetricType | StringMetricType;
41+
42+
class LabeledMetricType<T extends SupportedLabeledTypes> {
43+
// Define an index signature to make the Proxy aware of the expected return type.
44+
// Note that this is required because TypeScript does not allow different input and
45+
// output types in Proxy (https://github.com/microsoft/TypeScript/issues/20846).
46+
[label: string]: T;
47+
48+
constructor(
49+
meta: CommonMetricData,
50+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51+
submetric: new (...args: any) => T,
52+
labels?: string[],
53+
) {
54+
return new Proxy(this, {
55+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
get: (_target: LabeledMetricType<T>, label: string): any => {
57+
if (labels) {
58+
return LabeledMetricType.createFromStaticLabel<typeof submetric>(meta, submetric, labels, label);
59+
}
60+
61+
return LabeledMetricType.createFromDynamicLabel<typeof submetric>(meta, submetric, label);
62+
}
63+
});
64+
}
65+
66+
/**
67+
* Combines a metric's base identifier and label.
68+
*
69+
* @param metricName the metric base identifier
70+
* @param label the label
71+
*
72+
* @returns a string representing the complete metric id including the label.
73+
*/
74+
private static combineIdentifierAndLabel(
75+
metricName: string,
76+
label: string
77+
): string {
78+
return `${metricName}/${label}`;
79+
}
80+
81+
/**
82+
* Create an instance of the submetric type for the provided static label.
83+
*
84+
* @param meta the `CommonMetricData` information for the metric.
85+
* @param submetricClass the class type for the submetric.
86+
* @param allowedLabels the array of allowed labels.
87+
* @param label the desired label to record to.
88+
*
89+
* @returns an instance of the submetric class type that allows to record data.
90+
*/
91+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
92+
private static createFromStaticLabel<T extends new (...args: any) => InstanceType<T>>(
93+
meta: CommonMetricData,
94+
submetricClass: T,
95+
allowedLabels: string[],
96+
label: string
97+
): T {
98+
// If the label was provided in the registry file, then use it. Otherwise,
99+
// store data in the `OTHER_LABEL`.
100+
const adjustedLabel = allowedLabels.includes(label) ? label : OTHER_LABEL;
101+
const newMeta: CommonMetricData = {
102+
...meta,
103+
name: LabeledMetricType.combineIdentifierAndLabel(meta.name, adjustedLabel)
104+
};
105+
return new submetricClass(newMeta);
106+
}
107+
108+
/**
109+
* Create an instance of the submetric type for the provided dynamic label.
110+
*
111+
* @param meta the `CommonMetricData` information for the metric.
112+
* @param submetricClass the class type for the submetric.
113+
* @param label the desired label to record to.
114+
*
115+
* @returns an instance of the submetric class type that allows to record data.
116+
*/
117+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
118+
private static createFromDynamicLabel<T extends new (...args: any) => InstanceType<T>>(
119+
meta: CommonMetricData,
120+
submetricClass: T,
121+
label: string
122+
): T {
123+
const newMeta: CommonMetricData = {
124+
...meta,
125+
dynamicLabel: label
126+
};
127+
return new submetricClass(newMeta);
128+
}
129+
130+
static async getValidDynamicLabel(metric: MetricType): Promise<string> {
131+
// Note that we assume `metric.dynamicLabel` to always be available within this function.
132+
// This is a safe assumptions because we should only call `getValidDynamicLabel` if we have
133+
// a dynamic label.
134+
if (metric.dynamicLabel === undefined) {
135+
throw new Error("This point should never be reached.");
136+
}
137+
138+
const key = LabeledMetricType.combineIdentifierAndLabel(metric.baseIdentifier(), metric.dynamicLabel);
139+
140+
for (const ping of metric.sendInPings) {
141+
if (await Glean.metricsDatabase.hasMetric(metric.lifetime, ping, metric.type, key)) {
142+
return key;
143+
}
144+
}
145+
146+
let numUsedKeys = 0;
147+
for (const ping of metric.sendInPings) {
148+
numUsedKeys += await Glean.metricsDatabase.countByBaseIdentifier(
149+
metric.lifetime,
150+
ping,
151+
metric.type,
152+
metric.baseIdentifier());
153+
}
154+
155+
let hitError = false;
156+
if (numUsedKeys >= MAX_LABELS) {
157+
hitError = true;
158+
} else if (metric.dynamicLabel.length > MAX_LABEL_LENGTH) {
159+
console.error(`label length ${metric.dynamicLabel.length} exceeds maximum of ${MAX_LABEL_LENGTH}`);
160+
hitError = true;
161+
// TODO: record error in bug 1682574
162+
} else if (!LABEL_REGEX.test(metric.dynamicLabel)) {
163+
console.error(`label must be snake_case, got '${metric.dynamicLabel}'`);
164+
hitError = true;
165+
// TODO: record error in bug 1682574
166+
}
167+
168+
return (hitError)
169+
? LabeledMetricType.combineIdentifierAndLabel(metric.baseIdentifier(), OTHER_LABEL)
170+
: key;
171+
}
172+
}
173+
174+
export default LabeledMetricType;

0 commit comments

Comments
 (0)