Bug 1942239 - Add option to explicitly enable incremental origin initialization in...
[gecko.git] / toolkit / components / formautofill / shared / AutofillTelemetry.sys.mjs
blobad3636c30066cf9e633032bf835ad691b73db168
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/. */
5 import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
7 const { FIELD_STATES } = FormAutofillUtils;
9 class AutofillTelemetryBase {
10   SUPPORTED_FIELDS = {};
12   EVENT_CATEGORY = null;
13   EVENT_OBJECT_FORM_INTERACTION = null;
15   HISTOGRAM_NUM_USES = null;
16   HISTOGRAM_PROFILE_NUM_USES = null;
17   HISTOGRAM_PROFILE_NUM_USES_KEY = null;
19   #initFormEventExtra(value) {
20     let extra = {};
21     for (const field of Object.values(this.SUPPORTED_FIELDS)) {
22       extra[field] = value;
23     }
24     return extra;
25   }
27   #setFormEventExtra(extra, key, value) {
28     if (!this.SUPPORTED_FIELDS[key]) {
29       return;
30     }
32     extra[this.SUPPORTED_FIELDS[key]] = value;
33   }
35   /**
36    * Building the extra keys object that is included in the Legacy Telemetry event `cc_form_v2`
37    * or `address_form` event and the Glean event `cc_form`, and `address_form`.
38    * It indicates the detected credit card or address fields and which method (autocomplete property, regular expression heuristics or fathom) identified them.
39    *
40    * @param {Array<object>} fieldDetails fieldDetails to extract which fields were identified and how
41    * @param {string} undetected Default value when a field is not detected: 'undetected' (Glean) and 'false' in (Legacy)
42    * @param {string} autocomplete Value when a field is identified with autocomplete property: 'autocomplete' (Glean), 'true' (Legacy)
43    * @param {string} regexp Value when a field is identified with regex expression heuristics: 'regexp' (Glean), '0' (Legacy)
44    * @param {boolean} includeMultiPart Include multi part data or not
45    * @returns {object} Extra keys to include in the form event
46    */
47   #buildFormDetectedEventExtra(
48     fieldDetails,
49     undetected,
50     autocomplete,
51     regexp,
52     includeMultiPart
53   ) {
54     let extra = this.#initFormEventExtra(undetected);
56     let identified = new Set();
57     fieldDetails.forEach(detail => {
58       identified.add(detail.fieldName);
60       if (detail.reason == "autocomplete") {
61         this.#setFormEventExtra(extra, detail.fieldName, autocomplete);
62       } else {
63         // confidence exists only when a field is identified by fathom.
64         let confidence =
65           detail.confidence > 0 ? Math.floor(100 * detail.confidence) / 100 : 0;
67         this.#setFormEventExtra(
68           extra,
69           detail.fieldName,
70           confidence ? confidence.toString() : regexp
71         );
72       }
74       if (
75         detail.fieldName === "cc-number" &&
76         this.SUPPORTED_FIELDS[detail.fieldName] &&
77         includeMultiPart
78       ) {
79         extra.cc_number_multi_parts = detail.part ?? 1;
80       }
81     });
82     return extra;
83   }
85   recordFormDetected(flowId, fieldDetails) {
86     this.recordFormEvent(
87       "detected",
88       flowId,
89       this.#buildFormDetectedEventExtra(
90         fieldDetails,
91         "false",
92         "true",
93         "0",
94         false
95       )
96     );
98     this.recordGleanFormEvent(
99       "formDetected",
100       flowId,
101       this.#buildFormDetectedEventExtra(
102         fieldDetails,
103         "undetected",
104         "autocomplete",
105         "regexp",
106         true
107       )
108     );
110     try {
111       this.recordIframeLayoutDetection(flowId, fieldDetails);
112     } catch {}
113   }
115   recordPopupShown(flowId, fieldDetails) {
116     const extra = { field_name: fieldDetails[0].fieldName };
117     this.recordFormEvent("popup_shown", flowId, extra);
118     this.recordGleanFormEvent("formPopupShown", flowId, extra);
119   }
121   recordFormFilled(flowId, fieldDetails, data) {
122     // Calculate values for telemetry
123     const extra = this.#initFormEventExtra("unavailable");
125     for (const fieldDetail of fieldDetails) {
126       // It is possible that we don't autofill a field because it is cross-origin.
127       // When that happens, the data will not include that element.
128       let { filledState, filledValue } = data.get(fieldDetail.elementId) ?? {};
129       switch (filledState) {
130         case FIELD_STATES.AUTO_FILLED:
131           filledState = "filled";
132           break;
133         case FIELD_STATES.NORMAL:
134         default:
135           filledState =
136             fieldDetail.localName == "select" || filledValue?.length
137               ? "user_filled"
138               : "not_filled";
139           break;
140       }
141       this.#setFormEventExtra(extra, fieldDetail.fieldName, filledState);
142     }
144     this.recordFormEvent("filled", flowId, extra);
145     this.recordGleanFormEvent("formFilled", flowId, extra);
146   }
148   recordFilledModified(flowId, fieldDetails) {
149     const extra = { field_name: fieldDetails[0].fieldName };
150     this.recordFormEvent("filled_modified", flowId, extra);
151     this.recordGleanFormEvent("formFilledModified", flowId, extra);
152   }
154   recordFormSubmitted(flowId, fieldDetails, data) {
155     const extra = this.#initFormEventExtra("unavailable");
157     for (const fieldDetail of fieldDetails) {
158       let { filledState, filledValue } = data.get(fieldDetail.elementId) ?? {};
159       switch (filledState) {
160         case FIELD_STATES.AUTO_FILLED:
161           filledState = "autofilled";
162           break;
163         case FIELD_STATES.NORMAL:
164         default:
165           filledState =
166             fieldDetail.localName == "select" || filledValue?.length
167               ? "user_filled"
168               : "not_filled";
169           break;
170       }
171       this.#setFormEventExtra(extra, fieldDetail.fieldName, filledState);
172     }
174     this.recordFormEvent("submitted", flowId, extra);
175     this.recordGleanFormEvent("formSubmitted", flowId, extra);
176   }
178   recordFormCleared(flowId, fieldDetails) {
179     const extra = { field_name: fieldDetails[0].fieldName };
181     // Note that when a form is cleared, we also record `filled_modified` events
182     // for all the fields that have been cleared.
183     this.recordFormEvent("cleared", flowId, extra);
184     this.recordGleanFormEvent("formCleared", flowId, extra);
185   }
187   recordFormEvent(_method, _flowId, _extra) {
188     throw new Error("Not implemented.");
189   }
191   recordGleanFormEvent(_eventName, _flowId, _extra) {
192     throw new Error("Not implemented.");
193   }
195   recordFormInteractionEvent(method, flowId, fieldDetails, data) {
196     if (!this.EVENT_OBJECT_FORM_INTERACTION) {
197       return undefined;
198     }
199     switch (method) {
200       case "detected":
201         return this.recordFormDetected(flowId, fieldDetails);
202       case "popup_shown":
203         return this.recordPopupShown(flowId, fieldDetails);
204       case "filled":
205         return this.recordFormFilled(flowId, fieldDetails, data);
206       case "filled_modified":
207         return this.recordFilledModified(flowId, fieldDetails);
208       case "submitted":
209         return this.recordFormSubmitted(flowId, fieldDetails, data);
210       case "cleared":
211         return this.recordFormCleared(flowId, fieldDetails);
212     }
213     return undefined;
214   }
216   recordDoorhangerEvent(method, object, flowId) {
217     const eventName = `${method}_${object}`.replace(/(_[a-z])/g, c =>
218       c[1].toUpperCase()
219     );
220     Glean[this.EVENT_CATEGORY][eventName]?.record({ value: flowId });
221   }
223   recordManageEvent(method) {
224     const eventName =
225       method.replace(/(_[a-z])/g, c => c[1].toUpperCase()) + "Manage";
226     Glean[this.EVENT_CATEGORY][eventName]?.record();
227   }
229   recordAutofillProfileCount(_count) {
230     throw new Error("Not implemented.");
231   }
233   recordNumberOfUse(records) {
234     let histogram = Services.telemetry.getKeyedHistogramById(
235       this.HISTOGRAM_PROFILE_NUM_USES
236     );
237     histogram.clear();
239     for (let record of records) {
240       histogram.add(this.HISTOGRAM_PROFILE_NUM_USES_KEY, record.timesUsed);
241     }
242   }
244   recordIframeLayoutDetection(flowId, fieldDetails) {
245     const fieldsInMainFrame = [];
246     const fieldsInIframe = [];
247     const fieldsInSandboxedIframe = [];
248     const fieldsInCrossOrignIframe = [];
250     const iframes = new Set();
251     for (const fieldDetail of fieldDetails) {
252       const bc = BrowsingContext.get(fieldDetail.browsingContextId);
253       if (bc.top == bc) {
254         fieldsInMainFrame.push(fieldDetail);
255         continue;
256       }
258       iframes.add(bc);
259       fieldsInIframe.push(fieldDetail);
260       if (bc.sandboxFlags != 0) {
261         fieldsInSandboxedIframe.push(fieldDetail);
262       }
264       if (!FormAutofillUtils.isBCSameOriginWithTop(bc)) {
265         fieldsInCrossOrignIframe.push(fieldDetail);
266       }
267     }
269     const extra = {
270       category: this.EVENT_CATEGORY,
271       flow_id: flowId,
272       iframe_count: iframes.size,
273       main_frame: fieldsInMainFrame.map(f => f.fieldName).toString(),
274       iframe: fieldsInIframe.map(f => f.fieldName).toString(),
275       cross_origin: fieldsInCrossOrignIframe.map(f => f.fieldName).toString(),
276       sandboxed: fieldsInSandboxedIframe.map(f => f.fieldName).toString(),
277     };
279     Glean.formautofill.iframeLayoutDetection.record(extra);
280   }
283 export class AddressTelemetry extends AutofillTelemetryBase {
284   EVENT_CATEGORY = "address";
285   EVENT_OBJECT_FORM_INTERACTION = "AddressForm";
286   EVENT_OBJECT_FORM_INTERACTION_EXT = "AddressFormExt";
288   HISTOGRAM_PROFILE_NUM_USES = "AUTOFILL_PROFILE_NUM_USES";
289   HISTOGRAM_PROFILE_NUM_USES_KEY = "address";
291   // Fields that are record in `address_form` and `address_form_ext` telemetry
292   SUPPORTED_FIELDS = {
293     "street-address": "street_address",
294     "address-line1": "address_line1",
295     "address-line2": "address_line2",
296     "address-line3": "address_line3",
297     "address-level1": "address_level1",
298     "address-level2": "address_level2",
299     "postal-code": "postal_code",
300     country: "country",
301     name: "name",
302     "given-name": "given_name",
303     "additional-name": "additional_name",
304     "family-name": "family_name",
305     email: "email",
306     organization: "organization",
307     tel: "tel",
308   };
310   // Fields that are record in `address_form` event telemetry extra_keys
311   static SUPPORTED_FIELDS_IN_FORM = [
312     "street_address",
313     "address_line1",
314     "address_line2",
315     "address_line3",
316     "address_level2",
317     "address_level1",
318     "postal_code",
319     "country",
320   ];
322   // Fields that are record in `address_form_ext` event telemetry extra_keys
323   static SUPPORTED_FIELDS_IN_FORM_EXT = [
324     "name",
325     "given_name",
326     "additional_name",
327     "family_name",
328     "email",
329     "organization",
330     "tel",
331   ];
333   recordGleanFormEvent(_eventName, _flowId, _extra) {
334     // To be implemented when migrating the legacy event address.address_form to Glean
335   }
337   recordFormEvent(method, flowId, extra) {
338     let extExtra = {};
339     if (["detected", "filled", "submitted"].includes(method)) {
340       for (const [key, value] of Object.entries(extra)) {
341         if (AddressTelemetry.SUPPORTED_FIELDS_IN_FORM_EXT.includes(key)) {
342           extExtra[key] = value;
343           delete extra[key];
344         }
345       }
346     }
348     const eventMethod = method.replace(/(_[a-z])/g, c => c[1].toUpperCase());
349     Glean.address[eventMethod + this.EVENT_OBJECT_FORM_INTERACTION]?.record({
350       value: flowId,
351       ...extra,
352     });
354     if (Object.keys(extExtra).length) {
355       Glean.address[
356         eventMethod + this.EVENT_OBJECT_FORM_INTERACTION_EXT
357       ]?.record({ value: flowId, ...extExtra });
358     }
359   }
361   recordAutofillProfileCount(count) {
362     Glean.formautofillAddresses.autofillProfilesCount.set(count);
363   }
366 class CreditCardTelemetry extends AutofillTelemetryBase {
367   EVENT_CATEGORY = "creditcard";
368   EVENT_OBJECT_FORM_INTERACTION = "CcFormV2";
370   HISTOGRAM_NUM_USES = "CREDITCARD_NUM_USES";
371   HISTOGRAM_PROFILE_NUM_USES = "AUTOFILL_PROFILE_NUM_USES";
372   HISTOGRAM_PROFILE_NUM_USES_KEY = "credit_card";
374   // Mapping of field name used in formautofill code to the field name
375   // used in the telemetry.
376   SUPPORTED_FIELDS = {
377     "cc-name": "cc_name",
378     "cc-number": "cc_number",
379     "cc-type": "cc_type",
380     "cc-exp": "cc_exp",
381     "cc-exp-month": "cc_exp_month",
382     "cc-exp-year": "cc_exp_year",
383   };
385   recordGleanFormEvent(eventName, flowId, extra) {
386     extra.flow_id = flowId;
387     Glean.formautofillCreditcards[eventName].record(extra);
388   }
390   recordFormEvent(method, flowId, aExtra) {
391     // Don't modify the passed-in aExtra as it's reused.
392     const extra = Object.assign({ value: flowId }, aExtra);
393     const eventMethod = method.replace(/(_[a-z])/g, c => c[1].toUpperCase());
394     Glean.creditcard[eventMethod + this.EVENT_OBJECT_FORM_INTERACTION]?.record(
395       extra
396     );
397   }
399   recordNumberOfUse(records) {
400     super.recordNumberOfUse(records);
402     if (!this.HISTOGRAM_NUM_USES) {
403       return;
404     }
406     let histogram = Services.telemetry.getHistogramById(
407       this.HISTOGRAM_NUM_USES
408     );
409     histogram.clear();
411     for (let record of records) {
412       histogram.add(record.timesUsed);
413     }
414   }
416   recordAutofillProfileCount(count) {
417     Glean.formautofillCreditcards.autofillProfilesCount.set(count);
418   }
421 export class AutofillTelemetry {
422   static #creditCardTelemetry = new CreditCardTelemetry();
423   static #addressTelemetry = new AddressTelemetry();
425   // const for `type` parameter used in the utility functions
426   static ADDRESS = "address";
427   static CREDIT_CARD = "creditcard";
429   static #getTelemetryByFieldDetail(fieldDetail) {
430     return FormAutofillUtils.isAddressField(fieldDetail.fieldName)
431       ? this.#addressTelemetry
432       : this.#creditCardTelemetry;
433   }
435   static #getTelemetryByType(type) {
436     return type == AutofillTelemetry.CREDIT_CARD
437       ? this.#creditCardTelemetry
438       : this.#addressTelemetry;
439   }
441   /**
442    * Utility functions for `doorhanger` event (defined in Events.yaml)
443    *
444    * Category: address or creditcard
445    * Event name: doorhanger
446    */
447   static recordDoorhangerShown(type, object, flowId) {
448     const telemetry = this.#getTelemetryByType(type);
449     telemetry.recordDoorhangerEvent("show", object, flowId);
450   }
452   static recordDoorhangerClicked(type, method, object, flowId) {
453     const telemetry = this.#getTelemetryByType(type);
455     // We don't have `create` method in telemetry, we treat `create` as `save`
456     switch (method) {
457       case "create":
458         method = "save";
459         break;
460       case "open-pref":
461         method = "pref";
462         break;
463       case "learn-more":
464         method = "learn_more";
465         break;
466     }
468     telemetry.recordDoorhangerEvent(method, object, flowId);
469   }
471   /**
472    * Utility functions for form event (defined in Events.yaml)
473    *
474    * Category: address or creditcard
475    * Event name: cc_form_v2, or address_form
476    */
478   static recordFormInteractionEvent(method, flowId, fieldDetails, data) {
479     const telemetry = this.#getTelemetryByFieldDetail(fieldDetails[0]);
480     telemetry.recordFormInteractionEvent(method, flowId, fieldDetails, data);
481   }
483   static recordManageEvent(type, method) {
484     const telemetry = this.#getTelemetryByType(type);
485     telemetry.recordManageEvent(method);
486   }
488   static recordAutofillProfileCount(type, count) {
489     const telemetry = this.#getTelemetryByType(type);
490     telemetry.recordAutofillProfileCount(count);
491   }
493   /**
494    * Utility functions for address/credit card number of use
495    */
496   static recordNumberOfUse(type, records) {
497     const telemetry = this.#getTelemetryByType(type);
498     telemetry.recordNumberOfUse(records);
499   }
501   static recordFormSubmissionHeuristicCount(label) {
502     Glean.formautofill.formSubmissionHeuristic[label].add(1);
503   }