/
string_list.ts
227 lines (200 loc) · 6.54 KB
/
string_list.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import type { CommonMetricData } from "../index.js";
import type { MetricValidationResult } from "../metric.js";
import type { JSONValue } from "../../utils.js";
import log from "../../log.js";
import { Context } from "../../context.js";
import { ErrorType } from "../../error/error_type.js";
import { MetricType } from "../index.js";
import { Metric, MetricValidation, MetricValidationError } from "../metric.js";
import { validateString } from "../utils.js";
import { testOnlyCheck, truncateStringAtBytesBoundaryWithError } from "../../utils.js";
const LOG_TAG = "core.metrics.StringListMetricType";
export const MAX_LIST_LENGTH = 20;
export const MAX_STRING_LENGTH_IN_BYTES = 100;
export class StringListMetric extends Metric<string[], string[]> {
constructor(v: unknown) {
super(v);
}
validate(v: unknown): MetricValidationResult {
if (!Array.isArray(v)) {
return {
type: MetricValidation.Error,
errorMessage: `Expected array, got ${JSON.stringify(v)}`
};
}
for (const s of v) {
const validation = validateString(s);
if (validation.type === MetricValidation.Error) {
return validation;
}
}
return { type: MetricValidation.Success };
}
concat(list: unknown): void {
const correctedList = this.validateOrThrow(list);
const result = [...this.inner, ...correctedList];
if (result.length > MAX_LIST_LENGTH) {
throw new MetricValidationError(
`String list length of ${result.length} would exceed maximum of ${MAX_LIST_LENGTH}.`,
ErrorType.InvalidValue
);
}
this.inner = result;
}
payload(): string[] {
return this.inner;
}
}
/**
* Base implementation of the string list metric type,
* meant only for Glean internal use.
*
* This class exposes Glean-internal properties and methods
* of the string list metric type.
*/
class InternalStringListMetricType extends MetricType {
constructor(meta: CommonMetricData) {
super("string_list", meta, StringListMetric);
}
set(value: string[]): void {
if (!this.shouldRecord(Context.uploadEnabled)) {
return;
}
try {
if (value.length > MAX_LIST_LENGTH) {
Context.errorManager.record(
this,
ErrorType.InvalidValue,
`String list length of ${value.length} exceeds maximum of ${MAX_LIST_LENGTH}.`
);
}
// Create metric here, in order to run the validations and throw in case input in invalid.
const metric = new StringListMetric(value);
const truncatedList: string[] = [];
for (let i = 0; i < Math.min(value.length, MAX_LIST_LENGTH); ++i) {
const truncatedString = truncateStringAtBytesBoundaryWithError(
this,
value[i],
MAX_STRING_LENGTH_IN_BYTES
);
truncatedList.push(truncatedString);
}
metric.set(truncatedList);
Context.metricsDatabase.record(this, metric);
} catch (e) {
if (e instanceof MetricValidationError) {
e.recordError(this);
}
}
}
add(value: string): void {
if (!this.shouldRecord(Context.uploadEnabled)) {
return;
}
try {
const truncatedValue = truncateStringAtBytesBoundaryWithError(this, value, MAX_STRING_LENGTH_IN_BYTES);
Context.metricsDatabase.transform(
this,
this.addTransformFn(truncatedValue)
);
} catch (e) {
if (e instanceof MetricValidationError) {
e.recordError(this);
}
}
}
private addTransformFn(value: string) {
return (v?: JSONValue): StringListMetric => {
const metric = new StringListMetric([value]);
try {
v && metric.concat(v);
} catch (e) {
if (e instanceof MetricValidationError && e.type !== ErrorType.InvalidType) {
// We only want to bubble up errors that are not invalid type,
// those are only useful if it was the user that passed on an incorrect value
// and in this context that would mean there is invalid data in the database.
throw e;
} else {
log(
LOG_TAG,
`Unexpected value found in storage for metric ${this.name}: ${JSON.stringify(
v
)}. Overwriting.`
);
}
}
return metric;
};
}
/// TESTING ///
testGetValue(ping: string = this.sendInPings[0]): string[] | undefined {
if (testOnlyCheck("testGetValue", LOG_TAG)) {
return Context.metricsDatabase.getMetric<string[]>(ping, this);
}
}
}
export default class {
#inner: InternalStringListMetricType;
constructor(meta: CommonMetricData) {
this.#inner = new InternalStringListMetricType(meta);
}
/**
* Sets to the specified string list value.
*
* # Note
*
* Truncates the list if it is longer than `MAX_LIST_LENGTH` and records an error.
*
* Truncates the value if it is longer than `MAX_STRING_LENGTH` characters
* and records an error.
*
* @param value The list of strings to set the metric to.
*/
set(value: string[]): void {
this.#inner.set(value);
}
/**
* Adds a new string `value` to the list.
*
* # Note
*
* - If the list is already of length `MAX_LIST_LENGTH`, record an error.
* - Truncates the value if it is longer than `MAX_STRING_LENGTH` characters
* and records an error.
*
* @param value The string to add.
*/
add(value: string): void {
this.#inner.add(value);
}
/**
* Test-only API
*
* Gets the currently stored value as a string array.
*
* This doesn't clear the stored value.
*
* @param ping the ping from which we want to retrieve this metrics value from.
* Defaults to the first value in `sendInPings`.
* @returns The value found in storage or `undefined` if nothing was found.
*/
testGetValue(ping: string = this.#inner.sendInPings[0]): string[] | undefined {
return this.#inner.testGetValue(ping);
}
/**
* Test-only API
*
* Returns the number of errors recorded for the given metric.
*
* @param errorType The type of the error recorded.
* @param ping represents the name of the ping to retrieve the metric for.
* Defaults to the first value in `sendInPings`.
* @returns the number of errors recorded for the metric.
*/
testGetNumRecordedErrors(errorType: string, ping: string = this.#inner.sendInPings[0]): number {
return this.#inner.testGetNumRecordedErrors(errorType, ping);
}
}