Skip to content

Commit f5ee468

Browse files
author
brizental
committed
Implement core metrics
BONUS: Finish implementing initialize
1 parent 107277e commit f5ee468

File tree

6 files changed

+230
-4
lines changed

6 files changed

+230
-4
lines changed

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ export const PING_INFO_STORAGE = "glean_ping_info";
2929
//
3030
// See: https://mozilla.github.io/glean/book/dev/core/internal/reserved-ping-names.html
3131
export const CLIENT_INFO_STORAGE = "glean_client_info";
32+
33+
// We will set the client id to this client id in case upload is disabled.
34+
export const KNOWN_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";

src/glean.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import MetricsDatabase from "metrics/database";
66
import PingsDatabase from "pings/database";
77
import { isUndefined, sanitizeApplicationId } from "utils";
8+
import { CoreMetrics } from "internal_metrics";
89

910
class Glean {
1011
// The Glean singleton.
@@ -19,6 +20,8 @@ class Glean {
1920
private _uploadEnabled: boolean;
2021
// Whether or not Glean has been initialized.
2122
private _initialized: boolean;
23+
// Instances of Glean's core metrics.
24+
private _coreMetrics: CoreMetrics;
2225

2326
// Properties that will only be set on `initialize`.
2427
private _applicationId?: string;
@@ -30,6 +33,7 @@ class Glean {
3033
Use Glean.instance instead to access the Glean singleton.`);
3134
}
3235

36+
this._coreMetrics = new CoreMetrics();
3337
this._initialized = false;
3438
this._db = {
3539
metrics: new MetricsDatabase(),
@@ -39,6 +43,10 @@ class Glean {
3943
this._uploadEnabled = true;
4044
}
4145

46+
private static get coreMetrics(): CoreMetrics {
47+
return Glean.instance._coreMetrics;
48+
}
49+
4250
private static get instance(): Glean {
4351
if (!Glean._instance) {
4452
Glean._instance = new Glean();
@@ -51,14 +59,19 @@ class Glean {
5159
* Initialize Glean. This method should only be called once, subsequent calls will be no-op.
5260
*
5361
* @param applicationId The application ID (will be sanitized during initialization).
62+
* @param appBuild The build identifier generated by the CI system (e.g. "1234/A").
63+
* @param appDisplayVersion The user visible version string fro the application running Glean.js.
5464
*/
55-
static initialize(applicationId: string): void {
65+
static async initialize(applicationId: string, appBuild?: string, appDisplayVersion?: string): Promise<void> {
5666
if (Glean.instance._initialized) {
5767
console.warn("Attempted to initialize Glean, but it has already been initialized. Ignoring.");
5868
return;
5969
}
6070

6171
Glean.instance._applicationId = sanitizeApplicationId(applicationId);
72+
await Glean.coreMetrics.initialize(appBuild, appDisplayVersion);
73+
74+
Glean.instance._initialized = true;
6275
}
6376

6477
/**
@@ -79,6 +92,15 @@ class Glean {
7992
return Glean.instance._db.pings;
8093
}
8194

95+
/**
96+
* Gets this Glean's instance initialization status.
97+
*
98+
* @returns Whether or not the Glean singleton has been initialized.
99+
*/
100+
static get initialized(): boolean {
101+
return Glean.instance._initialized;
102+
}
103+
82104
// TODO: Make the following functions `private` once Bug 1682769 is resolved.
83105
static get uploadEnabled(): boolean {
84106
return Glean.instance._uploadEnabled;
@@ -104,6 +126,8 @@ class Glean {
104126
* TODO: Only allow this function to be called on test mode (depends on Bug 1682771).
105127
*/
106128
static async testRestGlean(): Promise<void> {
129+
// Get back to an uninitialized state.
130+
Glean.instance._initialized = false;
107131
// Reset upload enabled state, not to inerfere with other tests.
108132
Glean.uploadEnabled = true;
109133
// Clear the databases.

src/internal_metrics.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 { KNOWN_CLIENT_ID, CLIENT_INFO_STORAGE } from "./constants";
6+
import UUIDMetricType from "metrics/types/uuid";
7+
import DatetimeMetricType from "metrics/types/datetime";
8+
import StringMetricType from "metrics/types/string";
9+
import { createMetric } from "metrics/utils";
10+
import TimeUnit from "metrics/time_unit";
11+
import { Lifetime } from "metrics";
12+
import Glean from "glean";
13+
import PlatformInfo from "platform_info";
14+
15+
export class CoreMetrics {
16+
readonly clientId: UUIDMetricType;
17+
readonly firstRunDate: DatetimeMetricType;
18+
readonly os: StringMetricType;
19+
readonly osVersion: StringMetricType;
20+
readonly architecture: StringMetricType;
21+
readonly locale: StringMetricType;
22+
// Provided by the user
23+
readonly appBuild: StringMetricType;
24+
readonly appDisplayVersion: StringMetricType;
25+
26+
constructor() {
27+
this.clientId = new UUIDMetricType({
28+
name: "client_id",
29+
category: "",
30+
sendInPings: ["glean_client_info"],
31+
lifetime: Lifetime.User,
32+
disabled: false,
33+
});
34+
35+
this.firstRunDate = new DatetimeMetricType({
36+
name: "first_run_date",
37+
category: "",
38+
sendInPings: ["glean_client_info"],
39+
lifetime: Lifetime.User,
40+
disabled: false,
41+
}, TimeUnit.Day);
42+
43+
this.os = new StringMetricType({
44+
name: "os",
45+
category: "",
46+
sendInPings: ["glean_client_info"],
47+
lifetime: Lifetime.Application,
48+
disabled: false,
49+
});
50+
51+
this.osVersion = new StringMetricType({
52+
name: "os_version",
53+
category: "",
54+
sendInPings: ["glean_client_info"],
55+
lifetime: Lifetime.Application,
56+
disabled: false,
57+
});
58+
59+
this.architecture = new StringMetricType({
60+
name: "architecture",
61+
category: "",
62+
sendInPings: ["glean_client_info"],
63+
lifetime: Lifetime.Application,
64+
disabled: false,
65+
});
66+
67+
this.locale = new StringMetricType({
68+
name: "locale",
69+
category: "",
70+
sendInPings: ["glean_client_info"],
71+
lifetime: Lifetime.Application,
72+
disabled: false,
73+
});
74+
75+
this.appBuild = new StringMetricType({
76+
name: "app_build",
77+
category: "",
78+
sendInPings: ["glean_client_info"],
79+
lifetime: Lifetime.Application,
80+
disabled: false,
81+
});
82+
83+
this.appDisplayVersion = new StringMetricType({
84+
name: "app_display_version",
85+
category: "",
86+
sendInPings: ["glean_client_info"],
87+
lifetime: Lifetime.Application,
88+
disabled: false,
89+
});
90+
}
91+
92+
async initialize(appBuild?: string, appDisplayVersion?: string): Promise<void> {
93+
await this.initializeClientId();
94+
await this.initializeFirstRunDate();
95+
await this.initializeOs();
96+
await this.initializeOsVersion();
97+
await this.initializeArchitecture();
98+
await this.initializeLocale();
99+
await this.appBuild.set(appBuild || "Unknown");
100+
await this.appDisplayVersion.set(appDisplayVersion || "Unkown");
101+
}
102+
103+
/**
104+
* Generates and sets the client_id if it is not set,
105+
* or if the current value is currepted.
106+
*/
107+
private async initializeClientId(): Promise<void> {
108+
let needNewClientId = false;
109+
const clientIdData = await Glean.metricsDatabase.getMetric(CLIENT_INFO_STORAGE, this.clientId);
110+
if (clientIdData) {
111+
try {
112+
const currentClientId = createMetric("uuid", clientIdData);
113+
if (currentClientId.payload() === KNOWN_CLIENT_ID) {
114+
needNewClientId = true;
115+
}
116+
} catch {
117+
console.warn("Unexpected value found for Glean clientId. Ignoring.");
118+
needNewClientId = true;
119+
}
120+
} else {
121+
needNewClientId = true;
122+
}
123+
124+
if (needNewClientId) {
125+
await this.clientId.generateAndSet();
126+
}
127+
}
128+
129+
/**
130+
* Generates and sets the first_run_date if it is not set.
131+
*/
132+
private async initializeFirstRunDate(): Promise<void> {
133+
const firstRunDate = await Glean.metricsDatabase.getMetric(
134+
CLIENT_INFO_STORAGE,
135+
this.firstRunDate
136+
);
137+
138+
if (!firstRunDate) {
139+
await this.firstRunDate.set();
140+
}
141+
}
142+
143+
/**
144+
* Gets and sets the os.
145+
*/
146+
async initializeOs(): Promise<void> {
147+
await this.os.set(await PlatformInfo.os());
148+
}
149+
150+
/**
151+
* Gets and sets the os.
152+
*/
153+
async initializeOsVersion(): Promise<void> {
154+
await this.osVersion.set(await PlatformInfo.osVersion());
155+
}
156+
157+
/**
158+
* Gets and sets the system architecture.
159+
*/
160+
async initializeArchitecture(): Promise<void> {
161+
await this.architecture.set(await PlatformInfo.arch());
162+
}
163+
164+
/**
165+
* Gets and sets the system / browser locale.
166+
*/
167+
async initializeLocale(): Promise<void> {
168+
await this.locale.set(await PlatformInfo.locale());
169+
}
170+
}

src/pings/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class PingType {
4545
* @returns Whether or not the ping was successfully submitted.
4646
*/
4747
async submit(reason?: string): Promise<boolean> {
48+
if (!Glean.initialized) {
49+
console.info("Glean must be initialized before submitting pings.");
50+
return false;
51+
}
52+
4853
if (!Glean.uploadEnabled) {
4954
console.info("Glean disabled: not submitting any pings.");
5055
return false;

tests/pings/index.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Glean from "glean";
1212
describe("PingType", function() {
1313
beforeEach(async function() {
1414
await Glean.testRestGlean();
15+
await Glean.initialize("something something");
1516
});
1617

1718
it("collects and stores ping on submit", async function () {
@@ -36,7 +37,6 @@ describe("PingType", function() {
3637
const ping1 = new PingType("ping1", true, false, []);
3738
const ping2 = new PingType("ping2", true, true, []);
3839

39-
4040
// TODO: Make this nicer once we have a nice way to check if pings are enqueued,
4141
// possibly once Bug 1677440 is resolved.
4242
assert.ok(await ping1.submit());
@@ -58,4 +58,15 @@ describe("PingType", function() {
5858
const storedPings = await Glean.pingsDatabase["store"]._getWholeStore();
5959
assert.strictEqual(Object.keys(storedPings).length, 0);
6060
});
61+
62+
it("no pings are submitted if Glean has not been initialized", async function() {
63+
await Glean.testRestGlean();
64+
65+
const ping = new PingType("custom", true, false, []);
66+
assert.strictEqual(await ping.submit(), false);
67+
// TODO: Make this nicer once we have a nice way to check if pings are enqueued,
68+
// possibly once Bug 1677440 is resolved.
69+
const storedPings = await Glean.pingsDatabase["store"]._getWholeStore();
70+
assert.strictEqual(Object.keys(storedPings).length, 0);
71+
});
6172
});

tests/pings/maker.spec.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,22 @@ describe("PingMaker", function() {
3434

3535
it("buildClientInfo must report all the available data", async function() {
3636
const ping = new PingType("custom", true, false, []);
37-
const clientInfo = await PingMaker.buildClientInfoSection(ping);
37+
const clientInfo1 = await PingMaker.buildClientInfoSection(ping);
38+
assert.ok("telemetry_sdk_build" in clientInfo1);
3839

39-
assert.ok("telemetry_sdk_build" in clientInfo);
40+
// Initialize will also initialize core metrics that are part of the client info.
41+
await Glean.initialize("something something", "build", "display version");
42+
43+
const clientInfo2 = await PingMaker.buildClientInfoSection(ping);
44+
assert.ok("telemetry_sdk_build" in clientInfo2);
45+
assert.ok("client_id" in clientInfo2);
46+
assert.ok("first_run_date" in clientInfo2);
47+
assert.ok("os" in clientInfo2);
48+
assert.ok("os_version" in clientInfo2);
49+
assert.ok("architecture" in clientInfo2);
50+
assert.ok("locale" in clientInfo2);
51+
assert.ok("app_build" in clientInfo2);
52+
assert.ok("app_display_version" in clientInfo2);
4053
});
4154

4255
it("collectPing must return `undefined` if ping that must not be sent if empty, is empty", async function() {

0 commit comments

Comments
 (0)