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) {
21 for (const field of Object.values(this.SUPPORTED_FIELDS)) {
27 #setFormEventExtra(extra, key, value) {
28 if (!this.SUPPORTED_FIELDS[key]) {
32 extra[this.SUPPORTED_FIELDS[key]] = value;
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.
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
47 #buildFormDetectedEventExtra(
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);
63 // confidence exists only when a field is identified by fathom.
65 detail.confidence > 0 ? Math.floor(100 * detail.confidence) / 100 : 0;
67 this.#setFormEventExtra(
70 confidence ? confidence.toString() : regexp
75 detail.fieldName === "cc-number" &&
76 this.SUPPORTED_FIELDS[detail.fieldName] &&
79 extra.cc_number_multi_parts = detail.part ?? 1;
85 recordFormDetected(flowId, fieldDetails) {
89 this.#buildFormDetectedEventExtra(
98 this.recordGleanFormEvent(
101 this.#buildFormDetectedEventExtra(
111 this.recordIframeLayoutDetection(flowId, fieldDetails);
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);
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";
133 case FIELD_STATES.NORMAL:
136 fieldDetail.localName == "select" || filledValue?.length
141 this.#setFormEventExtra(extra, fieldDetail.fieldName, filledState);
144 this.recordFormEvent("filled", flowId, extra);
145 this.recordGleanFormEvent("formFilled", flowId, extra);
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);
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";
163 case FIELD_STATES.NORMAL:
166 fieldDetail.localName == "select" || filledValue?.length
171 this.#setFormEventExtra(extra, fieldDetail.fieldName, filledState);
174 this.recordFormEvent("submitted", flowId, extra);
175 this.recordGleanFormEvent("formSubmitted", flowId, extra);
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);
187 recordFormEvent(_method, _flowId, _extra) {
188 throw new Error("Not implemented.");
191 recordGleanFormEvent(_eventName, _flowId, _extra) {
192 throw new Error("Not implemented.");
195 recordFormInteractionEvent(method, flowId, fieldDetails, data) {
196 if (!this.EVENT_OBJECT_FORM_INTERACTION) {
201 return this.recordFormDetected(flowId, fieldDetails);
203 return this.recordPopupShown(flowId, fieldDetails);
205 return this.recordFormFilled(flowId, fieldDetails, data);
206 case "filled_modified":
207 return this.recordFilledModified(flowId, fieldDetails);
209 return this.recordFormSubmitted(flowId, fieldDetails, data);
211 return this.recordFormCleared(flowId, fieldDetails);
216 recordDoorhangerEvent(method, object, flowId) {
217 const eventName = `${method}_${object}`.replace(/(_[a-z])/g, c =>
220 Glean[this.EVENT_CATEGORY][eventName]?.record({ value: flowId });
223 recordManageEvent(method) {
225 method.replace(/(_[a-z])/g, c => c[1].toUpperCase()) + "Manage";
226 Glean[this.EVENT_CATEGORY][eventName]?.record();
229 recordAutofillProfileCount(_count) {
230 throw new Error("Not implemented.");
233 recordNumberOfUse(records) {
234 let histogram = Services.telemetry.getKeyedHistogramById(
235 this.HISTOGRAM_PROFILE_NUM_USES
239 for (let record of records) {
240 histogram.add(this.HISTOGRAM_PROFILE_NUM_USES_KEY, record.timesUsed);
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);
254 fieldsInMainFrame.push(fieldDetail);
259 fieldsInIframe.push(fieldDetail);
260 if (bc.sandboxFlags != 0) {
261 fieldsInSandboxedIframe.push(fieldDetail);
264 if (!FormAutofillUtils.isBCSameOriginWithTop(bc)) {
265 fieldsInCrossOrignIframe.push(fieldDetail);
270 category: this.EVENT_CATEGORY,
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(),
279 Glean.formautofill.iframeLayoutDetection.record(extra);
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
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",
302 "given-name": "given_name",
303 "additional-name": "additional_name",
304 "family-name": "family_name",
306 organization: "organization",
310 // Fields that are record in `address_form` event telemetry extra_keys
311 static SUPPORTED_FIELDS_IN_FORM = [
322 // Fields that are record in `address_form_ext` event telemetry extra_keys
323 static SUPPORTED_FIELDS_IN_FORM_EXT = [
333 recordGleanFormEvent(_eventName, _flowId, _extra) {
334 // To be implemented when migrating the legacy event address.address_form to Glean
337 recordFormEvent(method, flowId, extra) {
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;
348 const eventMethod = method.replace(/(_[a-z])/g, c => c[1].toUpperCase());
349 Glean.address[eventMethod + this.EVENT_OBJECT_FORM_INTERACTION]?.record({
354 if (Object.keys(extExtra).length) {
356 eventMethod + this.EVENT_OBJECT_FORM_INTERACTION_EXT
357 ]?.record({ value: flowId, ...extExtra });
361 recordAutofillProfileCount(count) {
362 Glean.formautofillAddresses.autofillProfilesCount.set(count);
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.
377 "cc-name": "cc_name",
378 "cc-number": "cc_number",
379 "cc-type": "cc_type",
381 "cc-exp-month": "cc_exp_month",
382 "cc-exp-year": "cc_exp_year",
385 recordGleanFormEvent(eventName, flowId, extra) {
386 extra.flow_id = flowId;
387 Glean.formautofillCreditcards[eventName].record(extra);
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(
399 recordNumberOfUse(records) {
400 super.recordNumberOfUse(records);
402 if (!this.HISTOGRAM_NUM_USES) {
406 let histogram = Services.telemetry.getHistogramById(
407 this.HISTOGRAM_NUM_USES
411 for (let record of records) {
412 histogram.add(record.timesUsed);
416 recordAutofillProfileCount(count) {
417 Glean.formautofillCreditcards.autofillProfilesCount.set(count);
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;
435 static #getTelemetryByType(type) {
436 return type == AutofillTelemetry.CREDIT_CARD
437 ? this.#creditCardTelemetry
438 : this.#addressTelemetry;
442 * Utility functions for `doorhanger` event (defined in Events.yaml)
444 * Category: address or creditcard
445 * Event name: doorhanger
447 static recordDoorhangerShown(type, object, flowId) {
448 const telemetry = this.#getTelemetryByType(type);
449 telemetry.recordDoorhangerEvent("show", object, flowId);
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`
464 method = "learn_more";
468 telemetry.recordDoorhangerEvent(method, object, flowId);
472 * Utility functions for form event (defined in Events.yaml)
474 * Category: address or creditcard
475 * Event name: cc_form_v2, or address_form
478 static recordFormInteractionEvent(method, flowId, fieldDetails, data) {
479 const telemetry = this.#getTelemetryByFieldDetail(fieldDetails[0]);
480 telemetry.recordFormInteractionEvent(method, flowId, fieldDetails, data);
483 static recordManageEvent(type, method) {
484 const telemetry = this.#getTelemetryByType(type);
485 telemetry.recordManageEvent(method);
488 static recordAutofillProfileCount(type, count) {
489 const telemetry = this.#getTelemetryByType(type);
490 telemetry.recordAutofillProfileCount(count);
494 * Utility functions for address/credit card number of use
496 static recordNumberOfUse(type, records) {
497 const telemetry = this.#getTelemetryByType(type);
498 telemetry.recordNumberOfUse(records);
501 static recordFormSubmissionHeuristicCount(label) {
502 Glean.formautofill.formSubmissionHeuristic[label].add(1);