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

import { DEFAULT_TELEMETRY_ENDPOINT } from "./constants";
import Plugin from "../plugins";
import { validateURL } from "./utils";

/**
Expand All @@ -25,6 +26,8 @@ export interface ConfigurationInterface {
readonly serverEndpoint?: string,
// Debug configuration.
debug?: DebugOptions,
// Optional list of plugins to include in current Glean instance.
plugins?: Plugin[],
}

export class Configuration implements ConfigurationInterface {
Expand All @@ -36,7 +39,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
78 changes: 78 additions & 0 deletions glean/src/core/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* 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 Plugin from "../../plugins";

import { PingPayload } from "../pings/database";
import { JSONObject } from "../utils";

export class CoreEvent<
// An array of arguments that the event will provide as context to the plugin action.
Context extends unknown[] = unknown[],
// The expected type of the action result. To be returned by the plugin.
Result extends unknown = unknown
> {
// The plugin to be triggered eveytime this even occurs.
private plugin?: Plugin<CoreEvent<Context, Result>>;

constructor(readonly name: string) {}

/**
* Registers a plugin that listens to this event.
*
* @param plugin The plugin to register.
*/
registerPlugin(plugin: Plugin<CoreEvent<Context, Result>>): void {
if (this.plugin) {
console.error(
`Attempted to register plugin '${plugin.name}', which listens to the event '${plugin.event}'.`,
`That event is already watched by plugin '${this.plugin.name}'`,
`Plugin '${plugin.name}' will be ignored.`
);
return;
}

this.plugin = plugin;
}

/**
* Deregisters the currently registered plugin.
*
* If no plugin is currently registered this is a no-op.
*/
deregisterPlugin(): void {
this.plugin = undefined;
}

/**
* Triggers this event.
*
* Will execute the action of the registered plugin, if there is any.
*
* @param args The arguments to be passed as context to the registered plugin.
*
* @returns The result from the plugin execution.
*/
trigger(...args: Context): Result | void {
if (this.plugin) {
return this.plugin.action(...args);
}
}
}

/**
* Glean internal events.
*/
const CoreEvents: {
afterPingCollection: CoreEvent<[PingPayload], Promise<JSONObject>>,
[unused: string]: CoreEvent
} = {
// Event that is triggered immediatelly after a ping is collect and before it is recorded.
//
// - Context: The `PingPayload` of the recently collected ping.
// - Result: The modified payload as a JSON object.
afterPingCollection: new CoreEvent<[PingPayload], Promise<JSONObject>>("afterPingCollection")
};

export default CoreEvents;
38 changes: 38 additions & 0 deletions glean/src/core/events/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* 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 CoreEvents, { CoreEvent } from "./index";
import Plugin from "../../plugins";

/**
* Registers a plugin to the desired Glean event.
*
* If the plugin is attempting to listen to an unknown event it will be ignored.
*
* @param plugin The plugin to register.
*/
export function registerPluginToEvent<E extends CoreEvent>(plugin: Plugin<E>): void {
const eventName = plugin.event;
if (eventName in CoreEvents) {
const event = CoreEvents[eventName];
event.registerPlugin(plugin);
return;
}

console.error(
`Attempted to register plugin '${plugin.name}', which listens to the event '${plugin.event}'.`,
"That is not a valid Glean event. Ignoring"
);
}

/**
* **Test-only API**
*
* Deregister plugins registered to all Glean events.
*/
export function testResetEvents(): void {
for (const event in CoreEvents) {
CoreEvents[event].deregisterPlugin();
}
}
42 changes: 30 additions & 12 deletions glean/src/core/glean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import UUIDMetricType from "./metrics/types/uuid";
import DatetimeMetricType, { DatetimeMetric } from "./metrics/types/datetime";
import Dispatcher from "./dispatcher";
import CorePings from "./internal_pings";
import { registerPluginToEvent, testResetEvents } from "./events/utils";

import Platform from "../platform/index";
import TestPlatform from "../platform/test";
Expand Down Expand Up @@ -50,7 +51,7 @@ class Glean {
metrics: MetricsDatabase,
events: EventsDatabase,
pings: PingsDatabase
}
};

private constructor() {
if (!isUndefined(Glean._instance)) {
Expand Down Expand Up @@ -231,6 +232,12 @@ class Glean {
// The configuration constructor will throw in case config has any incorrect prop.
const correctConfig = new Configuration(config);

if (config?.plugins) {
for (const plugin of config.plugins) {
registerPluginToEvent(plugin);
}
}

// Initialize the dispatcher and execute init before any other enqueued task.
//
// Note: We decide to execute the above tasks outside of the dispatcher task,
Expand Down Expand Up @@ -374,9 +381,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 +412,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 +449,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 +471,9 @@ class Glean {
// Get back to an uninitialized state.
Glean.instance._initialized = false;

// Deregiter all plugins
testResetEvents();

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

Expand All @@ -480,7 +494,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
17 changes: 13 additions & 4 deletions glean/src/core/pings/maker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import TimeUnit from "../metrics/time_unit";
import { ClientInfo, PingInfo, PingPayload } from "../pings/database";
import PingType from "../pings";
import Glean from "../glean";
import CoreEvents from "../events";

// The moment the current Glean.js session started.
const GLEAN_START_TIME = new Date();
Expand Down Expand Up @@ -206,27 +207,35 @@ 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 listening to 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 headers = getPingHeaders();

const modifiedPayload = await CoreEvents.afterPingCollection.trigger(collectedPayload);
const finalPayload = modifiedPayload ? modifiedPayload : collectedPayload;

return Glean.pingsDatabase.recordPing(
makePath(identifier, ping),
identifier,
payload,
finalPayload,
headers
);
}
Expand Down
39 changes: 39 additions & 0 deletions glean/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* 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 { CoreEvent } from "../core/events";

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

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

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

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

export default Plugin;
Loading