packages/fxa-content-server/app/scripts/lib/glean/index.ts (439 lines of code) (raw):

/* 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 Glean from '@mozilla/glean/web'; import { userIdSha256, userId } from './account'; import * as cachedLogin from './cachedLogin'; import * as cadApproveDevice from './cadApproveDevice'; import * as cadFirefox from './cadFirefox'; import * as cadMobilePair from './cadMobilePair'; import * as cadRedirectMobile from './cadRedirectMobile'; import * as cadMobilePairUseApp from './cadMobilePairUseApp'; import * as cad from './cad'; import * as cadRedirectDesktop from './cadRedirectDesktop'; import * as email from './email'; import * as event from './event'; import * as login from './login'; import * as passwordReset from './passwordReset'; import { accountsEvents } from './pings'; import * as reg from './reg'; import { oauthClientId, service } from './relyingParty'; import { deviceType, entrypoint, flowId } from './session'; import * as thirdPartyAuth from './thirdPartyAuth'; import * as thirdPartyAuthSetPassword from './thirdPartyAuthSetPassword'; import * as utm from './utm'; import * as entrypointQuery from './entrypoint'; export type GleanMetricsConfig = { enabled: boolean; applicationId: string; uploadEnabled: boolean; appDisplayVersion: string; channel: string; serverEndpoint: string; logPings: boolean; debugViewTag: string; }; export type GleanMetricsContext = { metrics: any; relier: any; user: any; userAgent: any; }; type SubmitPingFn = () => Promise<void>; const eventPropertyNames = ['reason'] as const; type PropertyNameT = typeof eventPropertyNames; type PropertyName = PropertyNameT[number]; type EventProperties = { [k in PropertyName]?: string; }; let EXEC_MUTEX = false; const lambdas: SubmitPingFn[] = []; const submitPing = async (fn: SubmitPingFn) => { lambdas.push(fn); if (EXEC_MUTEX) return; EXEC_MUTEX = true; let f: SubmitPingFn | undefined; while ((f = lambdas.shift())) { await f(); } EXEC_MUTEX = false; }; const encoder = new TextEncoder(); let gleanEnabled = false; let gleanMetricsContext; const hashUid = async (uid) => { const data = encoder.encode(uid); const hash = await crypto.subtle.digest('SHA-256', data); const uint8View = new Uint8Array(hash); const hex = uint8View.reduce( (str, byte) => str + ('00' + byte.toString(16)).slice(-2), '' ); return hex; }; const initMetrics = async () => { const account = gleanMetricsContext.user.getSignedInAccount(); // the "signed in" account could just be the most recently used account from // local storage; the user might not have proved that they know the password // of the account if (account.get('sessionToken')) { const accountUid = account.get('uid'); userId.set(accountUid); const hashedUid = await hashUid(accountUid); userIdSha256.set(hashedUid); } else { userId.set(''); userIdSha256.set(''); } const flowEventMetadata = gleanMetricsContext.metrics.getFlowEventMetadata(); oauthClientId.set(gleanMetricsContext.relier.get('clientId') || ''); service.set(gleanMetricsContext.relier.get('service') || ''); deviceType.set(gleanMetricsContext.userAgent.genericDeviceType() || ''); entrypoint.set(flowEventMetadata.entrypoint || ''); flowId.set(flowEventMetadata.flowId || ''); utm.campaign.set(flowEventMetadata.utmCampaign || ''); utm.content.set(flowEventMetadata.utmContent || ''); utm.medium.set(flowEventMetadata.utmMedium || ''); utm.source.set(flowEventMetadata.utmSource || ''); utm.term.set(flowEventMetadata.utmTerm || ''); entrypointQuery.experiment.set(flowEventMetadata.entrypointExperiment || ''); entrypointQuery.variation.set(flowEventMetadata.entrypointVariation || ''); }; const populateMetrics = async (properties: EventProperties = {}) => { await initMetrics(); for (const n of eventPropertyNames) { event[n].set(properties[n] || ''); } }; const recordEventMetric = (eventName: string, properties: EventProperties) => { switch (eventName) { case 'email_first_view': email.firstView.record(); break; case 'email_first_apple_oauth_start': email.firstAppleOauthStart.record(); break; case 'email_first_google_oauth_start': email.firstGoogleOauthStart.record(); break; case 'reg_cwts_engage': reg.cwtsEngage.record(); break; case 'reg_marketing_engage': reg.marketingEngage.record(); break; case 'reg_view': reg.view.record(); break; case 'reg_engage': reg.engage.record({ reason: properties['reason'] || '', }); break; case 'reg_submit': reg.submit.record(); break; case 'reg_submit_success': reg.submitSuccess.record(); break; case 'reg_signup_code_view': reg.signupCodeView.record(); break; case 'reg_signup_code_submit': reg.signupCodeSubmit.record(); break; case 'reg_success_view': reg.successView.record(); break; case 'login_view': login.view.record(); break; case 'login_forgot_pwd_submit': login.forgotPwdSubmit.record(); break; case 'login_submit': login.submit.record(); break; case 'login_submit_success': login.submitSuccess.record(); break; case 'login_submit_frontend_error': login.submitFrontendError.record({ reason: properties['reason'] || '', }); break; case 'login_diff_account_link_click': login.diffAccountLinkClick.record(); break; case 'cached_login_forgot_pwd_submit': cachedLogin.forgotPwdSubmit.record(); break; case 'cached_login_view': cachedLogin.view.record(); break; case 'cached_login_submit': cachedLogin.submit.record(); break; case 'cached_login_success_view': cachedLogin.successView.record(); break; case 'login_email_confirmation_view': login.emailConfirmationView.record(); break; case 'login_email_confirmation_submit': login.emailConfirmationSubmit.record(); break; case 'login_email_confirmation_success_view': login.emailConfirmationSuccessView.record(); break; case 'login_totp_form_view': login.totpFormView.record(); break; case 'login_totp_code_submit': login.totpCodeSubmit.record(); break; case 'login_totp_code_success_view': login.totpCodeSuccessView.record(); break; case 'password_reset_create_new_submit': passwordReset.createNewSubmit.record(); break; case 'password_reset_create_new_success_view': passwordReset.createNewSuccessView.record(); break; case 'password_reset_create_new_view': passwordReset.createNewView.record(); break; case 'password_reset_recovery_key_create_new_submit': passwordReset.recoveryKeyCreateNewSubmit.record(); break; case 'password_reset_recovery_key_create_new_view': passwordReset.recoveryKeyCreateNewView.record(); break; case 'password_reset_recovery_key_create_success_view': passwordReset.recoveryKeyCreateSuccessView.record(); break; case 'password_reset_recovery_key_submit': passwordReset.recoveryKeySubmit.record(); break; case 'password_reset_recovery_key_view': passwordReset.recoveryKeyView.record(); break; case 'password_reset_submit': passwordReset.submit.record(); break; case 'password_reset_view': passwordReset.view.record(); break; case 'cad_firefox_view': cadFirefox.view.record(); break; case 'cad_firefox_choice_view': cadFirefox.choiceView.record(); break; case 'cad_firefox_choice_engage': cadFirefox.choiceEngage.record({ reason: properties['reason'] || '', }); break; case 'cad_firefox_choice_submit': cadFirefox.choiceSubmit.record({ reason: properties['reason'] || '', }); break; case 'cad_firefox_choice_notnow_submit': cadFirefox.choiceNotnowSubmit.record(); break; case 'cad_firefox_notnow_submit': cadFirefox.notnowSubmit.record(); break; case 'cad_firefox_sync_device_submit': cadFirefox.syncDeviceSubmit.record(); break; case 'cad_approve_device_view': cadApproveDevice.view.record(); break; case 'cad_approve_device_submit': cadApproveDevice.submit.record(); break; case 'cad_mobile_pair_view': cadMobilePair.view.record(); break; case 'cad_mobile_pair_submit': cadMobilePair.submit.record(); break; case 'cad_mobile_pair_use_app_view': cadMobilePairUseApp.view.record(); break; case 'cad_view': cad.view.record(); break; case 'cad_submit': cad.submit.record(); break; case 'cad_redirect_mobile_view': cadRedirectMobile.view.record(); break; case 'cad_redirect_desktop_default_view': cadRedirectDesktop.defaultView.record(); break; case 'cad_redirect_desktop_view': cadRedirectDesktop.view.record(); break; case 'cad_redirect_desktop_download': cadRedirectDesktop.download.record(); break; case 'cad_startbrowsing_submit': cad.startbrowsingSubmit.record(); break; case 'third_party_auth_set_password_view': thirdPartyAuthSetPassword.view.record(); break; case 'third_party_auth_set_password_engage': thirdPartyAuthSetPassword.engage.record(); break; case 'third_party_auth_set_password_submit': thirdPartyAuthSetPassword.submit.record(); break; case 'third_party_auth_set_password_success': thirdPartyAuthSetPassword.success.record(); break; case 'third_party_auth_google_deeplink': thirdPartyAuth.googleDeeplink.record(); break; case 'third_party_auth_apple_deeplink': thirdPartyAuth.appleDeeplink.record(); break; } }; const createEventFn = (eventName) => async (properties: EventProperties = {}) => { if (!gleanEnabled) { return; } const fn = async () => { event.name.set(eventName); await populateMetrics(properties); // recording the event metric triggers the event ping because Glean is initialized with `maxEvents: 1` recordEventMetric(eventName, properties); accountsEvents.submit(); }; submitPing(fn); }; export const GleanMetrics = { initialize: async ( config: GleanMetricsConfig, context: GleanMetricsContext ) => { // https://bugzilla.mozilla.org/show_bug.cgi?id=1859629 // Starting with glean.js v2, accessing localStorage during // initialization could cause an error try { if (config.enabled) { Glean.initialize(config.applicationId, config.uploadEnabled, { appDisplayVersion: config.appDisplayVersion, channel: config.channel, serverEndpoint: config.serverEndpoint, // Glean does not offer direct control over when metrics are uploaded; // this ensures that events are uploaded. maxEvents: 1, enableAutoPageLoadEvents: true, enableAutoElementClickEvents: true, }); Glean.setLogPings(config.logPings); if (config.debugViewTag) { Glean.setDebugViewTag(config.debugViewTag); } gleanMetricsContext = context; } GleanMetrics.setEnabled(config.enabled); await initMetrics(); } catch (_) { // set some states so we won't try to do anything with glean.js later config.enabled = false; gleanEnabled = false; } }, setEnabled: (enabled) => { gleanEnabled = enabled; Glean.setUploadEnabled(gleanEnabled); }, /** * The ping calls are awaited internally for ease of use and that works in * most cases. But in the scenario where we want to wait for the pings to * finish before we unload the page, we are doing so, crudely, here. Do not * emit more pings after calling this function. */ isDone: () => new Promise((resolve) => { const checkForEmptyFnList = () => { if (lambdas.length === 0) { setTimeout(resolve, 5); } else { setTimeout(checkForEmptyFnList, lambdas.length * 5); } }; checkForEmptyFnList(); }), emailFirst: { view: createEventFn('email_first_view'), appleOauthStart: createEventFn('email_first_apple_oauth_start'), googleOauthStart: createEventFn('email_first_google_oauth_start'), }, registration: { view: createEventFn('reg_view'), submit: createEventFn('reg_submit'), success: createEventFn('reg_submit_success'), }, signupConfirmation: { view: createEventFn('reg_signup_code_view'), submit: createEventFn('reg_signup_code_submit'), }, login: { view: createEventFn('login_view'), submit: createEventFn('login_submit'), success: createEventFn('login_submit_success'), error: createEventFn('login_submit_frontend_error'), diffAccountLinkClick: createEventFn('login_diff_account_link_click'), }, cachedLogin: { view: createEventFn('cached_login_view'), submit: createEventFn('cached_login_submit'), success: createEventFn('cached_login_success_view'), }, loginConfirmation: { view: createEventFn('login_email_confirmation_view'), submit: createEventFn('login_email_confirmation_submit'), }, totpForm: { view: createEventFn('login_totp_form_view'), submit: createEventFn('login_totp_code_submit'), success: createEventFn('login_totp_code_success_view'), }, cad: { view: createEventFn('cad_view'), submit: createEventFn('cad_submit'), startbrowsingSubmit: createEventFn('cad_startbrowsing_submit'), }, cadFirefox: { view: createEventFn('cad_firefox_view'), choiceView: createEventFn('cad_firefox_choice_view'), choiceEngage: createEventFn('cad_firefox_choice_engage'), choiceSubmit: createEventFn('cad_firefox_choice_submit'), choiceNotnowSubmit: createEventFn('cad_firefox_choice_notnow_submit'), syncDeviceSubmit: createEventFn('cad_firefox_sync_device_submit'), notnowSubmit: createEventFn('cad_firefox_notnow_submit'), }, cadMobilePair: { view: createEventFn('cad_mobile_pair_view'), submit: createEventFn('cad_mobile_pair_submit'), }, cadMobilePairUseAppView: { view: createEventFn('cad_mobile_pair_use_app_view'), }, cadApproveDevice: { view: createEventFn('cad_approve_device_view'), submit: createEventFn('cad_approve_device_submit'), }, cadRedirectDesktop: { defaultView: createEventFn('cad_redirect_desktop_default_view'), view: createEventFn('cad_redirect_desktop_view'), download: createEventFn('cad_redirect_desktop_download'), }, cadRedirectMobile: { view: createEventFn('cad_redirect_mobile_view'), }, setPasswordThirdPartyAuth: { view: createEventFn('third_party_auth_set_password_view'), engage: createEventFn('third_party_auth_set_password_engage'), submit: createEventFn('third_party_auth_set_password_submit'), success: createEventFn('third_party_auth_set_password_success'), }, thirdPartyAuth: { googleDeeplink: createEventFn('third_party_auth_google_deeplink'), appleDeeplink: createEventFn('third_party_auth_apple_deeplink'), }, }; export default GleanMetrics;