From 534dc53f9f57102f989bc3b66dfb505e927939b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 1 Apr 2025 13:40:09 +0200 Subject: [PATCH 1/3] Registration Funnel --- .../src/components/authenticateBox/index.ts | 4 --- src/frontend/src/flows/register/finish.ts | 2 ++ src/frontend/src/flows/register/index.ts | 20 ++++++--------- src/frontend/src/flows/register/passkey.ts | 7 ++---- src/frontend/src/index.ts | 3 +++ .../src/utils/analytics/registrationFunnel.ts | 25 +++++++++++++++++++ 6 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 src/frontend/src/utils/analytics/registrationFunnel.ts diff --git a/src/frontend/src/components/authenticateBox/index.ts b/src/frontend/src/components/authenticateBox/index.ts index 1635e6de9b..e22a12773a 100644 --- a/src/frontend/src/components/authenticateBox/index.ts +++ b/src/frontend/src/components/authenticateBox/index.ts @@ -266,21 +266,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..1f23466edc 100644 --- a/src/frontend/src/flows/register/finish.ts +++ b/src/frontend/src/flows/register/finish.ts @@ -3,6 +3,7 @@ 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 +59,7 @@ 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..0fc27dc5f5 100644 --- a/src/frontend/src/flows/register/index.ts +++ b/src/frontend/src/flows/register/index.ts @@ -43,6 +43,7 @@ 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 +131,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 +148,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 +180,6 @@ export const registerFlow = async ({ const openIdResult = await openidIdentityRegistrationFinish(); if (openIdResult.kind === "loginSuccess") { - analytics.event("registration-openid"); return { ...openIdResult, authnMethod: "google", @@ -194,7 +194,6 @@ export const registerFlow = async ({ return "canceled"; } - analytics.event("registration-passkey"); const alias = await inferPasskeyAlias({ authenticatorType: identity.getAuthenticatorAttachment(), userAgent: navigator.userAgent, @@ -225,7 +224,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 +231,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_; } @@ -263,14 +260,13 @@ export const registerFlow = async ({ identity, }), ); - + 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 +286,7 @@ export const registerFlow = async ({ userNumber, marketingIntroSlot: finishSlot, }); + registrationFunnel.trigger(RegistrationEvents.Success); return { ...result, authnMethod }; }; @@ -535,23 +532,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..89781bf54f 100644 --- a/src/frontend/src/flows/register/passkey.ts +++ b/src/frontend/src/flows/register/passkey.ts @@ -12,9 +12,8 @@ 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 +128,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 +137,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..11d648bf46 --- /dev/null +++ b/src/frontend/src/utils/analytics/registrationFunnel.ts @@ -0,0 +1,25 @@ +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"); \ No newline at end of file From e4d7948c0741740037210181865b690c1f29234d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 1 Apr 2025 13:44:34 +0200 Subject: [PATCH 2/3] Format fixes --- src/frontend/src/flows/register/finish.ts | 9 +++++++-- src/frontend/src/flows/register/index.ts | 9 ++++++--- src/frontend/src/flows/register/passkey.ts | 5 ++++- src/frontend/src/utils/analytics/registrationFunnel.ts | 8 +++++--- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/frontend/src/flows/register/finish.ts b/src/frontend/src/flows/register/finish.ts index 1f23466edc..170e89f2e6 100644 --- a/src/frontend/src/flows/register/finish.ts +++ b/src/frontend/src/flows/register/finish.ts @@ -3,7 +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 { + RegistrationEvents, + registrationFunnel, +} from "$src/utils/analytics/registrationFunnel"; import { TemplateElement, mount, @@ -59,7 +62,9 @@ export const displayUserNumberTemplate = ({ @click=${async () => { try { await navigator.clipboard.writeText(userNumber.toString()); - registrationFunnel.trigger(RegistrationEvents.CopyNewIdentityNumber); + 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 0fc27dc5f5..e7d5fbe799 100644 --- a/src/frontend/src/flows/register/index.ts +++ b/src/frontend/src/flows/register/index.ts @@ -43,7 +43,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"; +import { + RegistrationEvents, + registrationFunnel, +} from "$src/utils/analytics/registrationFunnel"; /** Registration (identity creation) flow for new users */ export const registerFlow = async ({ @@ -260,12 +263,12 @@ export const registerFlow = async ({ identity, }), ); - + if (result.kind !== "loginSuccess") { return result; } result.kind satisfies "loginSuccess"; - + registrationFunnel.trigger(RegistrationEvents.Created); const userNumber = result.userNumber; await finalizeIdentity?.(userNumber); diff --git a/src/frontend/src/flows/register/passkey.ts b/src/frontend/src/flows/register/passkey.ts index 89781bf54f..7818684f19 100644 --- a/src/frontend/src/flows/register/passkey.ts +++ b/src/frontend/src/flows/register/passkey.ts @@ -13,7 +13,10 @@ import { import { nonNullish } from "@dfinity/utils"; import { html, TemplateResult } from "lit-html"; import copyJson from "./passkey.json"; -import { RegistrationEvents, registrationFunnel } from "$src/utils/analytics/registrationFunnel"; +import { + RegistrationEvents, + registrationFunnel, +} from "$src/utils/analytics/registrationFunnel"; /* Anchor construction component (for creating WebAuthn credentials) */ diff --git a/src/frontend/src/utils/analytics/registrationFunnel.ts b/src/frontend/src/utils/analytics/registrationFunnel.ts index 11d648bf46..7b221f3bee 100644 --- a/src/frontend/src/utils/analytics/registrationFunnel.ts +++ b/src/frontend/src/utils/analytics/registrationFunnel.ts @@ -2,9 +2,9 @@ import { Funnel } from "./Funnel"; /** * Registration flow events: - * + * * Square brackets [] indicate optional events. - * + * * registration-start (INIT) * registration-trigger * registration-webauthn-start @@ -22,4 +22,6 @@ export const RegistrationEvents = { Success: "registration-success", } as const; -export const registrationFunnel = new Funnel("registration"); \ No newline at end of file +export const registrationFunnel = new Funnel( + "registration", +); From 0f307dfa4dcc32897553ac55a1cf9df9698317ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 1 Apr 2025 13:49:04 +0200 Subject: [PATCH 3/3] Remove unused --- src/frontend/src/components/authenticateBox/index.ts | 1 - src/frontend/src/flows/register/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/frontend/src/components/authenticateBox/index.ts b/src/frontend/src/components/authenticateBox/index.ts index e22a12773a..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, diff --git a/src/frontend/src/flows/register/index.ts b/src/frontend/src/flows/register/index.ts index e7d5fbe799..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,