Skip to content
Closed
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
6 changes: 5 additions & 1 deletion glean/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { DEFAULT_TELEMETRY_ENDPOINT } from "./constants";
import GleanPlugin from "../plugins";
import GleanEvents from "./events";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are types, we pass plugins through the ConfigurationInterface to Glean.initialize, and this is here to help define exactly what we are passing through that property.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GleanEvents is a union of all the possible GleanEvents types (not a union right now since we only defined one event, but you get the gist). And GleanPlugin is the type of the accepted plugin class. Each plugin is always attached to a type of event. To ensure we only accept plugins that listen to valid Glean events, the final type for the plugins property is GleanPlugin<GleanEvents>[].

import { validateURL } from "./utils";

/**
Expand All @@ -25,6 +27,8 @@ export interface ConfigurationInterface {
readonly serverEndpoint?: string,
// Debug configuration.
debug?: DebugOptions,
// Optional list of plugins to instrument the current Glean instance.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Optional list of plugins to instrument the current Glean instance.
// Optional list of plugins to enable in the current Glean instance.

plugins?: GleanPlugin<GleanEvents>[],
}

export class Configuration implements ConfigurationInterface {
Expand All @@ -36,7 +40,7 @@ export class Configuration implements ConfigurationInterface {
readonly serverEndpoint: string;
// Debug configuration.
debug?: DebugOptions;

constructor(config?: ConfigurationInterface) {
this.appBuild = config?.appBuild;
this.appDisplayVersion = config?.appDisplayVersion;
Expand Down
15 changes: 15 additions & 0 deletions glean/src/core/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* 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 { PingPayload } from "./pings/database";
import { JSONObject } from "./utils";

import { GleanEvent } from "../plugins/index";

export const AfterPingCollection: GleanEvent<[PingPayload], Promise<JSONObject>> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you document what's this? When is this called? What's it for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this is here to define:

  • The type of the context provided by this event ([PingPayload]);
  • The type of the result expected from this event ([Promise<JSONObject>]);
  • The name of this event.

This way, when we define a plugin like this class MyPlugin extends GleanPlugin<typeof AfterPingCollection> the action in this plugin will already expected the correct arguments and return type defined by the event.

name: "afterPingCollection"
};

type GleanEvents = typeof AfterPingCollection;
export default GleanEvents;
76 changes: 64 additions & 12 deletions glean/src/core/glean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import CorePings from "./internal_pings";

import Platform from "../platform/index";
import TestPlatform from "../platform/test";
import GleanPlugin, { EventContext, EventResult, GleanEvent } from "../plugins";
import GleanEvents from "./events";

class Glean {
// The Glean singleton.
Expand Down Expand Up @@ -50,7 +52,8 @@ class Glean {
metrics: MetricsDatabase,
events: EventsDatabase,
pings: PingsDatabase
}
};
private _plugins: { [event: string]: GleanPlugin<GleanEvents> };

private constructor() {
if (!isUndefined(Glean._instance)) {
Expand All @@ -64,6 +67,7 @@ class Glean {
this._coreMetrics = new CoreMetrics();
this._corePings = new CorePings();
this._initialized = false;
this._plugins = {};
}

private static get instance(): Glean {
Expand Down Expand Up @@ -94,6 +98,30 @@ class Glean {
Glean.instance._uploadEnabled = value;
}

private static get plugins(): { [event: string]: GleanPlugin<GleanEvents> } {
return Glean.instance._plugins;
}

/**
* Registers a list of plugins.
*
* @param plugins The plugins list from the configuration.
*/
private static registerPlugins(plugins: GleanPlugin<GleanEvents>[]): void {
for (const plugin of plugins) {
if (plugin.event in Glean.plugins) {
console.error(
`Attempted to register plugin '${plugin.name}', which instruments the event '${plugin.event}'.`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh, what do you mean by "instrument"? We usually use this term to mean "add something to measure".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, maybe I am using this word wrong... checks wikipedia ... 100% I am using it wrong 🆘 Maybe I want "listens to"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thankfully I can always claim that English is not my first language 🥇

`That event is already instrumented by plugin '${Glean.plugins[plugin.event].name}'`,
`Plugin '${plugin.name}' will be ignored.`
);
return;
}

Glean.instance._plugins[plugin.event] = plugin;
}
}

/**
* Handles the changing of state from upload disabled to enabled.
*
Expand Down Expand Up @@ -230,6 +258,9 @@ class Glean {

// The configuration constructor will throw in case config has any incorrect prop.
const correctConfig = new Configuration(config);
if (config?.plugins) {
Glean.registerPlugins(config?.plugins);
}

// Initialize the dispatcher and execute init before any other enqueued task.
//
Expand Down Expand Up @@ -345,6 +376,16 @@ class Glean {
return Glean.instance._platform;
}

static triggerEvent<TriggeredEvent extends GleanEvent>(
event: TriggeredEvent,
...args: EventContext<TriggeredEvent>
): EventResult<TriggeredEvent> | void {
const plugin: GleanPlugin<TriggeredEvent> | undefined = Glean.plugins[event.name];
if (plugin) {
return plugin.action(...args);
}
}

/**
* Determines whether upload is enabled.
*
Expand Down Expand Up @@ -374,9 +415,9 @@ class Glean {
Glean.dispatcher.launch(async () => {
if (!Glean.initialized) {
console.error(
`Changing upload enabled before Glean is initialized is not supported.
Pass the correct state into \`Glean.initialize\`.
See documentation at https://mozilla.github.io/glean/book/user/general-api.html#initializing-the-glean-sdk`
"Changing upload enabled before Glean is initialized is not supported.\n",
"Pass the correct state into `Glean.initialize\n`.",
"See documentation at https://mozilla.github.io/glean/book/user/general-api.html#initializing-the-glean-sdk`"
);
return;
}
Expand Down Expand Up @@ -405,15 +446,15 @@ class Glean {
// All dispatched tasks are guaranteed to be run after initialize.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!Glean.instance._config!.debug) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Glean.instance._config!.debug = {};
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Glean.instance._config!.debug = {};
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Glean.instance._config!.debug.logPings = flag;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Glean.instance._config!.debug.logPings = flag;

// The dispatcher requires that dispatched functions return promises.
return Promise.resolve();
// The dispatcher requires that dispatched functions return promises.
return Promise.resolve();
});
}

Expand Down Expand Up @@ -442,7 +483,11 @@ class Glean {
* first_run_date) are cleared. Default to `true`.
* @param config Glean configuration options.
*/
static async testInitialize(applicationId: string, uploadEnabled = true, config?: Configuration): Promise<void> {
static async testInitialize(
applicationId: string,
uploadEnabled = true,
config?: ConfigurationInterface
): Promise<void> {
Glean.setPlatform(TestPlatform);
Glean.initialize(applicationId, uploadEnabled, config);

Expand All @@ -460,6 +505,9 @@ class Glean {
// Get back to an uninitialized state.
Glean.instance._initialized = false;

// Clear plugins
Glean.instance._plugins = {};

// Clear the dispatcher queue and return the dispatcher back to an uninitialized state.
await Glean.dispatcher.testUninitialize();

Expand All @@ -480,7 +528,11 @@ class Glean {
* first_run_date) are cleared. Default to `true`.
* @param config Glean configuration options.
*/
static async testResetGlean(applicationId: string, uploadEnabled = true, config?: Configuration): Promise<void> {
static async testResetGlean(
applicationId: string,
uploadEnabled = true,
config?: ConfigurationInterface
): Promise<void> {
await Glean.testUninitialize();

// Clear the databases.
Expand Down
5 changes: 2 additions & 3 deletions glean/src/core/pings/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@ export interface PingPayload extends JSONObject {
metrics?: MetricsPayload,
events?: JSONArray,
}

export interface PingInternalRepresentation extends JSONObject {
path: string,
payload: PingPayload,
payload: JSONObject,
headers?: Record<string, string>
}

Expand Down Expand Up @@ -117,7 +116,7 @@ class PingsDatabase {
async recordPing(
path: string,
identifier: string,
payload: PingPayload,
payload: JSONObject,
headers?: Record<string, string>
): Promise<void> {
const ping: PingInternalRepresentation = {
Expand Down
20 changes: 17 additions & 3 deletions glean/src/core/pings/maker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import TimeUnit from "../metrics/time_unit";
import { ClientInfo, PingInfo, PingPayload } from "../pings/database";
import PingType from "../pings";
import Glean from "../glean";
import { AfterPingCollection } from "../events";
import { JSONObject } from "../utils";

// The moment the current Glean.js session started.
const GLEAN_START_TIME = new Date();
Expand Down Expand Up @@ -206,20 +208,32 @@ function makePath(identifier: string, ping: PingType): string {
/**
* Collects and stores a ping on the pings database.
*
* This function will trigger the `AfterPingCollection` event.
* This event is triggered **after** logging the ping, which happens if `logPings` is set.
* We will log the payload before it suffers any change by plugins instrumenting this event.
*
* @param identifier The pings UUID identifier.
* @param ping The ping to submit.
* @param reason An optional reason code to include in the ping.
*
* @returns A promise that is resolved once collection and storing is done.
*/
export async function collectAndStorePing(identifier: string, ping: PingType, reason?: string): Promise<void> {
const payload = await collectPing(ping, reason);
if (!payload) {
const collectedPayload = await collectPing(ping, reason);
if (!collectedPayload) {
return;
}

if (Glean.logPings) {
console.info(JSON.stringify(payload, null, 2));
console.info(JSON.stringify(collectedPayload, null, 2));
}

const modifiedPayload = await Glean.triggerEvent(AfterPingCollection, collectedPayload);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider (not in this PR) a different way to inject the dependency here, reducing the calls to the Glean object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered making events completely unaware of the Glean singleton, but didn't try that out. I do think if we are going to go that way it should be now, otherwise we will be a bit stuck.

Let me attempt that and open an alternate to this PR with the different approach. I did not go that way before, because it would be quite different from what I defined in the proposal for this, but there is no harm in testing.

let payload: JSONObject;
if (modifiedPayload) {
payload = modifiedPayload;
} else {
payload = collectedPayload;
Comment on lines +231 to +236
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const modifiedPayload = await Glean.triggerEvent(AfterPingCollection, collectedPayload);
let payload: JSONObject;
if (modifiedPayload) {
payload = modifiedPayload;
} else {
payload = collectedPayload;
const modifiedPayload = await Glean.triggerEvent(AfterPingCollection, collectedPayload) ?? collectedPayload;

What about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better.

}

const headers = getPingHeaders();
Expand Down
51 changes: 51 additions & 0 deletions glean/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* 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/. */

export type GleanEvent<
// An array of arguments that the event will provide to the plugin action.
//
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Context extends unknown[] = unknown[],
// The exptected type of the action result. To be returned by the plugin.
//
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Result extends unknown = unknown
> = {
// The name of the event.
name: string;
};

// Helper type that extracts the type of the Context from a generic GleanEvent.
export type EventContext<Context> = Context extends GleanEvent<infer InnerContext, unknown>
? InnerContext
: undefined[];

// Helper type that extracts the type of the Result from a generic GleanEvent.
export type EventResult<Result> = Result extends GleanEvent<unknown[], infer InnerResult>
? InnerResult
: undefined[];

/**
* Plugins can listen to events that happen during Glean's lifecycle.
*
* Every Glean plugin must extend this class.
*/
abstract class GleanPlugin<GleanEvent> {
/**
* Instantiates the Glean plugin.
*
* @param event The name of the even this plugin instruments.
* @param name The name of this plugin.
*/
constructor(readonly event: string, readonly name: string) {}

/**
* An action that will be triggered everytime the instrumented event occurs.
*
* @param args The arguments that are expected to be passed by this event.
*/
abstract action(...args: EventContext<GleanEvent>): EventResult<GleanEvent>;
}

export default GleanPlugin;
Loading