fix(studio): emit sign_in on totp challenge (#49755)

The dashboard's `sign_in` event never fires when a user completes a TOTP
challenge: `SignInForm` only tracks when no MFA challenge is needed, and
the /sign-in-mfa page only tracks on mount when the assurance level is
already satisfied (OAuth/SSO returns). Sign-ins that go through the
actual MFA form were invisible to analytics, and the login audit event
was missing on the same path.

**Changed:**
- **MFA-challenged sign-ins now tracked**: `SignInMfaForm` fires
`sign_in` (reading the same `method` query param the page mount site
reads) plus the login audit event on successful TOTP verification, in
the sign-in context only. The forgot-password flow stays untracked: it
is a reset, not a sign-in.
- **Password+MFA sign-ins report `method: email`**: `SignInForm` now
passes `?method=email` when routing to /sign-in-mfa instead of falling
through to `unknown`.
- **Partner TOTP sign-ins carry their provider**: `SignInPartner` now
passes `?method=<partner>` when routing to /sign-in-mfa, matching the
raw-provider-name convention the other entry points use.
- **Join caveat documented**: the `SignInEvent` doc comment now notes
the event is captured server-side and races the identify call, so it is
not a valid funnel join key across the auth boundary.

## Linear
- fixes GROWTH-1156


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added sign-in method details to MFA redirects for email and partner
authentication, improving sign-in flow tracking.
* Added telemetry and login auditing for successful MFA sign-ins while
keeping forgot-password flows untracked.
* **Documentation**
* Clarified sign-in event tracking coverage, including OAuth providers,
server-side capture, anonymous identifiers, and the sign-in page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Pamela Chia authored and GitHub committed 2026-09-01 13:26:08 +00:00
1 parent 3146650a5a
commit 3338be76f0
4 files changed
+18 -5

No files matched your search

@@ -81,7 +81,7 @@ export const SignInForm = () => {
if (data) {
if (data.currentLevel !== data.nextLevel) {
toast.success(`You need to provide your second factor authentication`, { id: toastId })
const url = buildPathWithParams('/sign-in-mfa')
const url = buildPathWithParams('/sign-in-mfa?method=email')
router.replace(url)
return
}
@@ -1,7 +1,7 @@
import { zodResolver } from '@hookform/resolvers/zod'
import type { Factor } from '@supabase/supabase-js'
import { useQueryClient } from '@tanstack/react-query'
import { useAuthError } from 'common'
import { useAuthError, useParams } from 'common'
import { Lock } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
@@ -13,10 +13,12 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import z from 'zod'
import { AlertError } from '@/components/ui/AlertError'
import { useAddLoginEvent } from '@/data/misc/audit-login-mutation'
import { useMfaChallengeAndVerifyMutation } from '@/data/profile/mfa-challenge-and-verify-mutation'
import { useMfaListFactorsQuery } from '@/data/profile/mfa-list-factors-query'
import { useSignOut } from '@/lib/auth'
import { getReturnToPath } from '@/lib/gotrue'
import { useTrack } from '@/lib/telemetry/track'
const schema = z.object({
code: z.string().min(1, 'MFA Code is required'),
@@ -39,6 +41,10 @@ export const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => {
const router = useRouter()
const signOut = useSignOut()
const queryClient = useQueryClient()
const { method: signInMethod = 'unknown' } = useParams()
const track = useTrack()
const { mutate: addLoginEvent } = useAddLoginEvent()
const [selectedFactor, setSelectedFactor] = useState<Factor | null>(null)
const form = useForm<z.infer<typeof schema>>({
@@ -61,6 +67,11 @@ export const SignInMfaForm = ({ context = 'sign-in' }: SignInMfaFormProps) => {
isSuccess,
} = useMfaChallengeAndVerifyMutation({
onSuccess: async () => {
if (context === 'sign-in') {
track('sign_in', { category: 'account', method: signInMethod })
addLoginEvent({})
}
await queryClient.resetQueries()
if (context === 'forgot-password') {
@@ -21,7 +21,7 @@ export const SignInPartner = () => {
try {
await auth.signInWithIdToken({ provider: partner, token })
} finally {
router.replace({ pathname: '/sign-in-mfa' })
router.replace({ pathname: '/sign-in-mfa', query: { method: partner } })
}
} else {
router.replace({ pathname: '/sign-in' })
+4 -2
View File
@@ -46,14 +46,16 @@ export interface SignUpEvent {
}
/**
* Triggered when a user signs in with GitHub, Email and Password or SSO.
* Triggered when a user signs in with an OAuth provider, Email and Password, or SSO.
*
* Some unintuitive behavior:
* - If signing up with GitHub the SignInEvent gets triggered first before the SignUpEvent.
* - Captured server-side; the distinct_id often resolves to the anonymous cookie because
* the event races identify, so don't use it as a funnel join key across the auth boundary.
*
* @group Events
* @source studio
* @page /sign-in-mfa
* @page /sign-in, /sign-in-mfa
*/
export interface SignInEvent {
action: 'sign_in'