Skip to content

Commit e50f91a

Browse files
author
Beatriz Rizental
authored
Bug 1682574 - Implement error recording (#313)
1 parent 70a2b6c commit e50f91a

23 files changed

+439
-56
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
[Full changelog](https://github.com/mozilla/glean.js/compare/v0.12.0...main)
44

5+
* [#313](https://github.com/mozilla/glean.js/pull/313): Implement error recording mechanism and error checking testing API.
6+
57
# v0.12.0 (2021-05-11)
68

79
[Full changelog](https://github.com/mozilla/glean.js/compare/v0.11.0...v0.12.0)

glean/src/core/context.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Dispatcher from "./dispatcher.js";
77
import type MetricsDatabase from "./metrics/database";
88
import type EventsDatabase from "./metrics/events_database";
99
import type PingsDatabase from "./pings/database";
10+
import type ErrorManager from "./error";
1011

1112
/**
1213
* This class holds all of the Glean singleton's state and internal dependencies.
@@ -29,6 +30,10 @@ export class Context {
2930
private _metricsDatabase!: MetricsDatabase;
3031
private _eventsDatabase!: EventsDatabase;
3132
private _pingsDatabase!: PingsDatabase;
33+
// The reason this was added to the Context,
34+
// is to avoid a circular dependency between
35+
// the ErrorManager's module and the CounterMetricType module.
36+
private _errorManager!: ErrorManager;
3237

3338
private _applicationId!: string;
3439
private _initialized: boolean;
@@ -113,6 +118,14 @@ export class Context {
113118
Context.instance._pingsDatabase = db;
114119
}
115120

121+
static get errorManager(): ErrorManager {
122+
return Context.instance._errorManager;
123+
}
124+
125+
static set errorManager(db: ErrorManager) {
126+
Context.instance._errorManager = db;
127+
}
128+
116129
static get applicationId(): string {
117130
return Context.instance._applicationId;
118131
}

glean/src/core/error/error_type.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
/**
6+
* The possible error types for metric recording.
7+
*/
8+
export enum ErrorType {
9+
// For when the value to be recorded does not match the metric-specific restrictions
10+
InvalidValue = "invalid_value",
11+
// For when the label of a labeled metric does not match the restrictions
12+
InvalidLabel = "invalid_label",
13+
// For when the metric caught an invalid state while recording
14+
InvalidState = "invalid_state",
15+
// For when the value to be recorded overflows the metric-specific upper range
16+
InvalidOverflow = "invalid_overflow",
17+
}

glean/src/core/error/index.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 { MetricType } from "../metrics";
6+
import type { ErrorType } from "./error_type.js";
7+
import CounterMetricType from "../metrics/types/counter";
8+
import { combineIdentifierAndLabel, stripLabel } from "../metrics/types/labeled";
9+
10+
/**
11+
* For a given metric, get the metric in which to record errors.
12+
*
13+
* # Important
14+
*
15+
* Errors do not adhere to the usual "maximum label" restriction.
16+
*
17+
* @param metric The metric to record an error for.
18+
* @param error The error type to record.
19+
*
20+
* @returns The metric to record to.
21+
*/
22+
function getErrorMetricForMetric(metric: MetricType, error: ErrorType): CounterMetricType {
23+
const identifier = metric.baseIdentifier();
24+
const name = stripLabel(identifier);
25+
26+
// We don't use the labeled metric type here,
27+
// because we want to bypass the max number of allowed labels.
28+
return new CounterMetricType({
29+
name: combineIdentifierAndLabel(error, name),
30+
category: "glean.error",
31+
lifetime: "ping",
32+
// TODO: Also add the metric ping to the list. Depends on Bug 1710838.
33+
sendInPings: metric.sendInPings,
34+
disabled: false,
35+
});
36+
}
37+
38+
export default class ErrorManager {
39+
/**
40+
* Records an error into Glean.
41+
*
42+
* Errors are recorded as labeled counters in the `glean.error` category.
43+
*
44+
* @param metric The metric to record an error for.
45+
* @param error The error type to record.
46+
* @param message The message to log. This message is not sent with the ping.
47+
* It does not need to include the metric id, as that is automatically
48+
* prepended to the message.
49+
* @param numErrors The number of errors of the same type to report.
50+
*/
51+
async record (
52+
metric: MetricType,
53+
error: ErrorType,
54+
message: string,
55+
numErrors = 1
56+
): Promise<void> {
57+
const errorMetric = getErrorMetricForMetric(metric, error);
58+
console.warn(`${metric.baseIdentifier()}: ${message}`);
59+
if (numErrors > 0) {
60+
await CounterMetricType._private_addUndispatched(errorMetric, numErrors);
61+
} else {
62+
// TODO: Throw error only when in test mode. Depends on Bug 1682771.
63+
}
64+
}
65+
66+
/**
67+
* Gets the number of recorded errors for the given metric and error type.
68+
*
69+
* @param metric The metric to get the number of errors for.
70+
* @param error The error type to get count of.
71+
* @param ping The ping from which we want to retrieve the number of recorded errors.
72+
* Defaults to the first value in `sendInPings`.
73+
*
74+
* @returns The number of errors reported.
75+
*/
76+
async testGetNumRecordedErrors (
77+
metric: MetricType,
78+
error: ErrorType,
79+
ping?: string
80+
): Promise<number> {
81+
const errorMetric = getErrorMetricForMetric(metric, error);
82+
const numErrors = await errorMetric.testGetValue(ping);
83+
return numErrors || 0;
84+
}
85+
}
86+

glean/src/core/glean.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import UUIDMetricType from "./metrics/types/uuid.js";
1515
import DatetimeMetricType, { DatetimeMetric } from "./metrics/types/datetime.js";
1616
import CorePings from "./internal_pings.js";
1717
import { registerPluginToEvent, testResetEvents } from "./events/utils.js";
18-
18+
import ErrorManager from "./error/index.js";
1919
import type Platform from "../platform/index.js";
2020
import TestPlatform from "../platform/test/index.js";
2121
import { Lifetime } from "./metrics/lifetime.js";
@@ -197,6 +197,7 @@ class Glean {
197197
Context.metricsDatabase = new MetricsDatabase(Glean.platform.Storage);
198198
Context.eventsDatabase = new EventsDatabase(Glean.platform.Storage);
199199
Context.pingsDatabase = new PingsDatabase(Glean.platform.Storage);
200+
Context.errorManager = new ErrorManager();
200201

201202
Glean.instance._pingUploader = new PingUploader(correctConfig, Glean.platform, Context.pingsDatabase);
202203

glean/src/core/metrics/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import type { JSONValue } from "../utils.js";
66
import type { Lifetime } from "./lifetime.js";
77
import type MetricsDatabase from "./database.js";
8+
import type { ErrorType } from "../error/error_type.js";
89
import { isUndefined } from "../utils.js";
910
import { getValidDynamicLabel } from "./types/labeled.js";
11+
import { Context } from "../context.js";
1012

1113
export interface Metrics {
1214
[aMetricType: string]: {
@@ -108,4 +110,17 @@ export abstract class MetricType implements CommonMetricData {
108110
shouldRecord(uploadEnabled: boolean): boolean {
109111
return (uploadEnabled && !this.disabled);
110112
}
113+
114+
/**
115+
* Returns the number of errors recorded for the given metric.
116+
*
117+
* @param errorType The type of the error recorded.
118+
* @param ping represents the name of the ping to retrieve the metric for.
119+
* Defaults to the first value in `sendInPings`.
120+
*
121+
* @returns the number of errors recorded for the metric.
122+
*/
123+
async testGetNumRecordedErrors(errorType: string, ping: string = this.sendInPings[0]): Promise<number> {
124+
return Context.errorManager.testGetNumRecordedErrors(this, errorType as ErrorType, ping);
125+
}
111126
}

glean/src/core/metrics/types/counter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MetricType } from "../index.js";
88
import { isUndefined, isNumber } from "../../utils.js";
99
import { Context } from "../../context.js";
1010
import { Metric } from "../metric.js";
11+
import { ErrorType } from "../../error/error_type.js";
1112

1213
export class CounterMetric extends Metric<number, number> {
1314
constructor(v: unknown) {
@@ -64,8 +65,11 @@ class CounterMetricType extends MetricType {
6465
}
6566

6667
if (amount <= 0) {
67-
// TODO: record error once Bug 1682574 is resolved.
68-
console.warn(`Attempted to add an invalid amount ${amount}. Ignoring.`);
68+
await Context.errorManager.record(
69+
instance,
70+
ErrorType.InvalidValue,
71+
`Added negative and zero value ${amount}`
72+
);
6973
return;
7074
}
7175

glean/src/core/metrics/types/event.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import type { CommonMetricData } from "../index.js";
6+
import type { ExtraMap } from "../events_database.js";
67
import { MetricType } from "../index.js";
7-
import type { ExtraMap} from "../events_database.js";
88
import { RecordedEvent } from "../events_database.js";
9-
import { getMonotonicNow } from "../../utils.js";
9+
import { getMonotonicNow, truncateStringAtBoundaryWithError } from "../../utils.js";
1010
import { Context } from "../../context.js";
11+
import { ErrorType } from "../../error/error_type.js";
1112

1213
const MAX_LENGTH_EXTRA_KEY_VALUE = 100;
1314

@@ -45,10 +46,9 @@ class EventMetricType extends MetricType {
4546
truncatedExtra = {};
4647
for (const [name, value] of Object.entries(extra)) {
4748
if (this.allowedExtraKeys.includes(name)) {
48-
truncatedExtra[name] = value.substr(0, MAX_LENGTH_EXTRA_KEY_VALUE);
49+
truncatedExtra[name] = await truncateStringAtBoundaryWithError(this, value, MAX_LENGTH_EXTRA_KEY_VALUE);
4950
} else {
50-
// TODO: bug 1682574 - record an error.
51-
console.error(`Invalid key index ${name}`);
51+
await Context.errorManager.record(this, ErrorType.InvalidValue, `Invalid key index: ${name}`);
5252
continue;
5353
}
5454
}

glean/src/core/metrics/types/labeled.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type StringMetricType from "./string.js";
99
import type MetricsDatabase from "../database";
1010
import type { JSONValue } from "../../utils.js";
1111
import { Metric } from "../metric.js";
12+
import { Context } from "../../context.js";
13+
import { ErrorType } from "../../error/error_type.js";
1214

1315
/**
1416
* This is an internal metric representation for labeled metrics.
@@ -75,13 +77,26 @@ export function combineIdentifierAndLabel(
7577
return `${metricName}/${label}`;
7678
}
7779

80+
/**
81+
* Strips the label from a metric identifier.
82+
*
83+
* This is a no-op in case the identifier does not contain a label.
84+
*
85+
* @param identifier The identifier to strip a label from.
86+
*
87+
* @returns The identifier without the label.
88+
*/
89+
export function stripLabel(identifier: string): string {
90+
return identifier.split("/")[0];
91+
}
92+
7893
/**
7994
* Checks if the dynamic label stored in the metric data is
8095
* valid. If not, record an error and store data in the "__other__"
8196
* label.
8297
*
8398
* @param metricsDatabase the metrics database.
84-
* @param metric the metric metadata.
99+
* @param metric the metric to record to.
85100
*
86101
* @returns a valid label that can be used to store data.
87102
*/
@@ -114,13 +129,19 @@ export async function getValidDynamicLabel(metricsDatabase: MetricsDatabase, met
114129
if (numUsedKeys >= MAX_LABELS) {
115130
hitError = true;
116131
} else if (metric.dynamicLabel.length > MAX_LABEL_LENGTH) {
117-
console.error(`label length ${metric.dynamicLabel.length} exceeds maximum of ${MAX_LABEL_LENGTH}`);
118132
hitError = true;
119-
// TODO: record error in bug 1682574
133+
await Context.errorManager.record(
134+
metric,
135+
ErrorType.InvalidLabel,
136+
`Label length ${metric.dynamicLabel.length} exceeds maximum of ${MAX_LABEL_LENGTH}.`
137+
);
120138
} else if (!LABEL_REGEX.test(metric.dynamicLabel)) {
121-
console.error(`label must be snake_case, got '${metric.dynamicLabel}'`);
122139
hitError = true;
123-
// TODO: record error in bug 1682574
140+
await Context.errorManager.record(
141+
metric,
142+
ErrorType.InvalidLabel,
143+
`Label must be snake_case, got '${metric.dynamicLabel}'.`
144+
);
124145
}
125146

126147
return (hitError)

glean/src/core/metrics/types/quantity.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { MetricType } from "../index.js";
77
import { isNumber } from "../../utils.js";
88
import { Context } from "../../context.js";
99
import { Metric } from "../metric.js";
10+
import { ErrorType } from "../../error/error_type.js";
1011

1112
export class QuantityMetric extends Metric<number, number> {
1213
constructor(v: unknown) {
@@ -59,12 +60,15 @@ class QuantityMetricType extends MetricType {
5960
}
6061

6162
if (value < 0) {
62-
console.warn(`Attempted to set an invalid value ${value}. Ignoring.`);
63+
await Context.errorManager.record(
64+
instance,
65+
ErrorType.InvalidValue,
66+
`Set negative value ${value}`
67+
);
6368
return;
6469
}
6570

6671
if (value > Number.MAX_SAFE_INTEGER) {
67-
console.warn(`Attempted to set a big value ${value}. Capped at ${Number.MAX_SAFE_INTEGER}.`);
6872
value = Number.MAX_SAFE_INTEGER;
6973
}
7074

0 commit comments

Comments
 (0)