diff --git a/src/frontend/src/components/authenticateBox/index.ts b/src/frontend/src/components/authenticateBox/index.ts index e55d0ce3a5..29984b9f1c 100644 --- a/src/frontend/src/components/authenticateBox/index.ts +++ b/src/frontend/src/components/authenticateBox/index.ts @@ -64,6 +64,7 @@ import { TemplateResult, html, render } from "lit-html"; import { infoToastTemplate } from "../infoToast"; import infoToastCopy from "../infoToast/copy.json"; import authnTemplatesCopy from "./authnTemplatesCopy.json"; +import { LoginEvents, loginFunnel } from "$src/utils/analytics/loginFunnel"; /** Template used for rendering specific authentication screens. See `authnScreens` below * for meaning of "firstTime", "useExisting" and "pick". */ export type AuthnTemplates = { @@ -358,6 +359,7 @@ export const authenticateBoxFlow = async ({ > => { const result = await pages.useExisting(); if (result.tag === "submit") { + loginFunnel.trigger(LoginEvents.TriggerUseExisting); return doLogin({ userNumber: result.userNumber }); } @@ -402,10 +404,12 @@ export const authenticateBoxFlow = async ({ }); if (result.tag === "pick") { + loginFunnel.trigger(LoginEvents.TriggerListItem); return doLogin({ userNumber: result.userNumber }); } result satisfies { tag: "more_options" }; + loginFunnel.trigger(LoginEvents.GoUseExisting); return await doPrompt(); } else { const result = await pages.firstTime(); @@ -843,6 +847,10 @@ const useIdentityFlow = async ({ const doLoginPasskey = async () => { const result = await withLoader(() => loginPasskey(userNumber)); + // We need to trigger the success here because later we don't know whether it was a registration or login. + if (result.kind === "loginSuccess") { + loginFunnel.trigger(LoginEvents.Success); + } return { newAnchor: false, authnMethod: "passkey", ...result } as const; }; diff --git a/src/frontend/src/flows/authorize/postMessageInterface.ts b/src/frontend/src/flows/authorize/postMessageInterface.ts index e4eef97128..2f6f0d8f9d 100644 --- a/src/frontend/src/flows/authorize/postMessageInterface.ts +++ b/src/frontend/src/flows/authorize/postMessageInterface.ts @@ -1,6 +1,7 @@ // Types and functions related to the window post message interface used by // applications that want to authenticate the user using Internet Identity import { analytics } from "$src/utils/analytics/analytics"; +import { loginFunnel } from "$src/utils/analytics/loginFunnel"; import { type SignedDelegation as FrontendSignedDelegation } from "@dfinity/identity"; import { Principal } from "@dfinity/principal"; import { z } from "zod"; @@ -130,6 +131,8 @@ export async function authenticationProtocol({ analytics.event("authorize-client-request-valid", { origin: requestOrigin, }); + // TODO: Add origin to login funnel + loginFunnel.init(); const authContext = { authRequest: requestResult.request, diff --git a/src/frontend/src/utils/analytics/loginFunnel.ts b/src/frontend/src/utils/analytics/loginFunnel.ts new file mode 100644 index 0000000000..ba53c8f1af --- /dev/null +++ b/src/frontend/src/utils/analytics/loginFunnel.ts @@ -0,0 +1,25 @@ +import { Funnel } from "./Funnel"; + +/** + * Login flow events: + * + * Square brackets [] indicate optional events. + * + * login-start (INIT) - Triggered in Landing Page or List of identities + * login-trigger-list-item - Triggered when user clicks on a number + * login-webauthn-start - Triggered when the webauthn is triggered + * login-success - Triggered after successful webauthn interaction + * go-use-existing - Triggered on visiting the Use Existing page + * login-trigger-use-existing - Triggered when user clicks "Continue" in the Use Existing + * login-webauthn-start - Triggered when the webauthn is triggered + * login-success - Triggered after successful webauthn interaction + */ +export enum LoginEvents { + GoUseExisting = "go-use-existing", + TriggerListItem = "login-trigger-list-item", + TriggerUseExisting = "login-trigger-use-existing", + WebauthnStart = "login-webauthn-start", + Success = "login-success", +} + +export const loginFunnel = new Funnel("login"); diff --git a/src/frontend/src/utils/iiConnection.ts b/src/frontend/src/utils/iiConnection.ts index c5d4212be5..b5aa136ad4 100644 --- a/src/frontend/src/utils/iiConnection.ts +++ b/src/frontend/src/utils/iiConnection.ts @@ -78,6 +78,7 @@ import { MultiWebAuthnIdentity } from "./multiWebAuthnIdentity"; import { isRecoveryDevice, RecoveryDevice } from "./recoveryDevice"; import { supportsWebauthRoR } from "./userAgent"; import { isWebAuthnCancel } from "./webAuthnErrorUtils"; +import { LoginEvents, loginFunnel } from "./analytics/loginFunnel"; /* * A (dummy) identity that always uses the same keypair. The secret key is @@ -463,12 +464,10 @@ export class Connection { | UnknownUser | ApiError > => { - analytics.event("login-passkey-start"); let devices: Omit[]; try { devices = await this.lookupAuthenticators(userNumber); } catch (e: unknown) { - analytics.event("login-passkey-error-lookup"); const errObj = e instanceof Error ? e @@ -477,7 +476,6 @@ export class Connection { } if (devices.length === 0) { - analytics.event("login-passkey-error-unknown"); return { kind: "unknownUser", userNumber }; } @@ -488,7 +486,6 @@ export class Connection { // If we reach this point, it's because no PIN identity was found. // Therefore, it's because it was created in another domain. if (webAuthnAuthenticators.length === 0) { - analytics.event("login-passkey-error-pin-other-domain"); return { kind: "pinUserOtherDomain" }; } @@ -500,6 +497,7 @@ export class Connection { ); } + loginFunnel.trigger(LoginEvents.WebauthnStart); return this.fromWebauthnCredentials( userNumber, webAuthnAuthenticators