mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			430 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			430 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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 { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
 | 
						|
 | 
						|
const { FIELD_STATES } = FormAutofillUtils;
 | 
						|
 | 
						|
class AutofillTelemetryBase {
 | 
						|
  SUPPORTED_FIELDS = {};
 | 
						|
 | 
						|
  EVENT_CATEGORY = null;
 | 
						|
  EVENT_OBJECT_FORM_INTERACTION = null;
 | 
						|
 | 
						|
  #initFormEventExtra(value) {
 | 
						|
    let extra = {};
 | 
						|
    for (const field of Object.values(this.SUPPORTED_FIELDS)) {
 | 
						|
      extra[field] = value;
 | 
						|
    }
 | 
						|
    return extra;
 | 
						|
  }
 | 
						|
 | 
						|
  #setFormEventExtra(extra, key, value) {
 | 
						|
    if (!this.SUPPORTED_FIELDS[key]) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    extra[this.SUPPORTED_FIELDS[key]] = value;
 | 
						|
  }
 | 
						|
 | 
						|
  recordFormDetected(flowId, fieldDetails) {
 | 
						|
    let extra = this.#initFormEventExtra("false");
 | 
						|
 | 
						|
    let identified = new Set();
 | 
						|
    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 ? confidence.toString() : "0"
 | 
						|
        );
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    this.recordFormEvent("detected", flowId, extra);
 | 
						|
    try {
 | 
						|
      this.recordIframeLayoutDetection(flowId, fieldDetails);
 | 
						|
    } catch {}
 | 
						|
  }
 | 
						|
 | 
						|
  recordPopupShown(flowId, fieldDetails) {
 | 
						|
    const extra = { field_name: fieldDetails[0].fieldName };
 | 
						|
    this.recordFormEvent("popup_shown", flowId, extra);
 | 
						|
  }
 | 
						|
 | 
						|
  setUpFormFilledExtra(fieldDetails, data) {
 | 
						|
    // Calculate values for telemetry
 | 
						|
    const extra = this.#initFormEventExtra("unavailable");
 | 
						|
 | 
						|
    for (const fieldDetail of fieldDetails) {
 | 
						|
      // It is possible that we don't autofill a field because it is cross-origin.
 | 
						|
      // When that happens, the data will not include that element.
 | 
						|
      let { filledState, filledValue, isFilledOnFieldsUpdate } =
 | 
						|
        data.get(fieldDetail.elementId) ?? {};
 | 
						|
      switch (filledState) {
 | 
						|
        case FIELD_STATES.AUTO_FILLED:
 | 
						|
          filledState = isFilledOnFieldsUpdate
 | 
						|
            ? "filled_on_fields_update"
 | 
						|
            : "filled";
 | 
						|
          break;
 | 
						|
        case FIELD_STATES.NORMAL:
 | 
						|
        default:
 | 
						|
          filledState =
 | 
						|
            fieldDetail.localName == "select" || filledValue?.length
 | 
						|
              ? "user_filled"
 | 
						|
              : "not_filled";
 | 
						|
          break;
 | 
						|
      }
 | 
						|
      this.#setFormEventExtra(extra, fieldDetail.fieldName, filledState);
 | 
						|
    }
 | 
						|
    return extra;
 | 
						|
  }
 | 
						|
 | 
						|
  recordFormFilled(flowId, fieldDetails, data) {
 | 
						|
    const extra = this.setUpFormFilledExtra(fieldDetails, data);
 | 
						|
    this.recordFormEvent("filled", flowId, extra);
 | 
						|
  }
 | 
						|
 | 
						|
  recordFormFilledOnFieldsUpdate(flowId, fieldDetails, data) {
 | 
						|
    const extra = this.setUpFormFilledExtra(fieldDetails, data);
 | 
						|
    this.recordFormEvent("filled_on_fields_update", flowId, extra);
 | 
						|
  }
 | 
						|
 | 
						|
  recordFilledModified(flowId, fieldDetails) {
 | 
						|
    const extra = { field_name: fieldDetails[0].fieldName };
 | 
						|
    this.recordFormEvent("filled_modified", flowId, extra);
 | 
						|
  }
 | 
						|
 | 
						|
  recordFormSubmitted(flowId, fieldDetails, data) {
 | 
						|
    const extra = this.#initFormEventExtra("unavailable");
 | 
						|
 | 
						|
    for (const fieldDetail of fieldDetails) {
 | 
						|
      let { filledState, filledValue } = data.get(fieldDetail.elementId) ?? {};
 | 
						|
      switch (filledState) {
 | 
						|
        case FIELD_STATES.AUTO_FILLED:
 | 
						|
          filledState = "autofilled";
 | 
						|
          break;
 | 
						|
        case FIELD_STATES.NORMAL:
 | 
						|
        default:
 | 
						|
          filledState =
 | 
						|
            fieldDetail.localName == "select" || filledValue?.length
 | 
						|
              ? "user_filled"
 | 
						|
              : "not_filled";
 | 
						|
          break;
 | 
						|
      }
 | 
						|
      this.#setFormEventExtra(extra, fieldDetail.fieldName, filledState);
 | 
						|
    }
 | 
						|
 | 
						|
    this.recordFormEvent("submitted", flowId, extra);
 | 
						|
  }
 | 
						|
 | 
						|
  recordFormCleared(flowId, fieldDetails) {
 | 
						|
    const extra = { field_name: fieldDetails[0].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", flowId, extra);
 | 
						|
  }
 | 
						|
 | 
						|
  recordFormEvent(_method, _flowId, _extra) {
 | 
						|
    throw new Error("Not implemented.");
 | 
						|
  }
 | 
						|
 | 
						|
  recordFormInteractionEvent(method, flowId, fieldDetails, data) {
 | 
						|
    if (!this.EVENT_OBJECT_FORM_INTERACTION) {
 | 
						|
      return undefined;
 | 
						|
    }
 | 
						|
    switch (method) {
 | 
						|
      case "detected":
 | 
						|
        return this.recordFormDetected(flowId, fieldDetails);
 | 
						|
      case "popup_shown":
 | 
						|
        return this.recordPopupShown(flowId, fieldDetails);
 | 
						|
      case "filled":
 | 
						|
        return this.recordFormFilled(flowId, fieldDetails, data);
 | 
						|
      case "filled_on_fields_update":
 | 
						|
        return this.recordFormFilledOnFieldsUpdate(flowId, fieldDetails, data);
 | 
						|
      case "filled_modified":
 | 
						|
        return this.recordFilledModified(flowId, fieldDetails);
 | 
						|
      case "submitted":
 | 
						|
        return this.recordFormSubmitted(flowId, fieldDetails, data);
 | 
						|
      case "cleared":
 | 
						|
        return this.recordFormCleared(flowId, fieldDetails);
 | 
						|
    }
 | 
						|
    return undefined;
 | 
						|
  }
 | 
						|
 | 
						|
  recordDoorhangerEvent(method, object, flowId) {
 | 
						|
    const eventName = `${method}_${object}`.replace(/(_[a-z])/g, c =>
 | 
						|
      c[1].toUpperCase()
 | 
						|
    );
 | 
						|
    Glean[this.EVENT_CATEGORY][eventName]?.record({ value: flowId });
 | 
						|
  }
 | 
						|
 | 
						|
  recordManageEvent(method) {
 | 
						|
    const eventName =
 | 
						|
      method.replace(/(_[a-z])/g, c => c[1].toUpperCase()) + "Manage";
 | 
						|
    Glean[this.EVENT_CATEGORY][eventName]?.record();
 | 
						|
  }
 | 
						|
 | 
						|
  recordAutofillProfileCount(_count) {
 | 
						|
    throw new Error("Not implemented.");
 | 
						|
  }
 | 
						|
 | 
						|
  recordIframeLayoutDetection(flowId, fieldDetails) {
 | 
						|
    const fieldsInMainFrame = [];
 | 
						|
    const fieldsInIframe = [];
 | 
						|
    const fieldsInSandboxedIframe = [];
 | 
						|
    const fieldsInCrossOrignIframe = [];
 | 
						|
 | 
						|
    const iframes = new Set();
 | 
						|
    for (const fieldDetail of fieldDetails) {
 | 
						|
      const bc = BrowsingContext.get(fieldDetail.browsingContextId);
 | 
						|
      if (bc.top == bc) {
 | 
						|
        fieldsInMainFrame.push(fieldDetail);
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      iframes.add(bc);
 | 
						|
      fieldsInIframe.push(fieldDetail);
 | 
						|
      if (bc.sandboxFlags != 0) {
 | 
						|
        fieldsInSandboxedIframe.push(fieldDetail);
 | 
						|
      }
 | 
						|
 | 
						|
      if (!FormAutofillUtils.isBCSameOriginWithTop(bc)) {
 | 
						|
        fieldsInCrossOrignIframe.push(fieldDetail);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const extra = {
 | 
						|
      category: this.EVENT_CATEGORY,
 | 
						|
      flow_id: flowId,
 | 
						|
      iframe_count: iframes.size,
 | 
						|
      main_frame: fieldsInMainFrame.map(f => f.fieldName).toString(),
 | 
						|
      iframe: fieldsInIframe.map(f => f.fieldName).toString(),
 | 
						|
      cross_origin: fieldsInCrossOrignIframe.map(f => f.fieldName).toString(),
 | 
						|
      sandboxed: fieldsInSandboxedIframe.map(f => f.fieldName).toString(),
 | 
						|
    };
 | 
						|
 | 
						|
    Glean.formautofill.iframeLayoutDetection.record(extra);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export class AddressTelemetry extends AutofillTelemetryBase {
 | 
						|
  EVENT_CATEGORY = "address";
 | 
						|
  EVENT_OBJECT_FORM_INTERACTION = "AddressForm";
 | 
						|
  EVENT_OBJECT_FORM_INTERACTION_EXT = "AddressFormExt";
 | 
						|
 | 
						|
  // Fields that are recorded in `address_form` and `address_form_ext` telemetry
 | 
						|
  SUPPORTED_FIELDS = {
 | 
						|
    "street-address": "street_address",
 | 
						|
    "address-line1": "address_line1",
 | 
						|
    "address-line2": "address_line2",
 | 
						|
    "address-line3": "address_line3",
 | 
						|
    "address-level1": "address_level1",
 | 
						|
    "address-level2": "address_level2",
 | 
						|
    "postal-code": "postal_code",
 | 
						|
    country: "country",
 | 
						|
    name: "name",
 | 
						|
    "given-name": "given_name",
 | 
						|
    "additional-name": "additional_name",
 | 
						|
    "family-name": "family_name",
 | 
						|
    email: "email",
 | 
						|
    organization: "organization",
 | 
						|
    tel: "tel",
 | 
						|
  };
 | 
						|
 | 
						|
  // Fields that are recorded in `address_form` event telemetry extra_keys
 | 
						|
  static SUPPORTED_FIELDS_IN_FORM = [
 | 
						|
    "street_address",
 | 
						|
    "address_line1",
 | 
						|
    "address_line2",
 | 
						|
    "address_line3",
 | 
						|
    "address_level2",
 | 
						|
    "address_level1",
 | 
						|
    "postal_code",
 | 
						|
    "country",
 | 
						|
  ];
 | 
						|
 | 
						|
  // Fields that are recorded in `address_form_ext` event telemetry extra_keys
 | 
						|
  static SUPPORTED_FIELDS_IN_FORM_EXT = [
 | 
						|
    "name",
 | 
						|
    "given_name",
 | 
						|
    "additional_name",
 | 
						|
    "family_name",
 | 
						|
    "email",
 | 
						|
    "organization",
 | 
						|
    "tel",
 | 
						|
  ];
 | 
						|
 | 
						|
  recordFormEvent(method, flowId, extra) {
 | 
						|
    let extExtra = {};
 | 
						|
    if (["detected", "filled", "submitted"].includes(method)) {
 | 
						|
      for (const [key, value] of Object.entries(extra)) {
 | 
						|
        if (AddressTelemetry.SUPPORTED_FIELDS_IN_FORM_EXT.includes(key)) {
 | 
						|
          extExtra[key] = value;
 | 
						|
          delete extra[key];
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const eventMethod = method.replace(/(_[a-z])/g, c => c[1].toUpperCase());
 | 
						|
    Glean.address[eventMethod + this.EVENT_OBJECT_FORM_INTERACTION]?.record({
 | 
						|
      value: flowId,
 | 
						|
      ...extra,
 | 
						|
    });
 | 
						|
 | 
						|
    if (Object.keys(extExtra).length) {
 | 
						|
      Glean.address[
 | 
						|
        eventMethod + this.EVENT_OBJECT_FORM_INTERACTION_EXT
 | 
						|
      ]?.record({ value: flowId, ...extExtra });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  recordAutofillProfileCount(count) {
 | 
						|
    Glean.formautofillAddresses.autofillProfilesCount.set(count);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class CreditCardTelemetry extends AutofillTelemetryBase {
 | 
						|
  EVENT_CATEGORY = "creditcard";
 | 
						|
  EVENT_OBJECT_FORM_INTERACTION = "CcFormV2";
 | 
						|
 | 
						|
  // 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",
 | 
						|
  };
 | 
						|
  recordFormEvent(method, flowId, aExtra) {
 | 
						|
    // Don't modify the passed-in aExtra as it's reused.
 | 
						|
    const extra = Object.assign({ value: flowId }, aExtra);
 | 
						|
    const eventMethod = method.replace(/(_[a-z])/g, c => c[1].toUpperCase());
 | 
						|
    Glean.creditcard[eventMethod + this.EVENT_OBJECT_FORM_INTERACTION]?.record(
 | 
						|
      extra
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  recordFormDetected(flowId, fieldDetails) {
 | 
						|
    super.recordFormDetected(flowId, fieldDetails);
 | 
						|
    this.recordCcNumberFieldsCount(fieldDetails);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Collect the amount of consecutive cc number fields to help decide
 | 
						|
   * whether to support filling other field counts besides 1 and 4 fields
 | 
						|
   */
 | 
						|
  recordCcNumberFieldsCount(fieldDetails) {
 | 
						|
    const recordCount = count => {
 | 
						|
      const label = "cc_number_fields_" + (count > 4 ? "other" : count);
 | 
						|
      Glean.creditcard.detectedCcNumberFieldsCount[label].add(1);
 | 
						|
    };
 | 
						|
 | 
						|
    let consecutiveCcNumberCount = 0;
 | 
						|
    for (const { fieldName, reason } of fieldDetails) {
 | 
						|
      if (fieldName == "cc-number" && reason == "autocomplete") {
 | 
						|
        consecutiveCcNumberCount++;
 | 
						|
      } else if (consecutiveCcNumberCount) {
 | 
						|
        recordCount(consecutiveCcNumberCount);
 | 
						|
        consecutiveCcNumberCount = 0;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (consecutiveCcNumberCount) {
 | 
						|
      recordCount(consecutiveCcNumberCount);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  recordAutofillProfileCount(count) {
 | 
						|
    Glean.formautofillCreditcards.autofillProfilesCount.set(count);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export 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 #getTelemetryByFieldDetail(fieldDetail) {
 | 
						|
    return FormAutofillUtils.isAddressField(fieldDetail.fieldName)
 | 
						|
      ? this.#addressTelemetry
 | 
						|
      : this.#creditCardTelemetry;
 | 
						|
  }
 | 
						|
 | 
						|
  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, object, flowId) {
 | 
						|
    const telemetry = this.#getTelemetryByType(type);
 | 
						|
    telemetry.recordDoorhangerEvent("show", object, flowId);
 | 
						|
  }
 | 
						|
 | 
						|
  static recordDoorhangerClicked(type, method, object, flowId) {
 | 
						|
    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;
 | 
						|
      case "learn-more":
 | 
						|
        method = "learn_more";
 | 
						|
        break;
 | 
						|
    }
 | 
						|
 | 
						|
    telemetry.recordDoorhangerEvent(method, object, flowId);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Utility functions for form event (defined in Events.yaml)
 | 
						|
   *
 | 
						|
   * Category: address or creditcard
 | 
						|
   * Event name: cc_form_v2, or address_form
 | 
						|
   */
 | 
						|
 | 
						|
  static recordFormInteractionEvent(method, flowId, fieldDetails, data) {
 | 
						|
    const telemetry = this.#getTelemetryByFieldDetail(fieldDetails[0]);
 | 
						|
    telemetry.recordFormInteractionEvent(method, flowId, fieldDetails, data);
 | 
						|
  }
 | 
						|
 | 
						|
  static recordManageEvent(type, method) {
 | 
						|
    const telemetry = this.#getTelemetryByType(type);
 | 
						|
    telemetry.recordManageEvent(method);
 | 
						|
  }
 | 
						|
 | 
						|
  static recordAutofillProfileCount(type, count) {
 | 
						|
    const telemetry = this.#getTelemetryByType(type);
 | 
						|
    telemetry.recordAutofillProfileCount(count);
 | 
						|
  }
 | 
						|
 | 
						|
  static recordFormSubmissionHeuristicCount(label) {
 | 
						|
    Glean.formautofill.formSubmissionHeuristic[label].add(1);
 | 
						|
  }
 | 
						|
}
 |