diff --git a/src/frontend/src/components/authenticateBox/index.ts b/src/frontend/src/components/authenticateBox/index.ts index 1635e6de9b..e55d0ce3a5 100644 --- a/src/frontend/src/components/authenticateBox/index.ts +++ b/src/frontend/src/components/authenticateBox/index.ts @@ -23,7 +23,6 @@ import { } from "$src/flows/register"; import { I18n } from "$src/i18n"; import { getAnchors, setAnchorUsed } from "$src/storage"; -import { analytics } from "$src/utils/analytics/analytics"; import { AlreadyInProgress, ApiError, @@ -266,21 +265,17 @@ export const authenticateBoxFlow = async ({ | FlowError | { tag: "canceled" } > => { - analytics.event("registration-start"); const result2 = await registerFlow(registerFlowOpts); if (result2 === "canceled") { - analytics.event("registration-canceled"); return { tag: "canceled" } as const; } if (result2.kind !== "loginSuccess") { - analytics.event("registration-error"); return result2; } result2 satisfies LoginSuccess; - analytics.event("registration-success"); return { newAnchor: true, ...result2, diff --git a/src/frontend/src/flows/register/finish.ts b/src/frontend/src/flows/register/finish.ts index 1de0463d4b..170e89f2e6 100644 --- a/src/frontend/src/flows/register/finish.ts +++ b/src/frontend/src/flows/register/finish.ts @@ -3,6 +3,10 @@ import { checkmarkIcon, copyIcon } from "$src/components/icons"; import { identityCard } from "$src/components/identityCard"; import { mainWindow } from "$src/components/mainWindow"; import { toast } from "$src/components/toast"; +import { + RegistrationEvents, + registrationFunnel, +} from "$src/utils/analytics/registrationFunnel"; import { TemplateElement, mount, @@ -58,6 +62,9 @@ export const displayUserNumberTemplate = ({ @click=${async () => { try { await navigator.clipboard.writeText(userNumber.toString()); + registrationFunnel.trigger( + RegistrationEvents.CopyNewIdentityNumber, + ); withRef(userNumberCopy, (elem) => { elem.classList.add("is-copied"); }); diff --git a/src/frontend/src/flows/register/index.ts b/src/frontend/src/flows/register/index.ts index 4e83b12dfb..3d1b765c8a 100644 --- a/src/frontend/src/flows/register/index.ts +++ b/src/frontend/src/flows/register/index.ts @@ -10,7 +10,6 @@ import { idbStorePinIdentityMaterial } from "$src/flows/pin/idb"; import { registerDisabled } from "$src/flows/registerDisabled"; import { I18n } from "$src/i18n"; import { setAnchorUsed } from "$src/storage"; -import { analytics } from "$src/utils/analytics/analytics"; import { passkeyAuthnMethodData, pinAuthnMethodData, @@ -43,6 +42,10 @@ import { setPinFlow } from "../pin/setPin"; import { precomputeFirst, promptCaptcha } from "./captcha"; import { displayUserNumberWarmup } from "./finish"; import { savePasskeyPinOrOpenID } from "./passkey"; +import { + RegistrationEvents, + registrationFunnel, +} from "$src/utils/analytics/registrationFunnel"; /** Registration (identity creation) flow for new users */ export const registerFlow = async ({ @@ -130,6 +133,7 @@ export const registerFlow = async ({ // We register the device's origin in the current domain. // If we want to change it, we need to change this line. const deviceOrigin = window.location.origin; + registrationFunnel.trigger(RegistrationEvents.Trigger); const savePasskeyResult = await savePasskeyPinOrOpenID({ pinAllowed: await pinAllowed(), googleAllowed, @@ -146,7 +150,6 @@ export const registerFlow = async ({ } pinResult.tag satisfies "ok"; - analytics.event("registration-pin"); // XXX: this withLoader could be replaced with one that indicates what's happening (like the // "Hang tight, ..." spinner) @@ -179,7 +182,6 @@ export const registerFlow = async ({ const openIdResult = await openidIdentityRegistrationFinish(); if (openIdResult.kind === "loginSuccess") { - analytics.event("registration-openid"); return { ...openIdResult, authnMethod: "google", @@ -194,7 +196,6 @@ export const registerFlow = async ({ return "canceled"; } - analytics.event("registration-passkey"); const alias = await inferPasskeyAlias({ authenticatorType: identity.getAuthenticatorAttachment(), userAgent: navigator.userAgent, @@ -225,7 +226,6 @@ export const registerFlow = async ({ result_.kind === "loginSuccess" && result_.authnMethod === "google" ) { - analytics.event("registration-final-success"); // for now we switch to passkey here so dapps don't know it's google return { ...result_, authnMethod: "passkey" as const }; } else if ("kind" in result_ && result_.kind === "loginSuccess") { @@ -233,7 +233,6 @@ export const registerFlow = async ({ return { ...result_, authnMethod: "passkey" as const }; } else if ("kind" in result_) { // if openid returned some error - analytics.event("registration-final-error"); return result_; } @@ -265,12 +264,11 @@ export const registerFlow = async ({ ); if (result.kind !== "loginSuccess") { - analytics.event("registration-final-error"); return result; } result.kind satisfies "loginSuccess"; - analytics.event("registration-final-success"); + registrationFunnel.trigger(RegistrationEvents.Created); const userNumber = result.userNumber; await finalizeIdentity?.(userNumber); // We don't want to nudge the user with the recovery phrase warning page @@ -290,6 +288,7 @@ export const registerFlow = async ({ userNumber, marketingIntroSlot: finishSlot, }); + registrationFunnel.trigger(RegistrationEvents.Success); return { ...result, authnMethod }; }; @@ -535,23 +534,20 @@ async function captchaIfNecessary( > { const startResult = await flowStart(); if (startResult.kind !== "registrationFlowStepSuccess") { - analytics.event("registration-start-error"); return startResult; } startResult satisfies RegistrationFlowStepSuccess; if (startResult.nextStep.step === "checkCaptcha") { - analytics.event("registration-captcha"); + registrationFunnel.trigger(RegistrationEvents.CaptchaCheck); const captchaResult = await promptCaptcha({ captcha_png_base64: startResult.nextStep.captcha_png_base64, checkCaptcha, }); if (captchaResult === "canceled") { - analytics.event("registration-captcha-cancelled"); return "canceled"; } if (captchaResult.kind !== "registrationFlowStepSuccess") { - analytics.event("registration-captcha-error"); return captchaResult; } captchaResult satisfies RegistrationFlowStepSuccess; diff --git a/src/frontend/src/flows/register/passkey.ts b/src/frontend/src/flows/register/passkey.ts index bf476bd3e5..7818684f19 100644 --- a/src/frontend/src/flows/register/passkey.ts +++ b/src/frontend/src/flows/register/passkey.ts @@ -12,9 +12,11 @@ import { } from "$src/utils/webAuthnErrorUtils"; import { nonNullish } from "@dfinity/utils"; import { html, TemplateResult } from "lit-html"; - -import { analytics } from "$src/utils/analytics/analytics"; import copyJson from "./passkey.json"; +import { + RegistrationEvents, + registrationFunnel, +} from "$src/utils/analytics/registrationFunnel"; /* Anchor construction component (for creating WebAuthn credentials) */ @@ -129,7 +131,7 @@ export const savePasskeyPinOrOpenID = async ({ cancel: () => resolve("canceled"), scrollToTop: true, constructPasskey: async () => { - analytics.event("construct-passkey"); + registrationFunnel.trigger(RegistrationEvents.WebauthnStart); try { const rpId = origin === window.location.origin @@ -138,10 +140,8 @@ export const savePasskeyPinOrOpenID = async ({ const identity = await withLoader(() => constructIdentity({ rpId }), ); - analytics.event("construct-passkey-success"); resolve(identity); } catch (e) { - analytics.event("construct-passkey-error"); toast.error(errorMessage(e)); } }, diff --git a/src/frontend/src/index.ts b/src/frontend/src/index.ts index 559b3cd7c8..6e57477809 100644 --- a/src/frontend/src/index.ts +++ b/src/frontend/src/index.ts @@ -10,6 +10,7 @@ import { authFlowManage, renderManageWarmup } from "./flows/manage"; import { createSpa } from "./spa"; import { getAddDeviceAnchor } from "./utils/addDeviceLink"; import { analytics, initAnalytics } from "./utils/analytics/analytics"; +import { registrationFunnel } from "./utils/analytics/registrationFunnel"; void createSpa(async (connection) => { initAnalytics(connection.canisterConfig.analytics_config[0]?.[0]); @@ -50,6 +51,7 @@ void createSpa(async (connection) => { // Simple, #-based routing if (url.hash === "#authorize") { analytics.event("page-authorize"); + registrationFunnel.init(); // User was brought here by a dapp for authorization return authFlowAuthorize(connection); } else if (url.hash === WEBAUTHN_IFRAME_PATH) { @@ -58,6 +60,7 @@ void createSpa(async (connection) => { return webAuthnInIframeFlow(connection); } else { analytics.event("page-manage"); + registrationFunnel.init(); // The default flow return authFlowManage(connection); } diff --git a/src/frontend/src/utils/analytics/registrationFunnel.ts b/src/frontend/src/utils/analytics/registrationFunnel.ts new file mode 100644 index 0000000000..7b221f3bee --- /dev/null +++ b/src/frontend/src/utils/analytics/registrationFunnel.ts @@ -0,0 +1,27 @@ +import { Funnel } from "./Funnel"; + +/** + * Registration flow events: + * + * Square brackets [] indicate optional events. + * + * registration-start (INIT) + * registration-trigger + * registration-webauthn-start + * [captcha-check] + * registration-created + * [copy-new-identity-number] + * registration-success + */ +export const RegistrationEvents = { + Trigger: "registration-trigger", + CaptchaCheck: "captcha-check", + WebauthnStart: "registration-webauthn-start", + Created: "registration-created", + CopyNewIdentityNumber: "copy-new-identity-number", + Success: "registration-success", +} as const; + +export const registrationFunnel = new Funnel( + "registration", +);