From 7a546ff8a125473581c2b5befb8ca3fb37e9608c Mon Sep 17 00:00:00 2001 From: forehalo Date: Wed, 11 Sep 2024 03:28:32 +0000 Subject: [PATCH] feat(core): add auth metrics (#8194) close AF-849 --- .../core/src/components/affine/auth/index.tsx | 4 +- .../src/components/affine/auth/sign-in.tsx | 3 - packages/frontend/core/src/mixpanel/events.ts | 13 +- .../core/src/modules/cloud/services/auth.ts | 154 +++++++++++------- .../core/src/pages/auth/oauth-callback.tsx | 6 +- 5 files changed, 108 insertions(+), 72 deletions(-) diff --git a/packages/frontend/core/src/components/affine/auth/index.tsx b/packages/frontend/core/src/components/affine/auth/index.tsx index 98bbe34b04..43da4b204c 100644 --- a/packages/frontend/core/src/components/affine/auth/index.tsx +++ b/packages/frontend/core/src/components/affine/auth/index.tsx @@ -74,8 +74,8 @@ export function AuthModal() { break; } case 'oauth': { - const { code, state } = payload; - await authService.signInOauth(code, state); + const { code, state, provider } = payload; + await authService.signInOauth(code, state, provider); break; } } diff --git a/packages/frontend/core/src/components/affine/auth/sign-in.tsx b/packages/frontend/core/src/components/affine/auth/sign-in.tsx index 03fe90e64d..dfda02c981 100644 --- a/packages/frontend/core/src/components/affine/auth/sign-in.tsx +++ b/packages/frontend/core/src/components/affine/auth/sign-in.tsx @@ -2,7 +2,6 @@ import { notify } from '@affine/component'; import { AuthInput, ModalHeader } from '@affine/component/auth-components'; import { Button } from '@affine/component/ui/button'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; -import { track } from '@affine/core/mixpanel'; import { Trans, useI18n } from '@affine/i18n'; import { ArrowRightBigIcon } from '@blocksuite/icons/rc'; import { useService } from '@toeverything/infra'; @@ -59,7 +58,6 @@ export const SignIn: FC> = ({ email, }); } else { - track.$.$.auth.signIn(); await authService.sendEmailMagicLink(email, verifyToken, challenge); setAuthState({ state: 'afterSignInSendEmail', @@ -68,7 +66,6 @@ export const SignIn: FC> = ({ } } else { await authService.sendEmailMagicLink(email, verifyToken, challenge); - track.$.$.auth.signUp(); setAuthState({ state: 'afterSignUpSendEmail', email, diff --git a/packages/frontend/core/src/mixpanel/events.ts b/packages/frontend/core/src/mixpanel/events.ts index efa936f1a5..292c1d695f 100644 --- a/packages/frontend/core/src/mixpanel/events.ts +++ b/packages/frontend/core/src/mixpanel/events.ts @@ -91,7 +91,7 @@ type ShareEvents = | 'copyShareLink' | 'openShareMenu' | 'share'; -type AuthEvents = 'signIn' | 'signUp' | 'oauth' | 'signOut'; +type AuthEvents = 'signIn' | 'signInFail' | 'signedIn' | 'signOut'; type AccountEvents = 'uploadAvatar' | 'removeAvatar' | 'updateUserName'; type PaymentEvents = | 'viewPlans' @@ -141,7 +141,7 @@ const PageEvents = { $: { $: { $: ['createWorkspace', 'checkout'], - auth: ['oauth', 'signIn', 'signUp'], + auth: ['signIn', 'signedIn', 'signInFail', 'signOut'], }, sharePanel: { $: ['createShareLink', 'copyShareLink', 'export', 'open'], @@ -347,9 +347,16 @@ type TabActionType = | 'switchTab' | 'separateTabs'; +type AuthArgs = { + method: 'password' | 'magic-link' | 'oauth'; + provider?: string; +}; + export type EventArgs = { createWorkspace: { flavour: string }; - oauth: { provider: string }; + signIn: AuthArgs; + signedIn: AuthArgs; + signInFail: AuthArgs; viewPlans: PaymentEventArgs; checkout: PaymentEventArgs; subscribe: PaymentEventArgs; diff --git a/packages/frontend/core/src/modules/cloud/services/auth.ts b/packages/frontend/core/src/modules/cloud/services/auth.ts index 95d651d92a..df0b4121b5 100644 --- a/packages/frontend/core/src/modules/cloud/services/auth.ts +++ b/packages/frontend/core/src/modules/cloud/services/auth.ts @@ -1,4 +1,5 @@ import { AIProvider } from '@affine/core/blocksuite/presets/ai'; +import { track } from '@affine/core/mixpanel'; import { appInfo } from '@affine/electron-api'; import type { OAuthProviderType } from '@affine/graphql'; import { @@ -82,32 +83,41 @@ export class AuthService extends Service { verifyToken: string, challenge?: string ) { - const res = await this.fetchService.fetch('/api/auth/sign-in', { - method: 'POST', - body: JSON.stringify({ - email, - // we call it [callbackUrl] instead of [redirect_uri] - // to make it clear the url is used to finish the sign-in process instead of redirect after signed-in - callbackUrl: `/magic-link?client=${environment.isElectron ? appInfo?.schema : 'web'}`, - }), - headers: { - 'content-type': 'application/json', - ...this.captchaHeaders(verifyToken, challenge), - }, - }); - if (!res.ok) { - throw new Error('Failed to send email'); + track.$.$.auth.signIn({ method: 'magic-link' }); + try { + await this.fetchService.fetch('/api/auth/sign-in', { + method: 'POST', + body: JSON.stringify({ + email, + // we call it [callbackUrl] instead of [redirect_uri] + // to make it clear the url is used to finish the sign-in process instead of redirect after signed-in + callbackUrl: `/magic-link?client=${environment.isElectron ? appInfo?.schema : 'web'}`, + }), + headers: { + 'content-type': 'application/json', + ...this.captchaHeaders(verifyToken, challenge), + }, + }); + } catch (e) { + track.$.$.auth.signInFail({ method: 'magic-link' }); + throw e; } } async signInMagicLink(email: string, token: string) { - await this.fetchService.fetch('/api/auth/magic-link', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, token }), - }); + try { + await this.fetchService.fetch('/api/auth/magic-link', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, token }), + }); + track.$.$.auth.signedIn({ method: 'magic-link' }); + } catch (e) { + track.$.$.auth.signInFail({ method: 'magic-link' }); + throw e; + } } async oauthPreflight( @@ -115,41 +125,54 @@ export class AuthService extends Service { client: string, /** @deprecated*/ redirectUrl?: string ) { - const res = await this.fetchService.fetch('/api/oauth/preflight', { - method: 'POST', - body: JSON.stringify({ provider, redirect_uri: redirectUrl }), - headers: { - 'content-type': 'application/json', - }, - }); + track.$.$.auth.signIn({ method: 'oauth', provider }); + try { + const res = await this.fetchService.fetch('/api/oauth/preflight', { + method: 'POST', + body: JSON.stringify({ provider, redirect_uri: redirectUrl }), + headers: { + 'content-type': 'application/json', + }, + }); - let { url } = await res.json(); + let { url } = await res.json(); - // change `state=xxx` to `state={state:xxx,native:true}` - // so we could know the callback should be redirect to native app - const oauthUrl = new URL(url); - oauthUrl.searchParams.set( - 'state', - JSON.stringify({ - state: oauthUrl.searchParams.get('state'), - client, - }) - ); - url = oauthUrl.toString(); + // change `state=xxx` to `state={state:xxx,native:true}` + // so we could know the callback should be redirect to native app + const oauthUrl = new URL(url); + oauthUrl.searchParams.set( + 'state', + JSON.stringify({ + state: oauthUrl.searchParams.get('state'), + client, + provider, + }) + ); + url = oauthUrl.toString(); - return url; + return url; + } catch (e) { + track.$.$.auth.signInFail({ method: 'oauth', provider }); + throw e; + } } - async signInOauth(code: string, state: string) { - const res = await this.fetchService.fetch('/api/oauth/callback', { - method: 'POST', - body: JSON.stringify({ code, state }), - headers: { - 'content-type': 'application/json', - }, - }); + async signInOauth(code: string, state: string, provider: string) { + try { + const res = await this.fetchService.fetch('/api/oauth/callback', { + method: 'POST', + body: JSON.stringify({ code, state }), + headers: { + 'content-type': 'application/json', + }, + }); - return await res.json(); + track.$.$.auth.signedIn({ method: 'oauth', provider }); + return res.json(); + } catch (e) { + track.$.$.auth.signInFail({ method: 'oauth', provider }); + throw e; + } } async signInPassword(credential: { @@ -158,18 +181,25 @@ export class AuthService extends Service { verifyToken: string; challenge?: string; }) { - const res = await this.fetchService.fetch('/api/auth/sign-in', { - method: 'POST', - body: JSON.stringify(credential), - headers: { - 'content-type': 'application/json', - ...this.captchaHeaders(credential.verifyToken, credential.challenge), - }, - }); - if (!res.ok) { - throw new Error('Failed to sign in'); + track.$.$.auth.signIn({ method: 'password' }); + try { + const res = await this.fetchService.fetch('/api/auth/sign-in', { + method: 'POST', + body: JSON.stringify(credential), + headers: { + 'content-type': 'application/json', + ...this.captchaHeaders(credential.verifyToken, credential.challenge), + }, + }); + if (!res.ok) { + throw new Error('Failed to sign in'); + } + this.session.revalidate(); + track.$.$.auth.signedIn({ method: 'password' }); + } catch (e) { + track.$.$.auth.signInFail({ method: 'password' }); + throw e; } - this.session.revalidate(); } async signOut() { diff --git a/packages/frontend/core/src/pages/auth/oauth-callback.tsx b/packages/frontend/core/src/pages/auth/oauth-callback.tsx index d72a2ff2b2..3ee148ca5e 100644 --- a/packages/frontend/core/src/pages/auth/oauth-callback.tsx +++ b/packages/frontend/core/src/pages/auth/oauth-callback.tsx @@ -14,6 +14,7 @@ import { supportedClient } from './common'; interface LoaderData { state: string; code: string; + provider: string; } export const loader: LoaderFunction = async ({ request }) => { @@ -27,12 +28,13 @@ export const loader: LoaderFunction = async ({ request }) => { } try { - const { state, client } = JSON.parse(stateStr); + const { state, client, provider } = JSON.parse(stateStr); stateStr = state; const payload: LoaderData = { state, code, + provider, }; if (!client || client === 'web') { @@ -64,7 +66,7 @@ export const Component = () => { useEffect(() => { auth - .signInOauth(data.code, data.state) + .signInOauth(data.code, data.state, data.provider) .then(({ redirectUri }) => { // TODO(@forehalo): need a good way to go back to previous tab and close current one nav(redirectUri ?? '/');