Skip to content

Commit dc544dd

Browse files
author
Beatriz Rizental
authored
Merge pull request #630 from brizental/1693487-event-extras
Bug 1693487 - Accept booleans and numbers as event extras
2 parents c7c1438 + ff2d279 commit dc544dd

File tree

11 files changed

+146
-78
lines changed

11 files changed

+146
-78
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* [#580](https://github.com/mozilla/glean.js/pull/580): Limit size of pings database to 250 pings or 10MB.
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.
9+
* [#630](https://github.com/mozilla/glean.js/pull/630): Accept booleans and numbers as event extras.
910

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

glean/package-lock.json

Lines changed: 0 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glean/src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const LOG_TAG = "CLI";
1919
const VIRTUAL_ENVIRONMENT_DIR = ".venv";
2020

2121
// The version of glean_parser to install from PyPI.
22-
const GLEAN_PARSER_VERSION = "3.7.0";
22+
const GLEAN_PARSER_VERSION = "3.8.0";
2323

2424
// This script runs a given Python module as a "main" module, like
2525
// `python -m module`. However, it first checks that the installed

glean/src/core/metrics/events_database/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import type Store from "../../storage/index.js";
66
import type { JSONArray, JSONObject, JSONValue } from "../../utils.js";
7+
import { isString } from "../../utils.js";
78
import type { StorageBuilder } from "../../../platform/index.js";
89
import { isUndefined } from "../../utils.js";
910
import EventMetricType from "../types/event.js";
@@ -12,6 +13,7 @@ import CounterMetricType from "../types/counter.js";
1213
import { Lifetime } from "../lifetime.js";
1314
import { Context } from "../../context.js";
1415
import { generateReservedMetricIdentifiers } from "../database.js";
16+
import type { ExtraValues} from "./recorded_event.js";
1517
import { RecordedEvent } from "./recorded_event.js";
1618
import {
1719
GLEAN_EXECUTION_COUNTER_EXTRA_KEY,
@@ -29,7 +31,11 @@ const LOG_TAG = "core.Metric.EventsDatabase";
2931
* @param str The string to generate a date from.
3032
* @returns The Date object created.
3133
*/
32-
function createDateObject(str = ""): Date {
34+
function createDateObject(str?: ExtraValues): Date {
35+
if (!isString(str)) {
36+
str = "";
37+
}
38+
3339
const date = new Date(str);
3440
// Date object will not throw errors when we attempt to create an invalid date.
3541
if (isNaN(date.getTime())) {
@@ -158,7 +164,7 @@ class EventsDatabase {
158164
for (const ping of metric.sendInPings) {
159165
const executionCounter = getExecutionCounterMetric([ping]);
160166

161-
let currentExecutionCount = await Context.metricsDatabase.getMetric(ping, executionCounter);
167+
let currentExecutionCount = await Context.metricsDatabase.getMetric<number>(ping, executionCounter);
162168
// There might not be an execution counter stored in case:
163169
//
164170
// 1. The ping was already sent during this session and the events storage was cleared;
@@ -171,8 +177,7 @@ class EventsDatabase {
171177
// this must **always** be the first event of any events list.
172178
await recordGleanRestartedEvent([ping], new Date());
173179
}
174-
// TODO (bug 1693487): Remove the stringification once the new events API is implemented.
175-
value.addExtra(GLEAN_EXECUTION_COUNTER_EXTRA_KEY, currentExecutionCount.toString());
180+
value.addExtra(GLEAN_EXECUTION_COUNTER_EXTRA_KEY, currentExecutionCount);
176181

177182
const transformFn = (v?: JSONValue): JSONArray => {
178183
const existing: JSONArray = (v as JSONArray) ?? [];
@@ -273,7 +278,7 @@ class EventsDatabase {
273278
* 1. Sorts event by execution counter and timestamp;
274279
* 2. Applies offset to events timestamps;
275280
* 3. Removes the first event if it is a `glean.restarted` event;
276-
* 4. Removes reserved extra keys.
281+
* 4. Removes reserved extra keys and stringifies all extras values.
277282
*
278283
* @param pingName The name of the ping for which the payload is being prepared.
279284
* @param pingData An unsorted list of events.
@@ -285,7 +290,6 @@ class EventsDatabase {
285290
): Promise<JSONArray> {
286291
// Sort events by execution counter and by timestamp.
287292
const sortedEvents = pingData.sort((a, b) => {
288-
// TODO (bug 1693487): Remove the number casting once the new events API is implemented.
289293
const executionCounterA = Number(a.extra?.[GLEAN_EXECUTION_COUNTER_EXTRA_KEY]);
290294
const executionCounterB = Number(b.extra?.[GLEAN_EXECUTION_COUNTER_EXTRA_KEY]);
291295
// Sort by execution counter, in case they are different.
@@ -364,7 +368,7 @@ class EventsDatabase {
364368
);
365369
}
366370

367-
return sortedEvents.map((e) => RecordedEvent.toJSONObject(e.withoutReservedExtras()));
371+
return sortedEvents.map((e) => RecordedEvent.toJSONObject(e.payload()));
368372
}
369373

370374
/**

glean/src/core/metrics/events_database/recorded_event.ts

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import { GLEAN_RESERVED_EXTRA_KEYS } from "../../constants.js";
66
import type { JSONObject } from "../../utils.js";
77

8-
// An helper type for the 'extra' map.
9-
export type ExtraMap = Record<string, string>;
8+
export type ExtraValues = string | boolean | number;
9+
// A helper type for the 'extra' map.
10+
export type ExtraMap = Record<string, ExtraValues>;
1011

1112
// Represents the recorded data for a single event.
1213
export class RecordedEvent {
@@ -47,13 +48,27 @@ export class RecordedEvent {
4748
);
4849
}
4950

51+
private static withTransformedExtras(
52+
e: RecordedEvent,
53+
transformFn: (extras: ExtraMap) => ExtraMap
54+
): RecordedEvent {
55+
const extras = e.extra || {};
56+
const transformedExtras = transformFn(extras);
57+
return new RecordedEvent(
58+
e.category,
59+
e.name,
60+
e.timestamp,
61+
(transformedExtras && Object.keys(transformedExtras).length > 0) ? transformedExtras : undefined
62+
);
63+
}
64+
5065
/**
5166
* Add another extra key to a RecordedEvent object.
5267
*
5368
* @param key The key to add.
5469
* @param value The value of the key.
5570
*/
56-
addExtra(key: string, value: string): void {
71+
addExtra(key: string, value: ExtraValues): void {
5772
if (!this.extra) {
5873
this.extra = {};
5974
}
@@ -68,19 +83,38 @@ export class RecordedEvent {
6883
* @returns A new RecordedEvent object.
6984
*/
7085
withoutReservedExtras(): RecordedEvent {
71-
const extras = this.extra || {};
72-
const filteredExtras = Object.keys(extras)
73-
.filter(key => !GLEAN_RESERVED_EXTRA_KEYS.includes(key))
74-
.reduce((obj: ExtraMap, key) => {
75-
obj[key] = extras[key];
76-
return obj;
77-
}, {});
86+
return RecordedEvent.withTransformedExtras(
87+
this,
88+
(extras: ExtraMap): ExtraMap => {
89+
return Object.keys(extras)
90+
.filter(key => !GLEAN_RESERVED_EXTRA_KEYS.includes(key))
91+
.reduce((obj: ExtraMap, key) => {
92+
obj[key] = extras[key];
93+
return obj;
94+
}, {});
95+
}
96+
);
97+
}
7898

79-
return new RecordedEvent(
80-
this.category,
81-
this.name,
82-
this.timestamp,
83-
(filteredExtras && Object.keys(filteredExtras).length > 0) ? filteredExtras : undefined
99+
/**
100+
* Generate a new RecordedEvent object,
101+
* in the format expected on ping payloads.
102+
*
103+
* Strips reserved extra keys
104+
* and stringifies all event extras.
105+
*
106+
* @returns A new RecordedEvent object.
107+
*/
108+
payload(): RecordedEvent {
109+
return RecordedEvent.withTransformedExtras(
110+
this.withoutReservedExtras(),
111+
(extras: ExtraMap): ExtraMap => {
112+
return Object.keys(extras)
113+
.reduce((extra: Record<string, string>, key) => {
114+
extra[key] = extras[key].toString();
115+
return extra;
116+
}, {});
117+
}
84118
);
85119
}
86120
}

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { CommonMetricData } from "../index.js";
66
import type { ExtraMap } from "../events_database/recorded_event.js";
77
import { MetricType } from "../index.js";
88
import { RecordedEvent } from "../events_database/recorded_event.js";
9-
import { getMonotonicNow, truncateStringAtBoundaryWithError } from "../../utils.js";
9+
import { getMonotonicNow, isString, truncateStringAtBoundaryWithError } from "../../utils.js";
1010
import { Context } from "../../context.js";
1111
import { ErrorType } from "../../error/error_type.js";
1212

@@ -15,7 +15,7 @@ const MAX_LENGTH_EXTRA_KEY_VALUE = 100;
1515
/**
1616
* An event metric.
1717
*/
18-
class EventMetricType extends MetricType {
18+
class EventMetricType<SpecificExtraMap extends ExtraMap = ExtraMap> extends MetricType {
1919
private allowedExtraKeys?: string[];
2020

2121
constructor(meta: CommonMetricData, allowedExtraKeys?: string[]) {
@@ -33,10 +33,8 @@ class EventMetricType extends MetricType {
3333
* that the recording of the metric will happen in order with other Glean API calls.
3434
*
3535
* @param instance The metric instance to record to.
36-
* @param extra optional. This is a map, both keys and values need to be
37-
* strings, keys are identifiers. This is used for events where
38-
* additional richer context is needed.
39-
* The maximum length for values is 100 bytes.
36+
* @param extra optional. Used for events where additional richer context is needed.
37+
* The maximum length for string values is 100 bytes.
4038
* @param timestamp The event timestamp, defaults to now.
4139
* @returns A promise that resolves once the event is recorded.
4240
*/
@@ -55,7 +53,11 @@ class EventMetricType extends MetricType {
5553
truncatedExtra = {};
5654
for (const [name, value] of Object.entries(extra)) {
5755
if (instance.allowedExtraKeys.includes(name)) {
58-
truncatedExtra[name] = await truncateStringAtBoundaryWithError(instance, value, MAX_LENGTH_EXTRA_KEY_VALUE);
56+
if (isString(value)) {
57+
truncatedExtra[name] = await truncateStringAtBoundaryWithError(instance, value, MAX_LENGTH_EXTRA_KEY_VALUE);
58+
} else {
59+
truncatedExtra[name] = value;
60+
}
5961
} else {
6062
await Context.errorManager.record(instance, ErrorType.InvalidValue, `Invalid key index: ${name}`);
6163
continue;
@@ -77,12 +79,10 @@ class EventMetricType extends MetricType {
7779
/**
7880
* Record an event.
7981
*
80-
* @param extra optional. This is a map, both keys and values need to be
81-
* strings, keys are identifiers. This is used for events where
82-
* additional richer context is needed.
83-
* The maximum length for values is 100 bytes.
82+
* @param extra optional. Used for events where additional richer context is needed.
83+
* The maximum length for string values is 100 bytes.
8484
*/
85-
record(extra?: ExtraMap): void {
85+
record(extra?: SpecificExtraMap): void {
8686
const timestamp = getMonotonicNow();
8787

8888
Context.dispatcher.launch(async () => {

glean/tests/integration/schema/metrics.yaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,15 @@ for_testing:
139139
send_in_pings:
140140
- testing
141141
extra_keys:
142-
sample:
143-
description: |
144-
Sample extra key.
142+
sample_string:
143+
description: Sample string extra key.
144+
type: string
145+
sample_boolean:
146+
description: Sample boolean extra key.
147+
type: boolean
148+
sample_quantity:
149+
description: Sample quantity extra key.
150+
type: quantity
145151
quantity:
146152
type: quantity
147153
description: |

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ describe("schema", function() {
6060
metrics.boolean.set(false);
6161
metrics.counter.add(10);
6262
metrics.datetime.set();
63-
metrics.event.record({ sample: "hey" });
63+
metrics.event.record({
64+
sample_string: "hey",
65+
sample_boolean: false,
66+
sample_quantity: 42,
67+
});
6468
metrics.labeledBoolean["a_label"].set(true);
6569
metrics.labeledCounter["a_label"].add();
6670
metrics.labeledString["a_label"].set("ho");

glean/tests/unit/core/metrics/event.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,50 @@ describe("EventMetric", function() {
234234
event.extra["truncatedExtra"]
235235
);
236236
});
237+
238+
it("all types of event keys are recorded correctly", async function () {
239+
const event = new EventMetricType<{
240+
"string"?: string,
241+
"boolean"?: boolean,
242+
"number"?: number
243+
}>({
244+
category: "test",
245+
name: "event",
246+
sendInPings: ["store1"],
247+
lifetime: Lifetime.Ping,
248+
disabled: false
249+
}, ["string", "boolean", "number"]);
250+
251+
event.record({
252+
"string": "aString",
253+
"boolean": false,
254+
"number": 42
255+
});
256+
// Record again with incomplete extras
257+
event.record({
258+
"string": "twoString",
259+
"boolean": true,
260+
});
261+
// Record again without extras
262+
event.record();
263+
264+
const snapshot = await event.testGetValue();
265+
assert.ok(snapshot);
266+
assert.strictEqual(snapshot.length, 3);
267+
268+
const extras1 = snapshot[0].extra;
269+
assert.ok(extras1);
270+
assert.strictEqual(extras1["string"], "aString");
271+
assert.strictEqual(extras1["boolean"], false);
272+
assert.strictEqual(extras1["number"], 42);
273+
274+
const extras2 = snapshot[1].extra;
275+
assert.ok(extras2);
276+
assert.strictEqual(extras2["string"], "twoString");
277+
assert.strictEqual(extras2["boolean"], true);
278+
assert.strictEqual(extras2["number"], undefined);
279+
280+
const extras3 = snapshot[2].extra;
281+
assert.ok(!extras3);
282+
});
237283
});

0 commit comments

Comments
 (0)