Skip to content

Commit 44ba6f3

Browse files
committed
Bug 1716956: Implement the String List metric type
1 parent a1cd532 commit 44ba6f3

File tree

7 files changed

+362
-0
lines changed

7 files changed

+362
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* [#580](https://github.com/mozilla/glean.js/pull/580): BUGFIX: Pending pings at startup up are uploaded from oldest to newest.
88
* [#607](https://github.com/mozilla/glean.js/pull/607): Record an error when incoherent timestamps are calculated for events after a restart.
99
* [#630](https://github.com/mozilla/glean.js/pull/630): Accept booleans and numbers as event extras.
10+
* [#614](https://github.com/mozilla/glean.js/pull/614): Implement the String List metric type.
1011

1112
# v0.18.1 (2021-07-22)
1213

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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 type { CommonMetricData } from "../index.js";
6+
import { MetricType } from "../index.js";
7+
import { Context } from "../../context.js";
8+
import { Metric } from "../metric.js";
9+
import { isString, JSONValue, truncateStringAtBoundaryWithError } from "../../utils.js";
10+
import { ErrorType } from "../../error/error_type.js";
11+
12+
export const MAX_LIST_LENGTH = 20;
13+
export const MAX_STRING_LENGTH = 50;
14+
15+
export class StringListMetric extends Metric<string[], string[]> {
16+
constructor(v: unknown) {
17+
super(v);
18+
}
19+
20+
validate(v: unknown): v is string[] {
21+
if (!Array.isArray(v)) {
22+
return false;
23+
}
24+
25+
if (v.length > MAX_LIST_LENGTH) {
26+
return false;
27+
}
28+
for (const s of v) {
29+
if (!isString(s) || s.length > MAX_STRING_LENGTH) {
30+
return false;
31+
}
32+
}
33+
34+
return true;
35+
}
36+
37+
payload(): string[] {
38+
return this._inner;
39+
}
40+
}
41+
42+
/**
43+
* A string list metric.
44+
*
45+
* This allows appending a string value with arbitrary content to a list.
46+
* The list is length-limited to `MAX_LIST_LENGTH`.
47+
* Strings are length-limited to `MAX_STRING_LENGTH` bytes.
48+
*/
49+
class StringListMetricType extends MetricType {
50+
constructor(meta: CommonMetricData) {
51+
super("string_list", meta);
52+
}
53+
54+
/**
55+
* Sets to the specified string list value.
56+
*
57+
* # Note
58+
*
59+
* Truncates the list if it is longer than `MAX_LIST_LENGTH` and logs an error.
60+
*
61+
* Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes
62+
* and logs an error.
63+
*
64+
* @param value the list of string to set the metric to.
65+
*/
66+
set(value: string[]): void {
67+
Context.dispatcher.launch(async () => {
68+
if (!this.shouldRecord(Context.uploadEnabled)) {
69+
return;
70+
}
71+
const truncatedList: string[] = [];
72+
if (value.length > MAX_LIST_LENGTH) {
73+
await Context.errorManager.record(
74+
this,
75+
ErrorType.InvalidValue,
76+
`String list length of ${value.length} exceeds maximum of ${MAX_LIST_LENGTH}.`
77+
);
78+
}
79+
80+
for (let i = 0; i < Math.min(value.length, MAX_LIST_LENGTH); ++i) {
81+
const truncatedString = await truncateStringAtBoundaryWithError(this, value[i], MAX_STRING_LENGTH);
82+
truncatedList.push(truncatedString)
83+
}
84+
const metric = new StringListMetric(truncatedList);
85+
await Context.metricsDatabase.record(this, metric);
86+
});
87+
}
88+
89+
/**
90+
* Adds a new string `value` to the list.
91+
*
92+
* # Note
93+
*
94+
* - If the list is already of length `MAX_LIST_LENGTH`, log an error.
95+
* - Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes
96+
* and logs an error.
97+
*
98+
* @param value The string to add.
99+
*/
100+
add(value: string): void {
101+
Context.dispatcher.launch(async () => {
102+
if (!this.shouldRecord(Context.uploadEnabled)) {
103+
return;
104+
}
105+
106+
const truncatedValue = await truncateStringAtBoundaryWithError(this, value, MAX_STRING_LENGTH);
107+
108+
const transformFn = ((value) => {
109+
return (v?: JSONValue): StringListMetric => {
110+
let metric: StringListMetric;
111+
let result: string[];
112+
try {
113+
metric = new StringListMetric(v);
114+
result = metric.get();
115+
if (result.length < MAX_LIST_LENGTH) {
116+
result.push(value);
117+
} else {
118+
Context.errorManager.record(
119+
this,
120+
ErrorType.InvalidValue,
121+
`String list length of ${result.length}+1 exceeds maximum of ${MAX_LIST_LENGTH}.`
122+
);
123+
}
124+
} catch {
125+
metric = new StringListMetric([value]);
126+
result = [value];
127+
}
128+
129+
metric.set(result);
130+
return metric
131+
}
132+
})(truncatedValue);
133+
134+
await Context.metricsDatabase.transform(this, transformFn);
135+
});
136+
}
137+
138+
/**
139+
* Test-only API**
140+
*
141+
* Gets the currently stored value as a string array.
142+
*
143+
* This doesn't clear the stored value.
144+
*
145+
* TODO: Only allow this function to be called on test mode (depends on Bug 1682771).
146+
*
147+
* @param ping the ping from which we want to retrieve this metrics value from.
148+
* Defaults to the first value in `sendInPings`.
149+
* @returns The value found in storage or `undefined` if nothing was found.
150+
*/
151+
async testGetValue(ping: string = this.sendInPings[0]): Promise<string[] | undefined> {
152+
let metric: string[] | undefined;
153+
await Context.dispatcher.testLaunch(async () => {
154+
metric = await Context.metricsDatabase.getMetric<string[]>(ping, this);
155+
});
156+
return metric;
157+
}
158+
}
159+
160+
export default StringListMetricType;

glean/src/core/metrics/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CounterMetric } from "./types/counter.js";
1212
import { DatetimeMetric } from "./types/datetime.js";
1313
import { QuantityMetric } from "./types/quantity.js";
1414
import { StringMetric } from "./types/string.js";
15+
import { StringListMetric } from "./types/string_list.js";
1516
import { TimespanMetric } from "./types/timespan.js";
1617
import { UrlMetric } from "./types/url.js";
1718
import { UUIDMetric } from "./types/uuid.js";
@@ -30,6 +31,7 @@ const METRIC_MAP: {
3031
"labeled_string": LabeledMetric,
3132
"quantity": QuantityMetric,
3233
"string": StringMetric,
34+
"string_list": StringListMetric,
3335
"timespan": TimespanMetric,
3436
"url": UrlMetric,
3537
"uuid": UUIDMetric,

glean/src/index/qt.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import EventMetricType from "../core/metrics/types/event.js";
1717
import LabeledMetricType from "../core/metrics/types/labeled.js";
1818
import QuantityMetricType from "../core/metrics/types/quantity.js";
1919
import StringMetricType from "../core/metrics/types/string.js";
20+
import StringListMetricType from "../core/metrics/types/string_list";
2021
import TimespanMetricType from "../core/metrics/types/timespan.js";
2122
import UUIDMetricType from "../core/metrics/types/uuid.js";
2223
import URLMetricType from "../core/metrics/types/url.js";
@@ -123,6 +124,7 @@ export default {
123124
LabeledMetricType,
124125
QuantityMetricType,
125126
StringMetricType,
127+
StringListMetricType,
126128
TimespanMetricType,
127129
UUIDMetricType,
128130
URLMetricType

glean/tests/integration/schema/metrics.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ for_testing:
8484
expires: never
8585
send_in_pings:
8686
- testing
87+
string_list:
88+
type: string_list
89+
description: |
90+
Sample string list metric.
91+
bugs:
92+
- https://bugzilla.mozilla.org/000000
93+
data_reviews:
94+
- https://bugzilla.mozilla.org/show_bug.cgi?id=000000#c3
95+
notification_emails:
96+
- me@mozilla.com
97+
expires: never
98+
send_in_pings:
99+
- testing
87100
timespan:
88101
type: timespan
89102
description: |

glean/tests/integration/schema/schema.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe("schema", function() {
7070
metrics.labeledString["a_label"].set("ho");
7171
metrics.quantity.set(42);
7272
metrics.string.set("let's go");
73+
metrics.stringList.set(["let's go"]);
7374
metrics.timespan.setRawNanos(10 * 10**6);
7475
metrics.uuid.generateAndSet();
7576
metrics.url.set("glean://test");

0 commit comments

Comments
 (0)