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;
}
case 'oauth': {
const { code, state } = payload;
await authService.signInOauth(code, state);
const { code, state, provider } = payload;
await authService.signInOauth(code, state, provider);
break;
}
}

View File

@@ -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<AuthPanelProps<'signIn'>> = ({
email,
});
} else {
track.$.$.auth.signIn();
await authService.sendEmailMagicLink(email, verifyToken, challenge);
setAuthState({
state: 'afterSignInSendEmail',
@@ -68,7 +66,6 @@ export const SignIn: FC<AuthPanelProps<'signIn'>> = ({
}
} else {
await authService.sendEmailMagicLink(email, verifyToken, challenge);
track.$.$.auth.signUp();
setAuthState({
state: 'afterSignUpSendEmail',
email,

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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 ?? '/');