AboutWelcomeTelemetry.sys.mjs (8274B)
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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 AttributionCode: 11 "moz-src:///browser/components/attribution/AttributionCode.sys.mjs", 12 ClientID: "resource://gre/modules/ClientID.sys.mjs", 13 TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs", 14 }); 15 16 ChromeUtils.defineLazyGetter(lazy, "telemetryClientId", () => 17 lazy.ClientID.getClientID() 18 ); 19 ChromeUtils.defineLazyGetter( 20 lazy, 21 "browserSessionId", 22 () => lazy.TelemetrySession.getMetadata("").sessionId 23 ); 24 25 ChromeUtils.defineLazyGetter(lazy, "log", () => { 26 const { Logger } = ChromeUtils.importESModule( 27 "resource://messaging-system/lib/Logger.sys.mjs" 28 ); 29 return new Logger("AboutWelcomeTelemetry"); 30 }); 31 32 export class AboutWelcomeTelemetry { 33 constructor() { 34 XPCOMUtils.defineLazyPreferenceGetter( 35 this, 36 "telemetryEnabled", 37 "browser.newtabpage.activity-stream.telemetry", 38 false 39 ); 40 } 41 42 /** 43 * Attach browser attribution data to a ping payload. 44 * 45 * It intentionally queries the *cached* attribution data other than calling 46 * `getAttrDataAsync()` in order to minimize the overhead here. 47 * For the same reason, we are not querying the attribution data from 48 * `TelemetryEnvironment.currentEnvironment.settings`. 49 * 50 * In practice, it's very likely that the attribution data is already read 51 * and cached at some point by `AboutWelcomeParent`, so it should be able to 52 * read the cached results for the most if not all of the pings. 53 */ 54 _maybeAttachAttribution(ping) { 55 const attribution = lazy.AttributionCode.getCachedAttributionData(); 56 if (attribution && Object.keys(attribution).length) { 57 ping.attribution = attribution; 58 } 59 return ping; 60 } 61 62 async _createPing(event) { 63 if (event.event_context && typeof event.event_context === "object") { 64 event.event_context = JSON.stringify(event.event_context); 65 } 66 let ping = { 67 ...event, 68 addon_version: Services.appinfo.appBuildID, 69 locale: Services.locale.appLocaleAsBCP47, 70 client_id: await lazy.telemetryClientId, 71 browser_session_id: lazy.browserSessionId, 72 }; 73 74 return this._maybeAttachAttribution(ping); 75 } 76 77 /** 78 * Augment the provided event with some metadata and then send it 79 * to the messaging-system's onboarding endpoint. 80 * 81 * Is sometimes used by non-onboarding events. 82 * 83 * @param event - an object almost certainly from an onboarding flow (though 84 * there is a case where spotlight may use this, too) 85 * containing a nested structure of data for reporting as 86 * telemetry, as documented in 87 * https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/data_events.html 88 * Does not have all of its data (`_createPing` will augment 89 * with ids and attribution if available). 90 */ 91 async sendTelemetry(event) { 92 if (!this.telemetryEnabled) { 93 return; 94 } 95 96 const ping = await this._createPing(event); 97 98 try { 99 this.submitGleanPingForPing(ping); 100 } catch (e) { 101 // Though Glean APIs are forbidden to throw, it may be possible that a 102 // mismatch between the shape of `ping` and the defined metrics is not 103 // adequately handled. 104 Glean.messagingSystem.gleanPingForPingFailures.add(1); 105 } 106 } 107 108 /** 109 * Tries to infer appropriate Glean metrics on the "messaging-system" ping, 110 * sets them, and submits a "messaging-system" ping. 111 * 112 * Does not check if telemetry is enabled. 113 * (Though Glean will check the global prefs). 114 * 115 * Note: This is a very unusual use of Glean that is specific to the use- 116 * cases of Messaging System. Please do not copy this pattern. 117 */ 118 submitGleanPingForPing(ping) { 119 lazy.log.debug(`Submitting Glean ping for ${JSON.stringify(ping)}`); 120 // event.event_context is an object, but it may have been stringified. 121 let event_context = ping?.event_context; 122 123 if (typeof event_context === "string") { 124 try { 125 event_context = JSON.parse(event_context); 126 } catch (e) { 127 // The Empty JSON strings and non-objects often provided by the 128 // existing telemetry we need to send failing to parse do not fit in 129 // the spirit of what this error is meant to capture. Instead, we want 130 // to capture when what we got should have been an object, 131 // but failed to parse. 132 if (event_context.length && event_context.includes("{")) { 133 Glean.messagingSystem.eventContextParseError.add(1); 134 } 135 } 136 } 137 138 // We echo certain properties from event_context into their own metrics 139 // to aid analysis. 140 if (event_context?.reason) { 141 Glean.messagingSystem.eventReason.set(event_context.reason); 142 } 143 if (event_context?.page) { 144 Glean.messagingSystem.eventPage.set(event_context.page); 145 } 146 if (event_context?.source) { 147 Glean.messagingSystem.eventSource.set(event_context.source); 148 } 149 if (event_context?.screen_family) { 150 Glean.messagingSystem.eventScreenFamily.set(event_context.screen_family); 151 } 152 // Screen_index was being coerced into a boolean value 153 // which resulted in 0 (first screen index) being ignored. 154 if (Number.isInteger(event_context?.screen_index)) { 155 Glean.messagingSystem.eventScreenIndex.set(event_context.screen_index); 156 } 157 if (event_context?.screen_id) { 158 Glean.messagingSystem.eventScreenId.set(event_context.screen_id); 159 } 160 if (event_context?.screen_initials) { 161 Glean.messagingSystem.eventScreenInitials.set( 162 event_context.screen_initials 163 ); 164 } 165 166 // The event_context is also provided as-is as stringified JSON. 167 if (event_context) { 168 Glean.messagingSystem.eventContext.set(JSON.stringify(event_context)); 169 } 170 171 if ("attribution" in ping) { 172 for (const [key, value] of Object.entries(ping.attribution)) { 173 const camelKey = this._snakeToCamelCase(key); 174 try { 175 Glean.messagingSystemAttribution[camelKey].set(value); 176 } catch (e) { 177 // We here acknowledge that we don't know the full breadth of data 178 // being collected. Ideally AttributionCode will later centralize 179 // definition and reporting of attribution data and we can be rid of 180 // this fail-safe for collecting the names of unknown keys. 181 Glean.messagingSystemAttribution.unknownKeys[camelKey].add(1); 182 } 183 } 184 } 185 186 // List of keys handled above. 187 const handledKeys = ["event_context", "attribution"]; 188 189 for (const [key, value] of Object.entries(ping)) { 190 if (handledKeys.includes(key)) { 191 continue; 192 } 193 const camelKey = this._snakeToCamelCase(key); 194 try { 195 // We here acknowledge that even known keys might have non-scalar 196 // values. We're pretty sure we handled them all with handledKeys, 197 // but we might not have. 198 // Ideally this can later be removed after running for a version or two 199 // with no values seen in messaging_system.invalid_nested_data 200 if (typeof value === "object") { 201 Glean.messagingSystem.invalidNestedData[camelKey].add(1); 202 } else { 203 Glean.messagingSystem[camelKey].set(value); 204 } 205 } catch (e) { 206 // We here acknowledge that we don't know the full breadth of data being 207 // collected. Ideally we will later gain that confidence and can remove 208 // this fail-safe for collecting the names of unknown keys. 209 Glean.messagingSystem.unknownKeys[camelKey].add(1); 210 // TODO(bug 1600008): For testing, also record the overall count. 211 Glean.messagingSystem.unknownKeyCount.add(1); 212 } 213 } 214 215 // With all the metrics set, now it's time to submit this ping. 216 GleanPings.messagingSystem.submit(); 217 } 218 219 _snakeToCamelCase(s) { 220 return s.toString().replace(/_([a-z])/gi, (_str, group) => { 221 return group.toUpperCase(); 222 }); 223 } 224 }