feat(core): add auth metrics (#8194)

close AF-849
This commit is contained in:
forehalo
2024-09-11 03:28:32 +00:00
parent 81ab8ac8b3
commit 7a546ff8a1
5 changed files with 108 additions and 72 deletions

View File

@@ -74,8 +74,8 @@ export function AuthModal() {
break; break;
} }
case 'oauth': { case 'oauth': {
const { code, state } = payload; const { code, state, provider } = payload;
await authService.signInOauth(code, state); await authService.signInOauth(code, state, provider);
break; break;
} }
} }

View File

@@ -2,7 +2,6 @@ import { notify } from '@affine/component';
import { AuthInput, ModalHeader } from '@affine/component/auth-components'; import { AuthInput, ModalHeader } from '@affine/component/auth-components';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { track } from '@affine/core/mixpanel';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
import { ArrowRightBigIcon } from '@blocksuite/icons/rc'; import { ArrowRightBigIcon } from '@blocksuite/icons/rc';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
@@ -59,7 +58,6 @@ export const SignIn: FC<AuthPanelProps<'signIn'>> = ({
email, email,
}); });
} else { } else {
track.$.$.auth.signIn();
await authService.sendEmailMagicLink(email, verifyToken, challenge); await authService.sendEmailMagicLink(email, verifyToken, challenge);
setAuthState({ setAuthState({
state: 'afterSignInSendEmail', state: 'afterSignInSendEmail',
@@ -68,7 +66,6 @@ export const SignIn: FC<AuthPanelProps<'signIn'>> = ({
} }
} else { } else {
await authService.sendEmailMagicLink(email, verifyToken, challenge); await authService.sendEmailMagicLink(email, verifyToken, challenge);
track.$.$.auth.signUp();
setAuthState({ setAuthState({
state: 'afterSignUpSendEmail', state: 'afterSignUpSendEmail',
email, email,

View File

@@ -91,7 +91,7 @@ type ShareEvents =
| 'copyShareLink' | 'copyShareLink'
| 'openShareMenu' | 'openShareMenu'
| 'share'; | 'share';
type AuthEvents = 'signIn' | 'signUp' | 'oauth' | 'signOut'; type AuthEvents = 'signIn' | 'signInFail' | 'signedIn' | 'signOut';
type AccountEvents = 'uploadAvatar' | 'removeAvatar' | 'updateUserName'; type AccountEvents = 'uploadAvatar' | 'removeAvatar' | 'updateUserName';
type PaymentEvents = type PaymentEvents =
| 'viewPlans' | 'viewPlans'
@@ -141,7 +141,7 @@ const PageEvents = {
$: { $: {
$: { $: {
$: ['createWorkspace', 'checkout'], $: ['createWorkspace', 'checkout'],
auth: ['oauth', 'signIn', 'signUp'], auth: ['signIn', 'signedIn', 'signInFail', 'signOut'],
}, },
sharePanel: { sharePanel: {
$: ['createShareLink', 'copyShareLink', 'export', 'open'], $: ['createShareLink', 'copyShareLink', 'export', 'open'],
@@ -347,9 +347,16 @@ type TabActionType =
| 'switchTab' | 'switchTab'
| 'separateTabs'; | 'separateTabs';
type AuthArgs = {
method: 'password' | 'magic-link' | 'oauth';
provider?: string;
};
export type EventArgs = { export type EventArgs = {
createWorkspace: { flavour: string }; createWorkspace: { flavour: string };
oauth: { provider: string }; signIn: AuthArgs;
signedIn: AuthArgs;
signInFail: AuthArgs;
viewPlans: PaymentEventArgs; viewPlans: PaymentEventArgs;
checkout: PaymentEventArgs; checkout: PaymentEventArgs;
subscribe: PaymentEventArgs; subscribe: PaymentEventArgs;

View File

@@ -1,4 +1,5 @@
import { AIProvider } from '@affine/core/blocksuite/presets/ai'; import { AIProvider } from '@affine/core/blocksuite/presets/ai';
import { track } from '@affine/core/mixpanel';
import { appInfo } from '@affine/electron-api'; import { appInfo } from '@affine/electron-api';
import type { OAuthProviderType } from '@affine/graphql'; import type { OAuthProviderType } from '@affine/graphql';
import { import {
@@ -82,32 +83,41 @@ export class AuthService extends Service {
verifyToken: string, verifyToken: string,
challenge?: string challenge?: string
) { ) {
const res = await this.fetchService.fetch('/api/auth/sign-in', { track.$.$.auth.signIn({ method: 'magic-link' });
method: 'POST', try {
body: JSON.stringify({ await this.fetchService.fetch('/api/auth/sign-in', {
email, method: 'POST',
// we call it [callbackUrl] instead of [redirect_uri] body: JSON.stringify({
// to make it clear the url is used to finish the sign-in process instead of redirect after signed-in email,
callbackUrl: `/magic-link?client=${environment.isElectron ? appInfo?.schema : 'web'}`, // 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
headers: { callbackUrl: `/magic-link?client=${environment.isElectron ? appInfo?.schema : 'web'}`,
'content-type': 'application/json', }),
...this.captchaHeaders(verifyToken, challenge), headers: {
}, 'content-type': 'application/json',
}); ...this.captchaHeaders(verifyToken, challenge),
if (!res.ok) { },
throw new Error('Failed to send email'); });
} catch (e) {
track.$.$.auth.signInFail({ method: 'magic-link' });
throw e;
} }
} }
async signInMagicLink(email: string, token: string) { async signInMagicLink(email: string, token: string) {
await this.fetchService.fetch('/api/auth/magic-link', { try {
method: 'POST', await this.fetchService.fetch('/api/auth/magic-link', {
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json',
body: JSON.stringify({ email, token }), },
}); body: JSON.stringify({ email, token }),
});
track.$.$.auth.signedIn({ method: 'magic-link' });
} catch (e) {
track.$.$.auth.signInFail({ method: 'magic-link' });
throw e;
}
} }
async oauthPreflight( async oauthPreflight(
@@ -115,41 +125,54 @@ export class AuthService extends Service {
client: string, client: string,
/** @deprecated*/ redirectUrl?: string /** @deprecated*/ redirectUrl?: string
) { ) {
const res = await this.fetchService.fetch('/api/oauth/preflight', { track.$.$.auth.signIn({ method: 'oauth', provider });
method: 'POST', try {
body: JSON.stringify({ provider, redirect_uri: redirectUrl }), const res = await this.fetchService.fetch('/api/oauth/preflight', {
headers: { method: 'POST',
'content-type': 'application/json', 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}` // change `state=xxx` to `state={state:xxx,native:true}`
// so we could know the callback should be redirect to native app // so we could know the callback should be redirect to native app
const oauthUrl = new URL(url); const oauthUrl = new URL(url);
oauthUrl.searchParams.set( oauthUrl.searchParams.set(
'state', 'state',
JSON.stringify({ JSON.stringify({
state: oauthUrl.searchParams.get('state'), state: oauthUrl.searchParams.get('state'),
client, client,
}) provider,
); })
url = oauthUrl.toString(); );
url = oauthUrl.toString();
return url; return url;
} catch (e) {
track.$.$.auth.signInFail({ method: 'oauth', provider });
throw e;
}
} }
async signInOauth(code: string, state: string) { async signInOauth(code: string, state: string, provider: string) {
const res = await this.fetchService.fetch('/api/oauth/callback', { try {
method: 'POST', const res = await this.fetchService.fetch('/api/oauth/callback', {
body: JSON.stringify({ code, state }), method: 'POST',
headers: { body: JSON.stringify({ code, state }),
'content-type': 'application/json', 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: { async signInPassword(credential: {
@@ -158,18 +181,25 @@ export class AuthService extends Service {
verifyToken: string; verifyToken: string;
challenge?: string; challenge?: string;
}) { }) {
const res = await this.fetchService.fetch('/api/auth/sign-in', { track.$.$.auth.signIn({ method: 'password' });
method: 'POST', try {
body: JSON.stringify(credential), const res = await this.fetchService.fetch('/api/auth/sign-in', {
headers: { method: 'POST',
'content-type': 'application/json', body: JSON.stringify(credential),
...this.captchaHeaders(credential.verifyToken, credential.challenge), headers: {
}, 'content-type': 'application/json',
}); ...this.captchaHeaders(credential.verifyToken, credential.challenge),
if (!res.ok) { },
throw new Error('Failed to sign in'); });
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() { async signOut() {

View File

@@ -14,6 +14,7 @@ import { supportedClient } from './common';
interface LoaderData { interface LoaderData {
state: string; state: string;
code: string; code: string;
provider: string;
} }
export const loader: LoaderFunction = async ({ request }) => { export const loader: LoaderFunction = async ({ request }) => {
@@ -27,12 +28,13 @@ export const loader: LoaderFunction = async ({ request }) => {
} }
try { try {
const { state, client } = JSON.parse(stateStr); const { state, client, provider } = JSON.parse(stateStr);
stateStr = state; stateStr = state;
const payload: LoaderData = { const payload: LoaderData = {
state, state,
code, code,
provider,
}; };
if (!client || client === 'web') { if (!client || client === 'web') {
@@ -64,7 +66,7 @@ export const Component = () => {
useEffect(() => { useEffect(() => {
auth auth
.signInOauth(data.code, data.state) .signInOauth(data.code, data.state, data.provider)
.then(({ redirectUri }) => { .then(({ redirectUri }) => {
// TODO(@forehalo): need a good way to go back to previous tab and close current one // TODO(@forehalo): need a good way to go back to previous tab and close current one
nav(redirectUri ?? '/'); nav(redirectUri ?? '/');