Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ export const PING_INFO_STORAGE = "glean_ping_info";
//
// See: https://mozilla.github.io/glean/book/dev/core/internal/reserved-ping-names.html
export const CLIENT_INFO_STORAGE = "glean_client_info";

// We will set the client id to this client id in case upload is disabled.
export const KNOWN_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
49 changes: 46 additions & 3 deletions src/glean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import MetricsDatabase from "metrics/database";
import PingsDatabase from "pings/database";
import { isUndefined, sanitizeApplicationId } from "utils";
import { CoreMetrics } from "internal_metrics";
import { Lifetime } from "metrics";

class Glean {
// The Glean singleton.
Expand All @@ -19,6 +21,8 @@ class Glean {
private _uploadEnabled: boolean;
// Whether or not Glean has been initialized.
private _initialized: boolean;
// Instances of Glean's core metrics.
private _coreMetrics: CoreMetrics;

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

this._coreMetrics = new CoreMetrics();
this._initialized = false;
this._db = {
metrics: new MetricsDatabase(),
Expand All @@ -39,6 +44,10 @@ class Glean {
this._uploadEnabled = true;
}

private static get coreMetrics(): CoreMetrics {
return Glean.instance._coreMetrics;
}

private static get instance(): Glean {
if (!Glean._instance) {
Glean._instance = new Glean();
Expand All @@ -51,14 +60,19 @@ class Glean {
* Initialize Glean. This method should only be called once, subsequent calls will be no-op.
*
* @param applicationId The application ID (will be sanitized during initialization).
* @param appBuild The build identifier generated by the CI system (e.g. "1234/A").
* @param appDisplayVersion The user visible version string fro the application running Glean.js.
*/
static initialize(applicationId: string): void {
static async initialize(applicationId: string, appBuild?: string, appDisplayVersion?: string): Promise<void> {
if (Glean.instance._initialized) {
console.warn("Attempted to initialize Glean, but it has already been initialized. Ignoring.");
return;
}

Glean.instance._applicationId = sanitizeApplicationId(applicationId);
await Glean.coreMetrics.initialize(appBuild, appDisplayVersion);

Glean.instance._initialized = true;
}

/**
Expand All @@ -79,6 +93,15 @@ class Glean {
return Glean.instance._db.pings;
}

/**
* Gets this Glean's instance initialization status.
*
* @returns Whether or not the Glean singleton has been initialized.
*/
static get initialized(): boolean {
return Glean.instance._initialized;
}

// TODO: Make the following functions `private` once Bug 1682769 is resolved.
static get uploadEnabled(): boolean {
return Glean.instance._uploadEnabled;
Expand All @@ -99,16 +122,36 @@ class Glean {
/**
* **Test-only API**
*
* Resets the Glean singleton to its initial state.
* Resets the Glean to an uninitialized state and clear app lifetime metrics.
*
* TODO: Only allow this function to be called on test mode (depends on Bug 1682771).
*/
static async testRestGlean(): Promise<void> {
static async testUninitialize(): Promise<void> {
Glean.instance._initialized = false;
await Glean.metricsDatabase.clear(Lifetime.Application);
}

/**
* **Test-only API**
*
* Resets the Glean singleton to its initial state and re-initializes it.
*
* TODO: Only allow this function to be called on test mode (depends on Bug 1682771).
*
* @param applicationId The application ID (will be sanitized during initialization).
* @param optionalInitializeArgs Optional arguments to be passed to `initialize`.
*/
static async testRestGlean(applicationId: string, ...optionalInitializeArgs: never[]): Promise<void> {
// Get back to an uninitialized state.
Glean.instance._initialized = false;
// Reset upload enabled state, not to inerfere with other tests.
Glean.uploadEnabled = true;
// Clear the databases.
await Glean.metricsDatabase.clearAll();
await Glean.pingsDatabase.clearAll();

// Initialize Glean.
await Glean.initialize(applicationId, ...optionalInitializeArgs);
}
}

Expand Down
170 changes: 170 additions & 0 deletions src/internal_metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/* 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 { KNOWN_CLIENT_ID, CLIENT_INFO_STORAGE } from "./constants";
import UUIDMetricType from "metrics/types/uuid";
import DatetimeMetricType from "metrics/types/datetime";
import StringMetricType from "metrics/types/string";
import { createMetric } from "metrics/utils";
import TimeUnit from "metrics/time_unit";
import { Lifetime } from "metrics";
import Glean from "glean";
import PlatformInfo from "platform_info";

export class CoreMetrics {
readonly clientId: UUIDMetricType;
readonly firstRunDate: DatetimeMetricType;
readonly os: StringMetricType;
readonly osVersion: StringMetricType;
readonly architecture: StringMetricType;
readonly locale: StringMetricType;
// Provided by the user
readonly appBuild: StringMetricType;
readonly appDisplayVersion: StringMetricType;

constructor() {
this.clientId = new UUIDMetricType({
name: "client_id",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.User,
disabled: false,
});

this.firstRunDate = new DatetimeMetricType({
name: "first_run_date",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.User,
disabled: false,
}, TimeUnit.Day);

this.os = new StringMetricType({
name: "os",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.Application,
disabled: false,
});

this.osVersion = new StringMetricType({
name: "os_version",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.Application,
disabled: false,
});

this.architecture = new StringMetricType({
name: "architecture",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.Application,
disabled: false,
});

this.locale = new StringMetricType({
name: "locale",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.Application,
disabled: false,
});

this.appBuild = new StringMetricType({
name: "app_build",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.Application,
disabled: false,
});

this.appDisplayVersion = new StringMetricType({
name: "app_display_version",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.Application,
disabled: false,
});
}

async initialize(appBuild?: string, appDisplayVersion?: string): Promise<void> {
await this.initializeClientId();
await this.initializeFirstRunDate();
await this.initializeOs();
await this.initializeOsVersion();
await this.initializeArchitecture();
await this.initializeLocale();
await this.appBuild.set(appBuild || "Unknown");
await this.appDisplayVersion.set(appDisplayVersion || "Unkown");
}

/**
* Generates and sets the client_id if it is not set,
* or if the current value is currepted.
*/
private async initializeClientId(): Promise<void> {
let needNewClientId = false;
const clientIdData = await Glean.metricsDatabase.getMetric(CLIENT_INFO_STORAGE, this.clientId);
if (clientIdData) {
try {
const currentClientId = createMetric("uuid", clientIdData);
if (currentClientId.payload() === KNOWN_CLIENT_ID) {
needNewClientId = true;
}
} catch {
console.warn("Unexpected value found for Glean clientId. Ignoring.");
needNewClientId = true;
}
} else {
needNewClientId = true;
}

if (needNewClientId) {
await this.clientId.generateAndSet();
}
}

/**
* Generates and sets the first_run_date if it is not set.
*/
private async initializeFirstRunDate(): Promise<void> {
const firstRunDate = await Glean.metricsDatabase.getMetric(
CLIENT_INFO_STORAGE,
this.firstRunDate
);

if (!firstRunDate) {
await this.firstRunDate.set();
}
}

/**
* Gets and sets the os.
*/
async initializeOs(): Promise<void> {
await this.os.set(await PlatformInfo.os());
}

/**
* Gets and sets the os.
*/
async initializeOsVersion(): Promise<void> {
await this.osVersion.set(await PlatformInfo.osVersion());
}

/**
* Gets and sets the system architecture.
*/
async initializeArchitecture(): Promise<void> {
await this.architecture.set(await PlatformInfo.arch());
}

/**
* Gets and sets the system / browser locale.
*/
async initializeLocale(): Promise<void> {
await this.locale.set(await PlatformInfo.locale());
}
}
5 changes: 5 additions & 0 deletions src/pings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ class PingType {
* @returns Whether or not the ping was successfully submitted.
*/
async submit(reason?: string): Promise<boolean> {
if (!Glean.initialized) {
console.info("Glean must be initialized before submitting pings.");
return false;
}

if (!Glean.uploadEnabled) {
console.info("Glean disabled: not submitting any pings.");
return false;
Expand Down
69 changes: 69 additions & 0 deletions src/platform_info/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* 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/. */

// Must be up to date with https://github.com/mozilla/glean/blob/main/glean-core/src/system.rs
export const enum KnownOperatingSystems {
Android = "Android",
iOS = "iOS",
Linux = "Linux",
MacOS = "Darwin",
Windows = "Windows",
FreeBSD = "FreeBSD",
NetBSD = "NetBSD",
OpenBSD = "OpenBSD",
Solaris = "Solaris",
// ChromeOS is not listed in the Glean SDK because it is not a possibility there.
ChromeOS = "ChromeOS",
Unknown = "Unknown",
}

export interface PlatformInfo {
/**
* Gets and returns the current OS system.
*
* @returns The current OS.
*/
os(): Promise<KnownOperatingSystems>;

/**
* Gets and returns the current OS system version.
*
* @returns The current OS version.
*/
osVersion(): Promise<string>;

/**
* Gets and returnst the current system architecture.
*
* @returns The current system architecture.
*/
arch(): Promise<string>;

/**
* Gets and returnst the current system / browser locale.
*
* @returns The current system / browser locale.
*/
locale(): Promise<string>;
}

// Default export for tests sake.
const MockPlatformInfo: PlatformInfo = {
os(): Promise<KnownOperatingSystems> {
return Promise.resolve(KnownOperatingSystems.Unknown);
},

osVersion(): Promise<string> {
return Promise.resolve("Unknown");
},

arch(): Promise<string> {
return Promise.resolve("Unknown");
},

locale(): Promise<string> {
return Promise.resolve("Unknown");
},
};
export default MockPlatformInfo;
Loading