Skip to content

Commit 8ce928a

Browse files
committed
feat(analytics): add auth events across all auth flows
1 parent 1e1d330 commit 8ce928a

16 files changed

Lines changed: 258 additions & 75 deletions

src/api/hooks/useVerifyMfa.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ import {
88
verifyMfa
99
} from '../requests'
1010

11-
type MfaVerifyRequest =
12-
| VerifyTotpRequest
13-
| VerifyPasskeyRequest
14-
| VerifyRecoveryRequest
11+
type AnalyticsMeta = {
12+
method?: string
13+
}
14+
15+
export type MfaVerifyRequest =
16+
| (VerifyTotpRequest & AnalyticsMeta)
17+
| (VerifyPasskeyRequest & AnalyticsMeta)
18+
| (VerifyRecoveryRequest & AnalyticsMeta)
1519

1620
export const useVerifyMfa = (
1721
options?: Omit<

src/components/auth/auth-social.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useGetAvailableSsoProviders } from '@/src/api/hooks'
1111
import { getAuthUrl } from '@/src/api/requests'
1212
import { SSO_PROVIDERS } from '@/src/constants'
1313
import { useFingerprint } from '@/src/hooks'
14+
import { analytics } from '@/src/lib/analytics'
1415

1516
export function AuthSocial() {
1617
const router = useRouter()
@@ -25,6 +26,8 @@ export function AuthSocial() {
2526
const { mutate, isPending } = useMutation({
2627
mutationKey: ['oauth login'],
2728
mutationFn: (provider: string) => {
29+
analytics.auth.social.redirect(provider)
30+
2831
const payload =
2932
fingerprint && !fpError
3033
? {
@@ -35,10 +38,14 @@ export function AuthSocial() {
3538

3639
return getAuthUrl(provider, payload)
3740
},
38-
onSuccess(data) {
41+
onSuccess(data, variables) {
42+
analytics.auth.social.success(variables)
43+
3944
router.push(data.url)
4045
},
41-
onError(error: any) {
46+
onError(error: any, variables) {
47+
analytics.auth.social.fail(variables, error.message)
48+
4249
toast.error(
4350
error.response?.data?.message ?? 'Ошибка при создании URL'
4451
)
@@ -66,7 +73,10 @@ export function AuthSocial() {
6673
return (
6774
<Button
6875
key={index}
69-
onClick={() => mutate(meta.id)}
76+
onClick={() => {
77+
analytics.auth.social.click(meta.id)
78+
mutate(meta.id)
79+
}}
7080
variant='outline'
7181
className='[&_svg]:size-[21px]'
7282
disabled={isPending}

src/components/auth/login-form.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { useLogin } from '@/src/api/hooks'
2626
import { instance } from '@/src/api/instance'
2727
import { ROUTES } from '@/src/constants'
2828
import { useFingerprint } from '@/src/hooks'
29-
import { analytics } from '@/src/lib/analytics/events'
29+
import { analytics } from '@/src/lib/analytics'
3030
import { cookies } from '@/src/lib/cookie'
3131

3232
const loginSchema = z.object({
@@ -54,15 +54,17 @@ export function LoginForm() {
5454

5555
const { mutateAsync, isPending } = useLogin({
5656
onSuccess(data) {
57-
analytics.auth.login.success()
58-
5957
if ('ticket' in data && typeof data.ticket === 'string') {
58+
analytics.auth.login.mfaRequested(data.allowedMethods)
59+
6060
setTicket(data.ticket)
6161
setMethods(data.allowedMethods)
6262
setUserId(data.userId)
6363
}
6464

6565
if ('token' in data && typeof data.token === 'string') {
66+
analytics.auth.login.success()
67+
6668
cookies.set('token', data.token, { expires: 30 })
6769

6870
instance.defaults.headers['X-Session-Token'] = data.token
@@ -90,6 +92,10 @@ export function LoginForm() {
9092
}
9193
})
9294

95+
useEffect(() => {
96+
analytics.auth.login.view()
97+
}, [])
98+
9399
useEffect(() => {
94100
if (form.formState.isSubmitSuccessful && form.getValues('captcha')) {
95101
form.reset()
@@ -149,6 +155,10 @@ export function LoginForm() {
149155
placeholder='tony@starkindustries.com'
150156
disabled={isPending}
151157
{...field}
158+
onChange={e => {
159+
field.onChange(e)
160+
analytics.auth.login.emailInput()
161+
}}
152162
/>
153163
</FormControl>
154164
<FormMessage />
@@ -175,6 +185,10 @@ export function LoginForm() {
175185
placeholder='******'
176186
disabled={isPending}
177187
{...field}
188+
onChange={e => {
189+
field.onChange(e)
190+
analytics.auth.login.passwordInput()
191+
}}
178192
/>
179193
</FormControl>
180194
<FormMessage />

src/components/auth/mfa-form.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { startAuthentication } from '@simplewebauthn/browser'
22
import { ArrowLeftIcon, KeyIcon } from 'lucide-react'
33
import { useRouter, useSearchParams } from 'next/navigation'
4-
import { useState } from 'react'
4+
import { useEffect, useState } from 'react'
55
import { toast } from 'sonner'
66

77
import { Button } from '../ui/button'
@@ -13,6 +13,7 @@ import { useVerifyMfa } from '@/src/api/hooks/useVerifyMfa'
1313
import { instance } from '@/src/api/instance'
1414
import { generateAuthenticationOptions } from '@/src/api/requests'
1515
import { MFA_OPTIONS, MfaMethod, ROUTES } from '@/src/constants'
16+
import { analytics } from '@/src/lib/analytics'
1617
import { cookies } from '@/src/lib/cookie'
1718
import { cn } from '@/src/lib/utils'
1819

@@ -32,7 +33,11 @@ export function MfaForm({ ticket, methods, userId, onBack }: MfaFormProps) {
3233
const searchParams = useSearchParams()
3334

3435
const { mutate, isPending } = useVerifyMfa({
35-
onSuccess(data) {
36+
onSuccess(data, variables) {
37+
console.log('METHOD: ', variables.method)
38+
39+
analytics.auth.mfa.success(variables.method!)
40+
3641
cookies.set('token', data.token, { expires: 30 })
3742

3843
instance.defaults.headers['X-Session-Token'] = data.token
@@ -42,16 +47,24 @@ export function MfaForm({ ticket, methods, userId, onBack }: MfaFormProps) {
4247

4348
router.push(redirectTo)
4449
},
45-
onError(error: any) {
46-
toast.error(error.response?.data?.message ?? 'Ошибка при входе')
50+
onError(error: any, variables) {
51+
const message = error.response?.data?.message ?? 'Ошибка при входе'
52+
analytics.auth.mfa.fail(variables.method!, message)
53+
54+
toast.error(message)
4755
}
4856
})
4957

58+
useEffect(() => {
59+
analytics.auth.mfa.methodsShown(methods)
60+
}, [methods])
61+
5062
const availableOptions = MFA_OPTIONS.filter(option =>
5163
methods.includes(option.id)
5264
)
53-
5465
const handleMethodSelect = (method: MfaMethod) => {
66+
analytics.auth.mfa.select(method)
67+
5568
setSelectedMethod(method)
5669
setCode('')
5770
}
@@ -68,6 +81,8 @@ export function MfaForm({ ticket, methods, userId, onBack }: MfaFormProps) {
6881
const handleVerify = () => {
6982
if (!selectedMethod || !code.trim()) return
7083

84+
analytics.auth.mfa.submit(selectedMethod)
85+
7186
const payload =
7287
selectedMethod === 'totp'
7388
? { ticket, totpCode: code }
@@ -77,14 +92,17 @@ export function MfaForm({ ticket, methods, userId, onBack }: MfaFormProps) {
7792
}
7893

7994
const handlePasskeyAuth = async () => {
95+
analytics.auth.mfa.passkeyStart()
8096
setIsLoading(true)
8197
try {
8298
const options = await generateAuthenticationOptions({ userId })
83-
8499
const attestationResponse = await startAuthentication(options)
85100

101+
analytics.auth.mfa.passkeySuccess()
102+
86103
mutate({ ticket, attestationResponse })
87-
} catch (error) {
104+
} catch (error: any) {
105+
analytics.auth.mfa.passkeyFail(error?.message)
88106
console.error('Passkey authentication failed:', error)
89107
} finally {
90108
setIsLoading(false)

src/components/auth/new-password-form.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Input } from '../ui/input'
2222
import { AuthWrapper } from './auth-wrapper'
2323
import { passwordReset } from '@/src/api/requests'
2424
import { ROUTES } from '@/src/constants'
25+
import { analytics } from '@/src/lib/analytics'
2526

2627
const newPasswordSchema = z.object({
2728
token: z.string().max(128, { message: 'Некорректный токен' }),
@@ -41,12 +42,15 @@ export function NewPasswordForm() {
4142
mutationKey: ['password reset'],
4243
mutationFn: (data: NewPassword) => passwordReset(data),
4344
onSuccess() {
45+
analytics.auth.newPassword.success()
4446
push('/auth/login')
4547
},
4648
onError(error: any) {
47-
toast.error(
48-
error.response?.data?.message ?? 'Ошибка при регистрации'
49-
)
49+
const message =
50+
error.response?.data?.message ?? 'Ошибка при сбросе пароля'
51+
analytics.auth.newPassword.fail(message)
52+
53+
toast.error(message)
5054
}
5155
})
5256

@@ -58,11 +62,17 @@ export function NewPasswordForm() {
5862
}
5963
})
6064

65+
useEffect(() => {
66+
analytics.auth.newPassword.view()
67+
}, [])
68+
6169
useEffect(() => {
6270
form.reset()
6371
}, [form, form.reset, form.formState.isSubmitSuccessful])
6472

6573
async function onSubmit(data: NewPassword) {
74+
analytics.auth.newPassword.submit()
75+
6676
await mutateAsync({
6777
token,
6878
password: data.password

src/components/auth/register-form.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { useRegister } from '@/src/api/hooks'
2525
import { instance } from '@/src/api/instance'
2626
import { ROUTES } from '@/src/constants'
2727
import { useFingerprint } from '@/src/hooks'
28+
import { analytics } from '@/src/lib/analytics'
2829
import { cookies } from '@/src/lib/cookie'
2930

3031
const registerSchema = z.object({
@@ -48,16 +49,20 @@ export function RegisterForm() {
4849

4950
const { mutateAsync, isPending } = useRegister({
5051
onSuccess(data) {
52+
analytics.auth.register.success()
53+
5154
cookies.set('token', data.token, { expires: 30 })
5255

5356
instance.defaults.headers['X-Session-Token'] = data.token
5457

55-
push('/account')
58+
push(ROUTES.ACCOUNT.ROOT)
5659
},
5760
onError(error: any) {
58-
toast.error(
61+
const message =
5962
error.response?.data?.message ?? 'Ошибка при регистрации'
60-
)
63+
analytics.auth.register.fail(message)
64+
65+
toast.error(message)
6166
}
6267
})
6368

@@ -71,13 +76,19 @@ export function RegisterForm() {
7176
}
7277
})
7378

79+
useEffect(() => {
80+
analytics.auth.register.view()
81+
}, [])
82+
7483
useEffect(() => {
7584
if (form.formState.isSubmitSuccessful && form.getValues('captcha')) {
7685
form.reset()
7786
}
7887
}, [form, form.reset, form.formState.isSubmitSuccessful])
7988

8089
async function onSubmit(values: Register) {
90+
analytics.auth.register.submit()
91+
8192
if (!values.captcha) {
8293
toast.warning('Пройдите капчу!')
8394
return
@@ -118,6 +129,10 @@ export function RegisterForm() {
118129
placeholder='Tony Stark'
119130
disabled={isPending}
120131
{...field}
132+
onChange={e => {
133+
field.onChange(e)
134+
analytics.auth.register.nameInput()
135+
}}
121136
/>
122137
</FormControl>
123138
<FormMessage />
@@ -135,6 +150,10 @@ export function RegisterForm() {
135150
placeholder='tony@starkindustries.com'
136151
disabled={isPending}
137152
{...field}
153+
onChange={e => {
154+
field.onChange(e)
155+
analytics.auth.register.emailInput()
156+
}}
138157
/>
139158
</FormControl>
140159
<FormMessage />
@@ -153,6 +172,10 @@ export function RegisterForm() {
153172
placeholder='******'
154173
disabled={isPending}
155174
{...field}
175+
onChange={e => {
176+
field.onChange(e)
177+
analytics.auth.register.passwordInput()
178+
}}
156179
/>
157180
</FormControl>
158181
<FormMessage />
@@ -181,6 +204,7 @@ export function RegisterForm() {
181204
size='lg'
182205
isLoading={isPending}
183206
className='w-full'
207+
onClick={() => analytics.auth.register.click()}
184208
>
185209
Продолжить
186210
</Button>

src/components/auth/reset-password-form.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Input } from '../ui/input'
2222
import { AuthWrapper } from './auth-wrapper'
2323
import { sendPasswordReset } from '@/src/api/requests'
2424
import { ROUTES } from '@/src/constants'
25+
import { analytics } from '@/src/lib/analytics'
2526

2627
const resetPasswordSchema = z.object({
2728
email: z
@@ -37,13 +38,17 @@ export function ResetPasswordForm() {
3738
mutationKey: ['send password reset'],
3839
mutationFn: (data: ResetPassword) => sendPasswordReset(data),
3940
onSuccess() {
41+
analytics.auth.resetPassword.success()
42+
4043
form.reset()
4144
toast.success('Письмо с инструкциями отправлено на вашу почту')
4245
},
4346
onError(error: any) {
44-
toast.error(
47+
const message =
4548
error.response?.data?.message ?? 'Ошибка при сбросе пароля'
46-
)
49+
analytics.auth.resetPassword.fail(message)
50+
51+
toast.error(message)
4752
}
4853
})
4954

@@ -55,13 +60,19 @@ export function ResetPasswordForm() {
5560
}
5661
})
5762

63+
useEffect(() => {
64+
analytics.auth.resetPassword.view()
65+
}, [])
66+
5867
useEffect(() => {
5968
if (form.formState.isSubmitSuccessful && form.getValues('captcha')) {
6069
form.reset()
6170
}
6271
}, [form, form.reset, form.formState.isSubmitSuccessful])
6372

6473
async function onSubmit(data: ResetPassword) {
74+
analytics.auth.resetPassword.submit()
75+
6576
if (!data.captcha) {
6677
toast.warning('Пройдите капчу!')
6778
return

0 commit comments

Comments
 (0)