Skip to content

Commit 2886705

Browse files
author
brizental
committed
Create plugin and event infra and define afterPingCollection event
1 parent 2e5881e commit 2886705

File tree

6 files changed

+211
-5
lines changed

6 files changed

+211
-5
lines changed

glean/src/core/config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import { DEFAULT_TELEMETRY_ENDPOINT } from "./constants";
6+
import Plugin from "../plugins";
67
import { validateURL } from "./utils";
78

89
/**
@@ -25,6 +26,8 @@ export interface ConfigurationInterface {
2526
readonly serverEndpoint?: string,
2627
// Debug configuration.
2728
debug?: DebugOptions,
29+
// Optional list of plugins to instrument the current Glean instance.
30+
plugins?: Plugin[],
2831
}
2932

3033
export class Configuration implements ConfigurationInterface {
@@ -36,7 +39,7 @@ export class Configuration implements ConfigurationInterface {
3639
readonly serverEndpoint: string;
3740
// Debug configuration.
3841
debug?: DebugOptions;
39-
42+
4043
constructor(config?: ConfigurationInterface) {
4144
this.appBuild = config?.appBuild;
4245
this.appDisplayVersion = config?.appDisplayVersion;

glean/src/core/events/index.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 CoreEvents from "../../../dist/webext/types/core/events";
6+
import Plugin from "../../plugins";
7+
8+
import { PingPayload } from "../pings/database";
9+
import { JSONObject } from "../utils";
10+
11+
export class CoreEvent<
12+
// An array of arguments that the event will provide as context to the plugin action.
13+
Context extends unknown[] = unknown[],
14+
// The expected type of the action result. To be returned by the plugin.
15+
Result extends unknown = unknown
16+
> {
17+
// The plugin to be triggered eveytime this even occurs.
18+
private plugin?: Plugin<CoreEvent<Context, Result>>;
19+
20+
constructor(readonly name: string) {}
21+
22+
/**
23+
* Registers a plugin that listens to this event.
24+
*
25+
* @param plugin The plugin to register.
26+
*/
27+
registerPlugin(plugin: Plugin<CoreEvent<Context, Result>>): void {
28+
if (this.plugin) {
29+
console.error(
30+
`Attempted to register plugin '${plugin.name}', which listens to the event '${plugin.event}'.`,
31+
`That event is already watched by plugin '${this.plugin.name}'`,
32+
`Plugin '${plugin.name}' will be ignored.`
33+
);
34+
return;
35+
}
36+
37+
this.plugin = plugin;
38+
}
39+
40+
/**
41+
* Deregisters the currently registered plugin.
42+
*
43+
* If no plugin is currently registered this is a no-op.
44+
*/
45+
deregisterPlugin(): void {
46+
this.plugin = undefined;
47+
}
48+
49+
/**
50+
* Triggers this event.
51+
*
52+
* Will execute the action of the registered plugin, if there is any.
53+
*
54+
* @param args The arguments to be passed as context to the registered plugin.
55+
*
56+
* @returns The result from the plugin execution.
57+
*/
58+
trigger(...args: Context): Result | void {
59+
if (this.plugin) {
60+
return this.plugin.action(...args);
61+
}
62+
}
63+
}
64+
65+
/**
66+
* Glean internal events.
67+
*/
68+
const CoreEvents: {
69+
afterPingCollection: CoreEvent<[PingPayload], Promise<JSONObject>>,
70+
[unused: string]: CoreEvent
71+
} = {
72+
// Event that is triggered immediatelly after a ping is collect and before it is recorded.
73+
//
74+
// - Context: The `PingPayload` of the recently collected ping.
75+
// - Result: The modified payload as a JSON object.
76+
afterPingCollection: new CoreEvent<[PingPayload], Promise<JSONObject>>("afterPingCollection")
77+
};
78+
79+
export default CoreEvents;

glean/src/core/events/utils.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 CoreEvents, { CoreEvent } from "./index";
6+
import Plugin from "../../plugins";
7+
8+
/**
9+
* Registers a plugin to the desired Glean event.
10+
*
11+
* If the plugin is attempting to listen to an unknown event it will be ignored.
12+
*
13+
* @param plugin The plugin to register.
14+
*/
15+
export function registerPluginToEvent<E extends CoreEvent>(plugin: Plugin<E>): void {
16+
const eventName = plugin.event;
17+
if (eventName in CoreEvents) {
18+
const event = CoreEvents[eventName];
19+
event.registerPlugin(plugin);
20+
return;
21+
}
22+
23+
console.error(
24+
`Attempted to register plugin '${plugin.name}', which listens to the event '${plugin.event}'.`,
25+
"That is not a valid Glean event. Ignoring"
26+
);
27+
}
28+
29+
/**
30+
* **Test-only API**
31+
*
32+
* Deregister plugins registered to all Glean events.
33+
*/
34+
export function testResetEvents(): void {
35+
for (const event in CoreEvents) {
36+
CoreEvents[event].deregisterPlugin();
37+
}
38+
}
39+
40+
41+
42+

glean/src/core/glean.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import UUIDMetricType from "./metrics/types/uuid";
1515
import DatetimeMetricType, { DatetimeMetric } from "./metrics/types/datetime";
1616
import Dispatcher from "./dispatcher";
1717
import CorePings from "./internal_pings";
18+
import { registerPluginToEvent, testResetEvents } from "./events/utils";
1819

1920
import Platform from "../platform/index";
2021
import TestPlatform from "../platform/test";
@@ -50,7 +51,7 @@ class Glean {
5051
metrics: MetricsDatabase,
5152
events: EventsDatabase,
5253
pings: PingsDatabase
53-
}
54+
};
5455

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

235+
if (config?.plugins) {
236+
for (const plugin of config.plugins) {
237+
registerPluginToEvent(plugin);
238+
}
239+
}
240+
234241
// Initialize the dispatcher and execute init before any other enqueued task.
235242
//
236243
// Note: We decide to execute the above tasks outside of the dispatcher task,
@@ -374,9 +381,9 @@ class Glean {
374381
Glean.dispatcher.launch(async () => {
375382
if (!Glean.initialized) {
376383
console.error(
377-
`Changing upload enabled before Glean is initialized is not supported.
378-
Pass the correct state into \`Glean.initialize\`.
379-
See documentation at https://mozilla.github.io/glean/book/user/general-api.html#initializing-the-glean-sdk`
384+
"Changing upload enabled before Glean is initialized is not supported.\n",
385+
"Pass the correct state into `Glean.initialize\n`.",
386+
"See documentation at https://mozilla.github.io/glean/book/user/general-api.html#initializing-the-glean-sdk`"
380387
);
381388
return;
382389
}
@@ -464,6 +471,9 @@ class Glean {
464471
// Get back to an uninitialized state.
465472
Glean.instance._initialized = false;
466473

474+
// Deregiter all plugins
475+
testResetEvents();
476+
467477
// Clear the dispatcher queue and return the dispatcher back to an uninitialized state.
468478
await Glean.dispatcher.testUninitialize();
469479

glean/src/plugins/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 { CoreEvent } from "../core/events";
6+
7+
// Helper type that extracts the type of the Context from a generic CoreEvent.
8+
export type EventContext<Context> = Context extends CoreEvent<infer InnerContext, unknown>
9+
? InnerContext
10+
: undefined[];
11+
12+
// Helper type that extracts the type of the Result from a generic CoreEvent.
13+
export type EventResult<Result> = Result extends CoreEvent<unknown[], infer InnerResult>
14+
? InnerResult
15+
: void;
16+
17+
/**
18+
* Plugins can listen to events that happen during Glean's lifecycle.
19+
*
20+
* Every Glean plugin must extend this class.
21+
*/
22+
abstract class Plugin<E extends CoreEvent = CoreEvent> {
23+
/**
24+
* Instantiates the Glean plugin.
25+
*
26+
* @param event The name of the even this plugin instruments.
27+
* @param name The name of this plugin.
28+
*/
29+
constructor(readonly event: string, readonly name: string) {}
30+
31+
/**
32+
* An action that will be triggered everytime the instrumented event occurs.
33+
*
34+
* @param args The arguments that are expected to be passed by this event.
35+
*/
36+
abstract action(...args: EventContext<E>): EventResult<E>;
37+
}
38+
39+
export default Plugin;

glean/tests/core/glean.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,26 @@ import assert from "assert";
66
import sinon from "sinon";
77

88
import { CLIENT_INFO_STORAGE, DELETION_REQUEST_PING_NAME, KNOWN_CLIENT_ID } from "../../src/core/constants";
9+
import CoreEvents from "../../src/core/events";
910
import Glean from "../../src/core/glean";
1011
import { Lifetime } from "../../src/core/metrics";
1112
import StringMetricType from "../../src/core/metrics/types/string";
1213
import PingType from "../../src/core/pings";
14+
import { JSONObject } from "../../src/core/utils";
1315
import TestPlatform from "../../src/platform/qt";
16+
import Plugin from "../../src/plugins";
1417

1518
const GLOBAL_APPLICATION_ID = "org.mozilla.glean.test.app";
19+
class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
20+
constructor() {
21+
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
22+
}
23+
24+
action(): Promise<JSONObject> {
25+
return Promise.resolve({});
26+
}
27+
}
28+
1629
const sandbox = sinon.createSandbox();
1730

1831
describe("Glean", function() {
@@ -164,6 +177,26 @@ describe("Glean", function() {
164177
}
165178
});
166179

180+
it("initialization registers plugins when provided", async function() {
181+
await Glean.testUninitialize();
182+
183+
const mockPlugin = new MockPlugin();
184+
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, {
185+
// We need to ignore TypeScript here,
186+
// otherwise it will error since mockEvent is not listed as a Glean event in core/events.ts
187+
//
188+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
189+
// @ts-ignore
190+
plugins: [ mockPlugin ]
191+
});
192+
193+
assert.deepStrictEqual(CoreEvents["afterPingCollection"]["plugin"], mockPlugin);
194+
195+
await Glean.testUninitialize();
196+
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
197+
assert.strictEqual(CoreEvents["afterPingCollection"]["plugin"], undefined);
198+
});
199+
167200
it("disabling upload should disable metrics recording", async function() {
168201
const pings = ["aPing", "twoPing", "threePing"];
169202
const metric = new StringMetricType({

0 commit comments

Comments
 (0)