Skip to content

Commit 21ff67e

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

File tree

10 files changed

+301
-12
lines changed

10 files changed

+301
-12
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/core/pings/database.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,9 @@ export interface PingPayload extends JSONObject {
4242
metrics?: MetricsPayload,
4343
events?: JSONArray,
4444
}
45-
4645
export interface PingInternalRepresentation extends JSONObject {
4746
path: string,
48-
payload: PingPayload,
47+
payload: JSONObject,
4948
headers?: Record<string, string>
5049
}
5150

@@ -117,7 +116,7 @@ class PingsDatabase {
117116
async recordPing(
118117
path: string,
119118
identifier: string,
120-
payload: PingPayload,
119+
payload: JSONObject,
121120
headers?: Record<string, string>
122121
): Promise<void> {
123122
const ping: PingInternalRepresentation = {

glean/src/core/pings/maker.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import TimeUnit from "../metrics/time_unit";
1010
import { ClientInfo, PingInfo, PingPayload } from "../pings/database";
1111
import PingType from "../pings";
1212
import Glean from "../glean";
13+
import CoreEvents from "../events";
14+
import { JSONObject } from "../utils";
1315

1416
// The moment the current Glean.js session started.
1517
const GLEAN_START_TIME = new Date();
@@ -206,27 +208,35 @@ function makePath(identifier: string, ping: PingType): string {
206208
/**
207209
* Collects and stores a ping on the pings database.
208210
*
211+
* This function will trigger the `AfterPingCollection` event.
212+
* This event is triggered **after** logging the ping, which happens if `logPings` is set.
213+
* We will log the payload before it suffers any change by plugins instrumenting this event.
214+
*
209215
* @param identifier The pings UUID identifier.
210216
* @param ping The ping to submit.
211217
* @param reason An optional reason code to include in the ping.
212218
*
213219
* @returns A promise that is resolved once collection and storing is done.
214220
*/
215221
export async function collectAndStorePing(identifier: string, ping: PingType, reason?: string): Promise<void> {
216-
const payload = await collectPing(ping, reason);
217-
if (!payload) {
222+
const collectedPayload = await collectPing(ping, reason);
223+
if (!collectedPayload) {
218224
return;
219225
}
220226

221227
if (Glean.logPings) {
222-
console.info(JSON.stringify(payload, null, 2));
228+
console.info(JSON.stringify(collectedPayload, null, 2));
223229
}
224230

225231
const headers = getPingHeaders();
232+
233+
const modifiedPayload = await CoreEvents.afterPingCollection.trigger(collectedPayload);
234+
const finalPayload = modifiedPayload ? modifiedPayload : collectedPayload;
235+
226236
return Glean.pingsDatabase.recordPing(
227237
makePath(identifier, ping),
228238
identifier,
229-
payload,
239+
finalPayload,
230240
headers
231241
);
232242
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 CompactEncrypt from "jose/jwe/compact/encrypt";
6+
import parseJwk from "jose/jwk/parse";
7+
import { JWK } from "jose/types";
8+
9+
import Plugin from "../index";
10+
import { PingPayload } from "../../core/pings/database";
11+
import { JSONObject } from "../../core/utils";
12+
import CoreEvents from "../../core/events";
13+
14+
class PingEncryptionPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
15+
constructor(readonly jwk: JWK) {
16+
super(CoreEvents["afterPingCollection"].name, "pingEncryptionPlugin");
17+
}
18+
19+
async action(payload: PingPayload): Promise<JSONObject> {
20+
const key = await parseJwk(this.jwk);
21+
const encoder = new TextEncoder();
22+
const encodedPayload = await new CompactEncrypt(encoder.encode(JSON.stringify(payload)))
23+
.setProtectedHeader({ kid: this.jwk.kid })
24+
.encrypt(key);
25+
return { payload: encodedPayload };
26+
}
27+
}
28+
29+
export default PingEncryptionPlugin;

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)