summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs')
-rw-r--r--browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs276
1 files changed, 276 insertions, 0 deletions
diff --git a/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs b/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs
new file mode 100644
index 0000000000..1447d3ebde
--- /dev/null
+++ b/browser/components/aboutwelcome/modules/AboutWelcomeTelemetry.sys.mjs
@@ -0,0 +1,276 @@
+/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
+ ClientID: "resource://gre/modules/ClientID.sys.mjs",
+ TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "telemetryClientId", () =>
+ lazy.ClientID.getClientID()
+);
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "browserSessionId",
+ () => lazy.TelemetrySession.getMetadata("").sessionId
+);
+
+ChromeUtils.defineLazyGetter(lazy, "log", () => {
+ const { Logger } = ChromeUtils.importESModule(
+ "resource://messaging-system/lib/Logger.sys.mjs"
+ );
+ return new Logger("AboutWelcomeTelemetry");
+});
+
+export class AboutWelcomeTelemetry {
+ constructor() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "telemetryEnabled",
+ "browser.newtabpage.activity-stream.telemetry",
+ false
+ );
+ }
+
+ /**
+ * Attach browser attribution data to a ping payload.
+ *
+ * It intentionally queries the *cached* attribution data other than calling
+ * `getAttrDataAsync()` in order to minimize the overhead here.
+ * For the same reason, we are not querying the attribution data from
+ * `TelemetryEnvironment.currentEnvironment.settings`.
+ *
+ * In practice, it's very likely that the attribution data is already read
+ * and cached at some point by `AboutWelcomeParent`, so it should be able to
+ * read the cached results for the most if not all of the pings.
+ */
+ _maybeAttachAttribution(ping) {
+ const attribution = lazy.AttributionCode.getCachedAttributionData();
+ if (attribution && Object.keys(attribution).length) {
+ ping.attribution = attribution;
+ }
+ return ping;
+ }
+
+ async _createPing(event) {
+ if (event.event_context && typeof event.event_context === "object") {
+ event.event_context = JSON.stringify(event.event_context);
+ }
+ let ping = {
+ ...event,
+ addon_version: Services.appinfo.appBuildID,
+ locale: Services.locale.appLocaleAsBCP47,
+ client_id: await lazy.telemetryClientId,
+ browser_session_id: lazy.browserSessionId,
+ };
+
+ return this._maybeAttachAttribution(ping);
+ }
+
+ /**
+ * Augment the provided event with some metadata and then send it
+ * to the messaging-system's onboarding endpoint.
+ *
+ * Is sometimes used by non-onboarding events.
+ *
+ * @param event - an object almost certainly from an onboarding flow (though
+ * there is a case where spotlight may use this, too)
+ * containing a nested structure of data for reporting as
+ * telemetry, as documented in
+ * https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/data_events.html
+ * Does not have all of its data (`_createPing` will augment
+ * with ids and attribution if available).
+ */
+ async sendTelemetry(event) {
+ if (!this.telemetryEnabled) {
+ return;
+ }
+
+ const ping = await this._createPing(event);
+
+ try {
+ this.submitGleanPingForPing(ping);
+ } catch (e) {
+ // Though Glean APIs are forbidden to throw, it may be possible that a
+ // mismatch between the shape of `ping` and the defined metrics is not
+ // adequately handled.
+ Glean.messagingSystem.gleanPingForPingFailures.add(1);
+ }
+ }
+
+ /**
+ * Tries to infer appropriate Glean metrics on the "messaging-system" ping,
+ * sets them, and submits a "messaging-system" ping.
+ *
+ * Does not check if telemetry is enabled.
+ * (Though Glean will check the global prefs).
+ *
+ * Note: This is a very unusual use of Glean that is specific to the use-
+ * cases of Messaging System. Please do not copy this pattern.
+ */
+ submitGleanPingForPing(ping) {
+ lazy.log.debug(`Submitting Glean ping for ${JSON.stringify(ping)}`);
+ // event.event_context is an object, but it may have been stringified.
+ let event_context = ping?.event_context;
+ let shopping_callout_impression =
+ ping?.message_id?.startsWith("FAKESPOT_CALLOUT") &&
+ ping?.event === "IMPRESSION";
+
+ if (typeof event_context === "string") {
+ try {
+ event_context = JSON.parse(event_context);
+ // This code is for directing Shopping component based clicks into
+ // the Glean Events ping.
+ if (
+ event_context?.page === "about:shoppingsidebar" ||
+ shopping_callout_impression
+ ) {
+ this.handleShoppingPings(ping, event_context);
+ }
+ } catch (e) {
+ // The Empty JSON strings and non-objects often provided by the
+ // existing telemetry we need to send failing to parse do not fit in
+ // the spirit of what this error is meant to capture. Instead, we want
+ // to capture when what we got should have been an object,
+ // but failed to parse.
+ if (event_context.length && event_context.includes("{")) {
+ Glean.messagingSystem.eventContextParseError.add(1);
+ }
+ }
+ }
+
+ // We echo certain properties from event_context into their own metrics
+ // to aid analysis.
+ if (event_context?.reason) {
+ Glean.messagingSystem.eventReason.set(event_context.reason);
+ }
+ if (event_context?.page) {
+ Glean.messagingSystem.eventPage.set(event_context.page);
+ }
+ if (event_context?.source) {
+ Glean.messagingSystem.eventSource.set(event_context.source);
+ }
+ if (event_context?.screen_family) {
+ Glean.messagingSystem.eventScreenFamily.set(event_context.screen_family);
+ }
+ // Screen_index was being coerced into a boolean value
+ // which resulted in 0 (first screen index) being ignored.
+ if (Number.isInteger(event_context?.screen_index)) {
+ Glean.messagingSystem.eventScreenIndex.set(event_context.screen_index);
+ }
+ if (event_context?.screen_id) {
+ Glean.messagingSystem.eventScreenId.set(event_context.screen_id);
+ }
+ if (event_context?.screen_initials) {
+ Glean.messagingSystem.eventScreenInitials.set(
+ event_context.screen_initials
+ );
+ }
+
+ // The event_context is also provided as-is as stringified JSON.
+ if (event_context) {
+ Glean.messagingSystem.eventContext.set(JSON.stringify(event_context));
+ }
+
+ if ("attribution" in ping) {
+ for (const [key, value] of Object.entries(ping.attribution)) {
+ const camelKey = this._snakeToCamelCase(key);
+ try {
+ Glean.messagingSystemAttribution[camelKey].set(value);
+ } catch (e) {
+ // We here acknowledge that we don't know the full breadth of data
+ // being collected. Ideally AttributionCode will later centralize
+ // definition and reporting of attribution data and we can be rid of
+ // this fail-safe for collecting the names of unknown keys.
+ Glean.messagingSystemAttribution.unknownKeys[camelKey].add(1);
+ }
+ }
+ }
+
+ // List of keys handled above.
+ const handledKeys = ["event_context", "attribution"];
+
+ for (const [key, value] of Object.entries(ping)) {
+ if (handledKeys.includes(key)) {
+ continue;
+ }
+ const camelKey = this._snakeToCamelCase(key);
+ try {
+ // We here acknowledge that even known keys might have non-scalar
+ // values. We're pretty sure we handled them all with handledKeys,
+ // but we might not have.
+ // Ideally this can later be removed after running for a version or two
+ // with no values seen in messaging_system.invalid_nested_data
+ if (typeof value === "object") {
+ Glean.messagingSystem.invalidNestedData[camelKey].add(1);
+ } else {
+ Glean.messagingSystem[camelKey].set(value);
+ }
+ } catch (e) {
+ // We here acknowledge that we don't know the full breadth of data being
+ // collected. Ideally we will later gain that confidence and can remove
+ // this fail-safe for collecting the names of unknown keys.
+ Glean.messagingSystem.unknownKeys[camelKey].add(1);
+ // TODO(bug 1600008): For testing, also record the overall count.
+ Glean.messagingSystem.unknownKeyCount.add(1);
+ }
+ }
+
+ // With all the metrics set, now it's time to submit this ping.
+ GleanPings.messagingSystem.submit();
+ }
+
+ _snakeToCamelCase(s) {
+ return s.toString().replace(/_([a-z])/gi, (_str, group) => {
+ return group.toUpperCase();
+ });
+ }
+
+ handleShoppingPings(ping, event_context) {
+ const message_id = ping?.message_id;
+ // This function helps direct a shopping ping to the correct Glean event.
+ if (message_id.startsWith("FAKESPOT_OPTIN_DEFAULT")) {
+ // Onboarding page message IDs are generated, but can reliably be
+ // assumed to start in this manner.
+ switch (ping?.event) {
+ case "CLICK_BUTTON":
+ switch (event_context?.source) {
+ case "privacy_policy":
+ Glean.shopping.surfaceShowPrivacyPolicyClicked.record();
+ break;
+ case "terms_of_use":
+ Glean.shopping.surfaceShowTermsClicked.record();
+ break;
+ case "primary_button":
+ // corresponds to 'Analyze Reviews'
+ Glean.shopping.surfaceOptInClicked.record();
+ break;
+ case "additional_button":
+ // corresponds to "Not Now"
+ Glean.shopping.surfaceNotNowClicked.record();
+ break;
+ case "learn_more":
+ Glean.shopping.surfaceLearnMoreClicked.record();
+ break;
+ }
+ break;
+ case "IMPRESSION":
+ Glean.shopping.surfaceOnboardingDisplayed.record({
+ configuration: ping?.message_id,
+ });
+ break;
+ }
+ }
+ if (message_id.startsWith("FAKESPOT_CALLOUT")) {
+ Glean.shopping.addressBarFeatureCalloutDisplayed.record({
+ configuration: message_id,
+ });
+ }
+ }
+}