-
Notifications
You must be signed in to change notification settings - Fork 34
Bug 1689547 - Create plugin / events infrastructure and define afterPingCollection event
#94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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"; | ||||||
| import { validateURL } from "./utils"; | ||||||
|
|
||||||
| /** | ||||||
|
|
@@ -25,6 +27,8 @@ export interface ConfigurationInterface { | |||||
| readonly serverEndpoint?: string, | ||||||
| // Debug configuration. | ||||||
| debug?: DebugOptions, | ||||||
| // Optional list of plugins to instrument the current Glean instance. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| plugins?: GleanPlugin<GleanEvents>[], | ||||||
| } | ||||||
|
|
||||||
| export class Configuration implements ConfigurationInterface { | ||||||
|
|
@@ -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; | ||||||
|
|
||||||
| 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>> = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, this is here to define:
This way, when we define a plugin like this |
||
| name: "afterPingCollection" | ||
| }; | ||
|
|
||
| type GleanEvents = typeof AfterPingCollection; | ||
| export default GleanEvents; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -50,7 +52,8 @@ class Glean { | |
| metrics: MetricsDatabase, | ||
| events: EventsDatabase, | ||
| pings: PingsDatabase | ||
| } | ||
| }; | ||
| private _plugins: { [event: string]: GleanPlugin<GleanEvents> }; | ||
|
|
||
| private constructor() { | ||
| if (!isUndefined(Glean._instance)) { | ||
|
|
@@ -64,6 +67,7 @@ class Glean { | |
| this._coreMetrics = new CoreMetrics(); | ||
| this._corePings = new CorePings(); | ||
| this._initialized = false; | ||
| this._plugins = {}; | ||
| } | ||
|
|
||
| private static get instance(): Glean { | ||
|
|
@@ -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}'.`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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".
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| * | ||
|
|
@@ -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. | ||
| // | ||
|
|
@@ -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. | ||
| * | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -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(); | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
|
|
||
|
|
@@ -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(); | ||
|
|
||
|
|
@@ -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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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(); | ||||||||||||||||
|
|
@@ -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); | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
What about this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Much better. |
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| const headers = getPingHeaders(); | ||||||||||||||||
|
|
||||||||||||||||
| 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
ConfigurationInterfacetoGlean.initialize, and this is here to help define exactly what we are passing through that property.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GleanEventsis a union of all the possible GleanEvents types (not a union right now since we only defined one event, but you get the gist). AndGleanPluginis 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 thepluginsproperty isGleanPlugin<GleanEvents>[].