Bug 1801039 - P1. Share common code for Address and Credit Card r=sgalich

Differential Revision: https://phabricator.services.mozilla.com/D164351
This commit is contained in:
Dimi 2023-01-03 20:55:56 +00:00
parent c4e930dbcd
commit 73e1a5d878
12 changed files with 640 additions and 419 deletions

View File

@ -12,6 +12,11 @@ ChromeUtils.defineModuleGetter(
"formAutofillStorage",
"resource://autofill/FormAutofillStorage.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"AutofillTelemetry",
"resource://autofill/AutofillTelemetry.jsm"
);
class AutofillEditDialog {
constructor(subStorageName, elements, record) {
@ -148,11 +153,21 @@ class AutofillEditDialog {
// An interface to be inherited.
localizeDocument() {}
recordFormSubmit() {
let method = this._record?.guid ? "edit" : "add";
AutofillTelemetry.recordManageEvent(this.telemetryType, method);
}
}
class EditAddressDialog extends AutofillEditDialog {
telemetryType = AutofillTelemetry.ADDRESS;
constructor(elements, record) {
super("addresses", elements, record);
if (record) {
AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
}
}
localizeDocument() {
@ -169,11 +184,15 @@ class EditAddressDialog extends AutofillEditDialog {
this._elements.fieldContainer.buildFormObject(),
this._record ? this._record.guid : null
);
this.recordFormSubmit();
window.close();
}
}
class EditCreditCardDialog extends AutofillEditDialog {
telemetryType = AutofillTelemetry.CREDIT_CARD;
constructor(elements, record) {
elements.fieldContainer._elements.billingAddress.disabled = true;
super("creditCards", elements, record);
@ -182,7 +201,7 @@ class EditCreditCardDialog extends AutofillEditDialog {
this._onCCNumberFieldBlur.bind(this)
);
if (record) {
Services.telemetry.recordEvent("creditcard", "show_entry", "manage");
AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
}
}
@ -212,11 +231,7 @@ class EditCreditCardDialog extends AutofillEditDialog {
this._record ? this._record.guid : null
);
if (this._record?.guid) {
Services.telemetry.recordEvent("creditcard", "edit", "manage");
} else {
Services.telemetry.recordEvent("creditcard", "add", "manage");
}
this.recordFormSubmit();
window.close();
} catch (ex) {

View File

@ -19,6 +19,9 @@ const { XPCOMUtils } = ChromeUtils.importESModule(
const { FormAutofill } = ChromeUtils.import(
"resource://autofill/FormAutofill.jsm"
);
const { AutofillTelemetry } = ChromeUtils.import(
"resource://autofill/AutofillTelemetry.jsm"
);
ChromeUtils.defineESModuleGetters(this, {
CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
@ -192,6 +195,10 @@ class ManageRecords {
Services.obs.addObserver(this, "formautofill-storage-changed");
// For testing only: notify record(s) has been removed
this._elements.records.dispatchEvent(new CustomEvent("RecordsRemoved"));
for (let i = 0; i < options.length; i++) {
AutofillTelemetry.recordManageEvent(this.telemetryType, "delete");
}
}
/**
@ -315,12 +322,15 @@ class ManageRecords {
}
class ManageAddresses extends ManageRecords {
telemetryType = AutofillTelemetry.ADDRESS;
constructor(elements) {
super("addresses", elements);
elements.add.setAttribute(
"search-l10n-ids",
FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
);
AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
}
/**
@ -343,6 +353,8 @@ class ManageAddresses extends ManageRecords {
}
class ManageCreditCards extends ManageRecords {
telemetryType = AutofillTelemetry.CREDIT_CARD;
constructor(elements) {
super("creditCards", elements);
elements.add.setAttribute(
@ -350,9 +362,8 @@ class ManageCreditCards extends ManageRecords {
FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
);
Services.telemetry.recordEvent("creditcard", "show", "manage");
this._isDecrypted = false;
AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
}
/**
@ -451,13 +462,6 @@ class ManageCreditCards extends ManageRecords {
}
}
async removeRecords(options) {
await super.removeRecords(options);
for (let i = 0; i < options.length; i++) {
Services.telemetry.recordEvent("creditcard", "delete", "manage");
}
}
updateButtonsStates(selectedCount) {
super.updateButtonsStates(selectedCount);
}

View File

@ -4,7 +4,7 @@ const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const { CreditCardTelemetry } = ChromeUtils.import(
"resource://autofill/FormAutofillTelemetryUtils.jsm"
"resource://autofill/Autofilltelemetry.jsm"
);
const CC_NUM_USES_HISTOGRAM = "CREDITCARD_NUM_USES";

View File

@ -789,6 +789,10 @@ TESTCASES.forEach(testcase => {
.creditCard) {
delete ccRecord.flowId;
}
for (let addrRecord of FormAutofillContent._onFormSubmit.args[0][0]
.address) {
delete addrRecord.flowId;
}
Assert.deepEqual(
FormAutofillContent._onFormSubmit.args[0][0],

View File

@ -0,0 +1,473 @@
/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["AutofillTelemetry"];
const { FormAutofillUtils } = ChromeUtils.import(
"resource://autofill/FormAutofillUtils.jsm"
);
const { FormAutofillCreditCardSection } = ChromeUtils.import(
"resource://autofill/FormAutofillHandler.jsm"
);
const { FIELD_STATES } = FormAutofillUtils;
class AutofillTelemetryBase {
SUPPORTED_FIELDS = {};
EVENT_CATEGORY = null;
EVENT_OBJECT_FORM_INTERACTION = null;
SCALAR_DETECTED_SECTION_COUNT = null;
SCALAR_SUBMITTED_SECTION_COUNT = null;
SCALAR_AUTOFILL_PROFILE_COUNT = null;
HISTOGRAM_NUM_USES = null;
HISTOGRAM_PROFILE_NUM_USES = null;
HISTOGRAM_PROFILE_NUM_USES_KEY = null;
#initFormEventExtra(value) {
let extra = {};
for (const field of Object.values(this.SUPPORTED_FIELDS)) {
extra[field] = value;
}
return extra;
}
#setFormEventExtra(extra, key, value) {
extra[this.SUPPORTED_FIELDS[key]] = value;
}
recordFormDetected(section) {
let extra = this.#initFormEventExtra("false");
let identified = new Set();
section.fieldDetails.forEach(detail => {
identified.add(detail.fieldName);
if (detail._reason == "autocomplete") {
this.#setFormEventExtra(extra, detail.fieldName, "true");
} else {
// confidence exists only when a field is identified by fathom.
let confidence =
detail.confidence > 0 ? Math.floor(100 * detail.confidence) / 100 : 0;
this.#setFormEventExtra(extra, detail.fieldName, confidence.toString());
}
});
this.recordFormEvent("detected", section.flowId, extra);
}
recordPopupShown(section, fieldName) {
const extra = { field_name: fieldName };
this.recordFormEvent("popup_shown", section.flowId, extra);
}
recordFormFilled(section, profile) {
// Calculate values for telemetry
let extra = this.#initFormEventExtra("unavailable");
for (let fieldDetail of section.fieldDetails) {
let element = fieldDetail.elementWeakRef.get();
let state = profile[fieldDetail.fieldName] ? "filled" : "not_filled";
if (
fieldDetail.state == FIELD_STATES.NORMAL &&
(HTMLSelectElement.isInstance(element) ||
(HTMLInputElement.isInstance(element) && element.value.length))
) {
state = "user_filled";
}
this.#setFormEventExtra(extra, fieldDetail.fieldName, state);
}
this.recordFormEvent("filled", section.flowId, extra);
}
recordFilledModified(section, fieldName) {
const extra = { field_name: fieldName };
this.recordFormEvent("filled_modified", section.flowId, extra);
}
recordFormSubmitted(section, record, form) {
let extra = this.#initFormEventExtra("unavailable");
if (record.guid !== null) {
// If the `guid` is not null, it means we're editing an existing record.
// In that case, all fields in the record are autofilled, and fields in
// `untouchedFields` are unmodified.
for (let fieldName of Object.keys(record.record)) {
if (record.untouchedFields?.includes(fieldName)) {
this.#setFormEventExtra(extra, fieldName, "autofilled");
} else {
this.#setFormEventExtra(extra, fieldName, "user_filled");
}
}
} else {
Object.keys(record.record).forEach(fieldName =>
this.#setFormEventExtra(extra, fieldName, "user_filled")
);
}
this.recordFormEvent("submitted", section.flowId, extra);
}
recordFormCleared(section, fieldName) {
const extra = { field_name: fieldName };
// Note that when a form is cleared, we also record `filled_modified` events
// for all the fields that have been cleared.
this.recordFormEvent("cleared", section.flowId, extra);
}
recordFormEvent(method, flowId, extra) {
Services.telemetry.recordEvent(
this.EVENT_CATEGORY,
method,
this.EVENT_OBJECT_FORM_INTERACTION,
flowId,
extra
);
}
recordFormInteractionEvent(
method,
section,
{ fieldName, profile, record, form } = {}
) {
if (!this.EVENT_OBJECT_FORM_INTERACTION) {
return undefined;
}
switch (method) {
case "detected":
return this.recordFormDetected(section);
case "popup_shown":
return this.recordPopupShown(section, fieldName);
case "filled":
return this.recordFormFilled(section, profile);
case "filled_modified":
return this.recordFilledModified(section, fieldName);
case "submitted":
return this.recordFormSubmitted(section, record, form);
case "cleared":
return this.recordFormCleared(section, fieldName);
}
return undefined;
}
recordDoorhangerEvent(method, record) {
Services.telemetry.recordEvent(
this.EVENT_CATEGORY,
method,
record.guid ? "update_doorhanger" : "capture_doorhanger",
record.flowId
);
}
recordManageEvent(method, object) {
Services.telemetry.recordEvent(this.EVENT_CATEGORY, method, object);
}
recordAutofillProfileCount(count) {
if (!this.SCALAR_AUTOFILL_PROFILE_COUNT) {
return;
}
Services.telemetry.scalarSet(this.SCALAR_AUTOFILL_PROFILE_COUNT, count);
}
recordDetectedSectionCount() {
if (!this.SCALAR_DETECTED_SECTION_COUNT) {
return;
}
Services.telemetry.scalarAdd(this.SCALAR_DETECTED_SECTION_COUNT, 1);
}
recordSubmittedSectionCount(count) {
if (!this.SCALAR_SUBMITTED_SECTION_COUNT || !count) {
return;
}
Services.telemetry.scalarAdd(this.SCALAR_SUBMITTED_SECTION_COUNT, count);
}
recordNumberOfUse(records) {
let histogram = Services.telemetry.getKeyedHistogramById(
this.HISTOGRAM_PROFILE_NUM_USES
);
histogram.clear();
for (let record of records) {
histogram.add(this.HISTOGRAM_PROFILE_NUM_USES_KEY, record.timesUsed);
}
}
}
class AddressTelemetry extends AutofillTelemetryBase {
EVENT_CATEGORY = "address";
}
class CreditCardTelemetry extends AutofillTelemetryBase {
EVENT_CATEGORY = "creditcard";
EVENT_OBJECT_FORM_INTERACTION = "cc_form_v2";
SCALAR_DETECTED_SECTION_COUNT =
"formautofill.creditCards.detected_sections_count";
SCALAR_SUBMITTED_SECTION_COUNT =
"formautofill.creditCards.submitted_sections_count";
SCALAR_AUTOFILL_PROFILE_COUNT =
"formautofill.creditCards.autofill_profiles_count";
HISTOGRAM_NUM_USES = "CREDITCARD_NUM_USES";
HISTOGRAM_PROFILE_NUM_USES = "AUTOFILL_PROFILE_NUM_USES";
HISTOGRAM_PROFILE_NUM_USES_KEY = "credit_card";
// Mapping of field name used in formautofill code to the field name
// used in the telemetry.
SUPPORTED_FIELDS = {
"cc-name": "cc_name",
"cc-number": "cc_number",
"cc-type": "cc_type",
"cc-exp": "cc_exp",
"cc-exp-month": "cc_exp_month",
"cc-exp-year": "cc_exp_year",
};
recordLegacyFormEvent(method, flowId, extra = null) {
Services.telemetry.recordEvent(
this.EVENT_CATEGORY,
method,
"cc_form",
flowId,
extra
);
}
recordFormDetected(section) {
super.recordFormDetected(section);
let identified = new Set();
section.fieldDetails.forEach(detail => {
identified.add(detail.fieldName);
});
let extra = {
cc_name_found: identified.has("cc-name") ? "true" : "false",
cc_number_found: identified.has("cc-number") ? "true" : "false",
cc_exp_found:
identified.has("cc-exp") ||
(identified.has("cc-exp-month") && identified.has("cc-exp-year"))
? "true"
: "false",
};
this.recordLegacyFormEvent("detected", section.flowId, extra);
}
recordPopupShown(section, fieldName) {
super.recordPopupShown(section, fieldName);
this.recordLegacyFormEvent("popup_shown", section.flowId);
}
recordFormFilled(section, profile) {
super.recordFormFilled(section, profile);
// Calculate values for telemetry
let extra = {
cc_name: "unavailable",
cc_number: "unavailable",
cc_exp: "unavailable",
};
for (let fieldDetail of section.fieldDetails) {
let element = fieldDetail.elementWeakRef.get();
let state = profile[fieldDetail.fieldName] ? "filled" : "not_filled";
if (
fieldDetail.state == FIELD_STATES.NORMAL &&
(HTMLSelectElement.isInstance(element) ||
(HTMLInputElement.isInstance(element) && element.value.length))
) {
state = "user_filled";
}
switch (fieldDetail.fieldName) {
case "cc-name":
extra.cc_name = state;
break;
case "cc-number":
extra.cc_number = state;
break;
case "cc-exp":
case "cc-exp-month":
case "cc-exp-year":
extra.cc_exp = state;
break;
}
}
this.recordLegacyFormEvent("filled", section.flowId, extra);
}
recordFilledModified(section, fieldName) {
super.recordFilledModified(section, fieldName);
let extra = { field_name: fieldName };
this.recordLegacyFormEvent("filled_modified", section.flowId, extra);
}
/**
* Called when a credit card form is submitted
*
* @param {object} section Section that produces this record
* @param {object} record Credit card record filled in the form.
* @param {Array<HTMLForm>} form Form that contains the section
*/
recordFormSubmitted(section, record, form) {
super.recordFormSubmitted(section, record, form);
// For legacy cc_form event telemetry
let extra = {
fields_not_auto: "0",
fields_auto: "0",
fields_modified: "0",
};
if (record.guid !== null) {
let totalCount = form.elements.length;
let autofilledCount = Object.keys(record.record).length;
let unmodifiedCount = record.untouchedFields.length;
extra.fields_not_auto = (totalCount - autofilledCount).toString();
extra.fields_auto = autofilledCount.toString();
extra.fields_modified = (autofilledCount - unmodifiedCount).toString();
} else {
// If the `guid` is null, we're filling a new form.
// In that case, all not-null fields are manually filled.
extra.fields_not_auto = Array.from(form.elements)
.filter(element => !!element.value?.trim().length)
.length.toString();
}
this.recordLegacyFormEvent("submitted", section.flowId, extra);
}
recordNumberOfUse(records) {
super.recordNumberOfUse(records);
if (!this.HISTOGRAM_NUM_USES) {
return;
}
let histogram = Services.telemetry.getHistogramById(
this.HISTOGRAM_NUM_USES
);
histogram.clear();
for (let record of records) {
histogram.add(record.timesUsed);
}
}
}
class AutofillTelemetry {
static #creditCardTelemetry = new CreditCardTelemetry();
static #addressTelemetry = new AddressTelemetry();
// const for `type` parameter used in the utility functions
static ADDRESS = "address";
static CREDIT_CARD = "creditcard";
static #getTelemetryBySection(section) {
return section instanceof FormAutofillCreditCardSection
? this.#creditCardTelemetry
: this.#addressTelemetry;
}
static #getTelemetryByType(type) {
return type == AutofillTelemetry.CREDIT_CARD
? this.#creditCardTelemetry
: this.#addressTelemetry;
}
/**
* Utility functions for `doorhanger` event (defined in Events.yaml)
*
* Category: address or creditcard
* Event name: doorhanger
*/
static recordDoorhangerShown(type, record) {
const telemetry = this.#getTelemetryByType(type);
telemetry.recordDoorhangerEvent("show", record);
}
static recordDoorhangerClicked(type, method, record) {
const telemetry = this.#getTelemetryByType(type);
// We don't have `create` method in telemetry, we treat `create` as `save`
switch (method) {
case "create":
method = "save";
break;
case "open-pref":
method = "pref";
break;
}
telemetry.recordDoorhangerEvent(method, record);
}
/**
* Utility functions for form event (defined in Events.yaml)
*
* Category: address or creditcard
* Event name: cc_form, cc_form_v2, or address_form
*/
static recordFormInteractionEvent(
method,
section,
{ fieldName, profile, record, form } = {}
) {
const telemetry = this.#getTelemetryBySection(section);
telemetry.recordFormInteractionEvent(method, section, {
fieldName,
profile,
record,
form,
});
}
/**
* Utility functions for submitted section count scalar (defined in Scalars.yaml)
*
* Category: formautofill.creditCards or formautofill.addresses
* Scalar name: submitted_sections_count
*/
static recordDetectedSectionCount(section) {
const telemetry = this.#getTelemetryBySection(section);
telemetry.recordDetectedSectionCount();
}
static recordSubmittedSectionCount(type, count) {
const telemetry = this.#getTelemetryByType(type);
telemetry.recordSubmittedSectionCount(count);
}
static recordManageEvent(type, method) {
const telemetry = this.#getTelemetryByType(type);
telemetry.recordManageEvent(method, "manage");
}
static recordAutofillProfileCount(type, count) {
const telemetry = this.#getTelemetryByType(type);
telemetry.recordAutofillProfileCount(count);
}
/**
* Utility functions for address/credit card number of use
*/
static recordNumberOfUse(type, records) {
const telemetry = this.#getTelemetryByType(type);
telemetry.recordNumberOfUse(records);
}
}

View File

@ -32,7 +32,7 @@ XPCOMUtils.defineLazyModuleGetters(lazy, {
AddressResult: "resource://autofill/ProfileAutoCompleteResult.jsm",
ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.jsm",
CreditCardTelemetry: "resource://autofill/FormAutofillTelemetryUtils.jsm",
AutofillTelemetry: "resource://autofill/AutofillTelemetry.jsm",
FormAutofill: "resource://autofill/FormAutofill.jsm",
FormAutofillHandler: "resource://autofill/FormAutofillHandler.jsm",
FormAutofillUtils: "resource://autofill/FormAutofillUtils.jsm",
@ -549,10 +549,26 @@ var FormAutofillContent = {
return;
}
lazy.CreditCardTelemetry.recordFormSubmitted(
records,
handler.form.elements
);
[records.address, records.creditCard].forEach((rs, idx) => {
lazy.AutofillTelemetry.recordSubmittedSectionCount(
idx == 0
? lazy.AutofillTelemetry.ADDRESS
: lazy.AutofillTelemetry.CREDIT_CARD,
rs?.length
);
rs?.forEach(r => {
lazy.AutofillTelemetry.recordFormInteractionEvent(
"submitted",
r.section,
{
record: r,
form: handler.form,
}
);
delete r.section;
});
});
this._onFormSubmit(records, domWin, handler.timeStartedFillingMS);
},
@ -762,9 +778,10 @@ var FormAutofillContent = {
let fieldName = FormAutofillContent.activeFieldDetail?.fieldName;
if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) {
lazy.CreditCardTelemetry.recordFormCleared(
this.activeSection?.flowId,
fieldName
lazy.AutofillTelemetry.recordFormInteractionEvent(
"cleared",
this.activeSection,
{ fieldName }
);
}
},
@ -841,9 +858,10 @@ var FormAutofillContent = {
let fieldName = FormAutofillContent.activeFieldDetail?.fieldName;
if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) {
lazy.CreditCardTelemetry.recordPopupShown(
this.activeSection?.flowId,
fieldName
lazy.AutofillTelemetry.recordFormInteractionEvent(
"popup_shown",
this.activeSection,
{ fieldName }
);
}
},

View File

@ -8,7 +8,7 @@
"use strict";
var EXPORTED_SYMBOLS = ["FormAutofillHandler"];
var EXPORTED_SYMBOLS = ["FormAutofillHandler", "FormAutofillCreditCardSection"];
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
@ -32,7 +32,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
CreditCardTelemetry: "resource://autofill/FormAutofillTelemetryUtils.jsm",
AutofillTelemetry: "resource://autofill/AutofillTelemetry.jsm",
FormAutofillHeuristics: "resource://autofill/FormAutofillHeuristics.jsm",
});
@ -87,6 +87,16 @@ class FormAutofillSection {
allFieldNames: null,
matchingSelectOption: null,
};
// Identifier used to correlate events relating to the same form
this.flowId = Services.uuid.generateUUID().toString();
lazy.log.debug(
"Creating new credit card section with flowId =",
this.flowId
);
lazy.AutofillTelemetry.recordDetectedSectionCount(this);
lazy.AutofillTelemetry.recordFormInteractionEvent("detected", this);
}
/*
@ -407,6 +417,11 @@ class FormAutofillSection {
}
}
focusedInput.focus({ preventScroll: true });
lazy.AutofillTelemetry.recordFormInteractionEvent("filled", this, {
profile,
});
return true;
}
@ -620,11 +635,14 @@ class FormAutofillSection {
guid: this.filledRecordGUID,
record: {},
untouchedFields: [],
section: this,
};
if (this.flowId) {
data.flowId = this.flowId;
}
let condensedDetails = this.fieldDetails;
// TODO: This is credit card specific code...
this._condenseMultipleCCNumberFields(condensedDetails);
condensedDetails.forEach(detail => {
@ -684,9 +702,12 @@ class FormAutofillSection {
this._changeFieldState(targetFieldDetail, FIELD_STATES.NORMAL);
if (isCreditCardField) {
lazy.CreditCardTelemetry.recordFilledModified(
this.flowId,
targetFieldDetail.fieldName
lazy.AutofillTelemetry.recordFormInteractionEvent(
"filled_modified",
this,
{
fieldName: targetFieldDetail.fieldName,
}
);
}
@ -984,19 +1005,10 @@ class FormAutofillCreditCardSection extends FormAutofillSection {
this.handler = handler;
// Identifier used to correlate events relating to the same form
this.flowId = Services.uuid.generateUUID().toString();
lazy.log.debug(
"Creating new credit card section with flowId =",
this.flowId
);
if (!this.isValidSection()) {
return;
}
lazy.CreditCardTelemetry.recordFormDetected(this.flowId, fieldDetails);
// Check whether the section is in an <iframe>; and, if so,
// watch for the <iframe> to pagehide.
if (handler.window.location != handler.window.parent?.location) {
@ -1394,11 +1406,6 @@ class FormAutofillCreditCardSection extends FormAutofillSection {
return false;
}
lazy.CreditCardTelemetry.recordFormFilled(
this.flowId,
this.fieldDetails,
profile
);
return true;
}

View File

@ -477,6 +477,9 @@ class FormAutofillParent extends JSWindowActorParent {
async _onAddressSubmit(address, browser, timeStartedFillingMS) {
let showDoorhanger = null;
// Bug 1808176 - We should always ecord used count in this function regardless
// whether capture is enabled or not.
if (!FormAutofill.isAutofillAddressesCaptureEnabled) {
return showDoorhanger;
}
@ -508,9 +511,11 @@ class FormAutofillParent extends JSWindowActorParent {
const description = FormAutofillUtils.getAddressLabel(address.record);
const state = await lazy.FormAutofillPrompter.promptToSaveAddress(
browser,
"updateAddress",
address,
description
);
// Bug 1808176 : We should sync how we run the following code with Credit Card
let changedGUIDs = await lazy.gFormAutofillStorage.addresses.mergeToStorage(
address.record,
true
@ -582,7 +587,7 @@ class FormAutofillParent extends JSWindowActorParent {
const description = FormAutofillUtils.getAddressLabel(address.record);
const state = await lazy.FormAutofillPrompter.promptToSaveAddress(
browser,
"firstTimeUse",
address,
description
);
if (state !== "open-pref") {

View File

@ -147,6 +147,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
AutofillTelemetry: "resource://autofill/AutofillTelemetry.jsm",
FormAutofillNameUtils: "resource://autofill/FormAutofillNameUtils.jsm",
FormAutofillUtils: "resource://autofill/FormAutofillUtils.jsm",
PhoneNumber: "resource://autofill/phonenumberutils/PhoneNumber.jsm",
@ -280,6 +281,8 @@ class AutofillRecords {
this._schemaVersion = schemaVersion;
this._initialize();
Services.obs.addObserver(this, "formautofill-storage-changed");
}
_initialize() {
@ -295,6 +298,23 @@ class AutofillRecords {
});
}
observe(subject, topic, data) {
switch (topic) {
case "formautofill-storage-changed":
let collectionName = subject.wrappedJSObject.collectionName;
if (collectionName != this._collectionName) {
return;
}
const telemetryType =
subject.wrappedJSObject.collectionName == "creditCards"
? lazy.AutofillTelemetry.CREDIT_CARD
: lazy.AutofillTelemetry.ADDRESS;
const count = this._data.filter(entry => !entry.deleted).length;
lazy.AutofillTelemetry.recordAutofillProfileCount(telemetryType, count);
break;
}
}
/**
* Gets the schema version number.
*
@ -534,6 +554,7 @@ class AutofillRecords {
* Indicates which record to be notified.
*/
notifyUsed(guid) {
dump("notifyUsed:" + guid + "\n");
this.log.debug("notifyUsed:", guid);
let recordFound = this._findByGUID(guid);
@ -559,7 +580,14 @@ class AutofillRecords {
);
}
updateUseCountTelemetry() {}
updateUseCountTelemetry() {
const telemetryType =
this._collectionName == "creditCards"
? lazy.AutofillTelemetry.CREDIT_CARD
: lazy.AutofillTelemetry.ADDRESS;
let records = this._data.filter(r => !r.deleted);
lazy.AutofillTelemetry.recordNumberOfUse(telemetryType, records);
}
/**
* Removes the specified record. No error occurs if the record isn't found.
@ -1677,19 +1705,6 @@ class CreditCardsBase extends AutofillRecords {
VALID_CREDIT_CARD_COMPUTED_FIELDS,
CREDIT_CARD_SCHEMA_VERSION
);
Services.obs.addObserver(this, "formautofill-storage-changed");
}
observe(subject, topic, data) {
switch (topic) {
case "formautofill-storage-changed":
let count = this._data.filter(entry => !entry.deleted).length;
Services.telemetry.scalarSet(
"formautofill.creditCards.autofill_profiles_count",
count
);
break;
}
}
async computeFields(creditCard) {
@ -1948,17 +1963,6 @@ class CreditCardsBase extends AutofillRecords {
async mergeIfPossible(guid, creditCard) {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
updateUseCountTelemetry() {
let histogram = Services.telemetry.getHistogramById("CREDITCARD_NUM_USES");
histogram.clear();
let records = this._data.filter(r => !r.deleted);
for (let record of records) {
histogram.add(record.timesUsed);
}
}
}
class FormAutofillStorageBase {

View File

@ -1,292 +0,0 @@
/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["CreditCardTelemetry"];
const { FormAutofillUtils } = ChromeUtils.import(
"resource://autofill/FormAutofillUtils.jsm"
);
const { FIELD_STATES } = FormAutofillUtils;
const CreditCardTelemetry = {
// Mapping of field name used in formautofill code to the field name
// used in the telemetry.
CC_FORM_V2_SUPPORTED_FIELDS: {
"cc-name": "cc_name",
"cc-number": "cc_number",
"cc-type": "cc_type",
"cc-exp": "cc_exp",
"cc-exp-month": "cc_exp_month",
"cc-exp-year": "cc_exp_year",
},
/**
* Utility function to get an `extra` object of `cc_form_v2` event telemetry
* with a default value that applies to all keys.
*
* @param {string} value The default value
* @returns {object} The extra object
*/
_ccFormV2InitExtra(value) {
let extra = {};
for (const field of Object.values(this.CC_FORM_V2_SUPPORTED_FIELDS)) {
extra[field] = value;
}
return extra;
},
/**
* Utility function to set the value of the specified fieldName of `cc_form_v2`
* extra object.
*
* @param {object} extra The `extra` object to be set
* @param {string} key Field name, all supported field names are listed in
* `CC_FORM_V2_SUPPORTED_FIELDS`
* @param {string} value
*/
_ccFormV2SetExtra(extra, key, value) {
extra[this.CC_FORM_V2_SUPPORTED_FIELDS[key]] = value;
},
/**
* Utility function to record both `cc_form` and `cc_form_v2` events
*
* @param {string} method The method name.
* @param {string} flowId Flow id.
* @param {object} ccFormExtra The extra object passed to `cc_form` telemetry
* @param {object} ccFormV2Extra The extra object passed to `cc_form_v2` telemetry
*/
_recordCCFormEvent(method, flowId, ccFormExtra, ccFormV2Extra) {
Services.telemetry.recordEvent(
"creditcard",
method,
"cc_form",
flowId,
ccFormExtra
);
Services.telemetry.recordEvent(
"creditcard",
method,
"cc_form_v2",
flowId,
ccFormV2Extra
);
},
/**
* Called when a form is recognized as a credit card form.
*
* @param {string} flowId Flow id.
* @param {Array<object>} fieldDetails List of current field details
*/
recordFormDetected(flowId, fieldDetails) {
// Record which fields could be identified
let ccFormV2Extra = this._ccFormV2InitExtra("false");
let identified = new Set();
fieldDetails.forEach(detail => {
identified.add(detail.fieldName);
if (detail._reason == "autocomplete") {
this._ccFormV2SetExtra(ccFormV2Extra, detail.fieldName, "true");
} else {
// confidence exists only when a field is identified by fathom.
let confidence =
detail.confidence > 0 ? Math.floor(100 * detail.confidence) / 100 : 0;
this._ccFormV2SetExtra(
ccFormV2Extra,
detail.fieldName,
confidence.toString()
);
}
});
let ccFormExtra = {
cc_name_found: identified.has("cc-name") ? "true" : "false",
cc_number_found: identified.has("cc-number") ? "true" : "false",
cc_exp_found:
identified.has("cc-exp") ||
(identified.has("cc-exp-month") && identified.has("cc-exp-year"))
? "true"
: "false",
};
this._recordCCFormEvent("detected", flowId, ccFormExtra, ccFormV2Extra);
Services.telemetry.scalarAdd(
"formautofill.creditCards.detected_sections_count",
1
);
},
/**
* Called when the credit card autofill popup is shown.
*
* @param {string} flowId Flow id.
* @param {string} fieldName Field that triggers the event.
*/
recordPopupShown(flowId, fieldName) {
if (!flowId) {
return;
}
let ccFormExtra = null;
let ccFormV2Extra = { field_name: fieldName };
this._recordCCFormEvent("popup_shown", flowId, ccFormExtra, ccFormV2Extra);
},
/**
* Called when a credit card form is autofilled.
*
* @param {string} flowId Flow id.
* @param {Array<object>} fieldDetails List of current field details
* @param {object} profile The profile to be autofilled
*/
recordFormFilled(flowId, fieldDetails, profile) {
// Calculate values for telemetry
let ccFormExtra = {
cc_name: "unavailable",
cc_number: "unavailable",
cc_exp: "unavailable",
};
let ccFormV2Extra = this._ccFormV2InitExtra("unavailable");
for (let fieldDetail of fieldDetails) {
let element = fieldDetail.elementWeakRef.get();
let state = profile[fieldDetail.fieldName] ? "filled" : "not_filled";
if (
fieldDetail.state == FIELD_STATES.NORMAL &&
(HTMLSelectElement.isInstance(element) ||
(HTMLInputElement.isInstance(element) && element.value.length))
) {
state = "user_filled";
}
this._ccFormV2SetExtra(ccFormV2Extra, fieldDetail.fieldName, state);
switch (fieldDetail.fieldName) {
case "cc-name":
ccFormExtra.cc_name = state;
break;
case "cc-number":
ccFormExtra.cc_number = state;
break;
case "cc-exp":
case "cc-exp-month":
case "cc-exp-year":
ccFormExtra.cc_exp = state;
break;
}
}
this._recordCCFormEvent("filled", flowId, ccFormExtra, ccFormV2Extra);
},
/**
* Called when a credit card field is filled and then modifed by
* the user.
*
* @param {string} flowId Flow id.
* @param {string} fieldName Field that triggers the clear form event.
*/
recordFilledModified(flowId, fieldName) {
let ccFormExtra = { field_name: fieldName };
let ccFormV2Extra = { field_name: fieldName };
this._recordCCFormEvent(
"filled_modified",
flowId,
ccFormExtra,
ccFormV2Extra
);
},
/**
* Called when a credit card form is submitted
*
* @param {Objecy} records Credit card and address records filled in the form.
* @param {Array<HTMLElements>} elements Elements in the form
*/
recordFormSubmitted(records, elements) {
records.creditCard.forEach(record => {
let ccFormExtra = {
// Fields which have been filled manually.
fields_not_auto: "0",
// Fields which have been autofilled.
fields_auto: "0",
// Fields which have been autofilled and then modified.
fields_modified: "0",
};
let ccFormV2Extra = this._ccFormV2InitExtra("unavailable");
if (record.guid !== null) {
// If the `guid` is not null, it means we're editing an existing record.
// In that case, all fields in the record are autofilled, and fields in
// `untouchedFields` are unmodified.
let totalCount = elements.length;
let autofilledCount = Object.keys(record.record).length;
let unmodifiedCount = record.untouchedFields.length;
for (let fieldName of Object.keys(record.record)) {
if (record.untouchedFields?.includes(fieldName)) {
this._ccFormV2SetExtra(ccFormV2Extra, fieldName, "autofilled");
} else {
this._ccFormV2SetExtra(ccFormV2Extra, fieldName, "user_filled");
}
}
ccFormExtra.fields_not_auto = (totalCount - autofilledCount).toString();
ccFormExtra.fields_auto = autofilledCount.toString();
ccFormExtra.fields_modified = (
autofilledCount - unmodifiedCount
).toString();
} else {
// If the `guid` is null, we're filling a new form.
// In that case, all not-null fields are manually filled.
ccFormExtra.fields_not_auto = Array.from(elements)
.filter(element => !!element.value?.trim().length)
.length.toString();
Object.keys(record.record).forEach(fieldName =>
this._ccFormV2SetExtra(ccFormV2Extra, fieldName, "user_filled")
);
}
this._recordCCFormEvent(
"submitted",
record.flowId,
ccFormExtra,
ccFormV2Extra
);
});
if (records.creditCard.length) {
Services.telemetry.scalarAdd(
"formautofill.creditCards.submitted_sections_count",
records.creditCard.length
);
}
},
/**
* Called when a credit card form is cleared.
*
* @param {string} flowId Flow id.
* @param {string} fieldName Field that triggers the clear form event.
*/
recordFormCleared(flowId, fieldName) {
// Note that when a form is cleared, we also record `filled_modified` events
// for all the fields that have been cleared.
Services.telemetry.recordEvent(
"creditcard",
"cleared",
"cc_form_v2",
flowId,
{ field_name: fieldName }
);
},
};

View File

@ -14,6 +14,9 @@ var EXPORTED_SYMBOLS = ["FormAutofillPrompter"];
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const { AutofillTelemetry } = ChromeUtils.import(
"resource://autofill/AutofillTelemetry.jsm"
);
const { FormAutofill } = ChromeUtils.import(
"resource://autofill/FormAutofill.jsm"
);
@ -377,8 +380,15 @@ let FormAutofillPrompter = {
}
},
async promptToSaveAddress(browser, type, description) {
return this._showCCorAddressCaptureDoorhanger(browser, type, description);
async promptToSaveAddress(browser, address, description) {
const state = this._showCCorAddressCaptureDoorhanger(
browser,
address,
address.guid ? "updateAddress" : "firstTimeUse",
description
);
return state;
},
async promptToSaveCreditCard(browser, creditCard, storage) {
@ -389,30 +399,16 @@ let FormAutofillPrompter = {
let type = lazy.CreditCard.getType(number);
let maskedNumber = lazy.CreditCard.getMaskedNumber(number);
let description = `${maskedNumber}, ${name}`;
const telemetryObject = creditCard.guid
? "update_doorhanger"
: "capture_doorhanger";
Services.telemetry.recordEvent(
"creditcard",
"show",
telemetryObject,
creditCard.flowId
);
const state = await FormAutofillPrompter._showCCorAddressCaptureDoorhanger(
browser,
creditCard,
creditCard.guid ? "updateCreditCard" : "addCreditCard",
description,
type
);
if (state == "cancel") {
Services.telemetry.recordEvent(
"creditcard",
"cancel",
telemetryObject,
creditCard.flowId
);
return;
}
@ -421,12 +417,6 @@ let FormAutofillPrompter = {
"extensions.formautofill.creditCards.enabled",
false
);
Services.telemetry.recordEvent(
"creditcard",
"disable",
telemetryObject,
creditCard.flowId
);
return;
}
@ -438,12 +428,6 @@ let FormAutofillPrompter = {
let changedGUIDs = [];
if (creditCard.guid) {
if (state == "update") {
Services.telemetry.recordEvent(
"creditcard",
"update",
telemetryObject,
creditCard.flowId
);
await storage.creditCards.update(
creditCard.guid,
creditCard.record,
@ -451,12 +435,6 @@ let FormAutofillPrompter = {
);
changedGUIDs.push(creditCard.guid);
} else if ("create") {
Services.telemetry.recordEvent(
"creditcard",
"save",
telemetryObject,
creditCard.flowId
);
changedGUIDs.push(await storage.creditCards.add(creditCard.record));
}
} else {
@ -464,12 +442,6 @@ let FormAutofillPrompter = {
...(await storage.creditCards.mergeToStorage(creditCard.record))
);
if (!changedGUIDs.length) {
Services.telemetry.recordEvent(
"creditcard",
"save",
telemetryObject,
creditCard.flowId
);
changedGUIDs.push(await storage.creditCards.add(creditCard.record));
}
}
@ -483,18 +455,26 @@ let FormAutofillPrompter = {
/**
* Show different types of doorhanger by leveraging PopupNotifications.
*
* @param {XULElement} browser
* Target browser element for showing doorhanger.
* @param {string} type
* The type of the doorhanger. There will have first time use/update/credit card.
* @param {string} description
* The message that provides more information on doorhanger.
* @param {string} network
* The network type for credit card doorhangers.
* @returns {Promise}
Resolved with action type when action callback is triggered.
* @param {XULElement} browser Target browser element for showing doorhanger.
* @param {object} record The record being saved
* @param {string} type The type of the doorhanger. There will have first time use/update/credit card.
* @param {string} description The message that provides more information on doorhanger.
* @param {string} network The network type for credit card doorhangers.
* @returns {Promise} Resolved with action type when action callback is triggered.
*/
async _showCCorAddressCaptureDoorhanger(browser, type, description, network) {
async _showCCorAddressCaptureDoorhanger(
browser,
record,
type,
description,
network
) {
const telemetryType = ["updateCreditCard", "addCreditCard"].includes(type)
? AutofillTelemetry.CREDIT_CARD
: AutofillTelemetry.ADDRESS;
AutofillTelemetry.recordDoorhangerShown(telemetryType, record);
lazy.log.debug("show doorhanger with type:", type);
return new Promise(resolve => {
let {
@ -564,6 +544,9 @@ let FormAutofillPrompter = {
...this._createActions(mainAction, secondaryActions, resolve),
options
);
}).then(state => {
AutofillTelemetry.recordDoorhangerClicked(telemetryType, state, record);
return state;
});
},
};

View File

@ -718,8 +718,8 @@
"toolkit/components/formautofill/FormAutofillStorageBase.jsm",
"resource://autofill/FormAutofillSync.jsm":
"toolkit/components/formautofill/FormAutofillSync.jsm",
"resource://autofill/FormAutofillTelemetryUtils.jsm":
"toolkit/components/formautofill/FormAutofillTelemetryUtils.jsm",
"resource://autofill/Autofilltelemetry.jsm":
"toolkit/components/formautofill/Autofilltelemetry.jsm",
"resource://autofill/FormAutofillUtils.jsm":
"toolkit/components/formautofill/FormAutofillUtils.jsm",
"resource://autofill/ProfileAutoCompleteResult.jsm":