Merged in fix/SW-2679-tracking-signup-details-forms (pull request #2236)

feat(SW-2679): Added form tracking for checkout and signup

* feat(SW-2679): Added form tracking for checkout and signup

* fix(SW-2679): fixes from review


Approved-by: Michael Zetterberg
This commit is contained in:
Tobias Johansson
2025-06-10 06:35:13 +00:00
parent 3688d5ece8
commit ead822fa62
5 changed files with 199 additions and 10 deletions

View File

@@ -1,7 +1,14 @@
"use client"
import isEqual from "fast-deep-equal"
import { usePathname } from "next/navigation"
import { startTransition, useEffect, useRef, useState } from "react"
import {
type Control,
type FieldErrors,
type FieldValues,
useFormState,
type UseFromSubscribe,
} from "react-hook-form"
import { trpc } from "@/lib/trpc/client"
import useRouterTransitionStore from "@/stores/router-transition"
@@ -11,6 +18,12 @@ import useLang from "@/hooks/useLang"
import { useSessionId } from "@/hooks/useSessionId"
import { promiseWithTimeout } from "@/utils/promiseWithTimeout"
import { createSDKPageObject, trackPageView } from "@/utils/tracking"
import {
type FormType,
trackFormAbandonment,
trackFormInputStarted,
trackFormValidationError,
} from "@/utils/tracking/form"
import type {
TrackingSDKProps,
@@ -261,3 +274,83 @@ const getPageLoadTimeEntry = () => {
observer.observe({ type: "navigation", buffered: true })
})
}
export function useFormTracking<T extends FieldValues>(
formType: FormType,
subscribe: UseFromSubscribe<T>,
control: Control<T>,
nameSuffix: string = ""
) {
const [formStarted, setFormStarted] = useState(false)
const lastAccessedField = useRef<string | undefined>(undefined)
const formState = useFormState({ control })
const previousErrors = useRef<FieldErrors<T>>({})
useEffect(() => {
const errors = formState.errors
const prevErrors = previousErrors.current
const isNewErrors = !isEqual(errors, prevErrors)
if (Object.keys(errors).length && isNewErrors) {
const errorString = Object.values(errors)
.map((err) => {
if (err && "message" in err) {
return err.message
}
const nested = Object.values(err ?? {}).find(
(val) => val && typeof val === "object" && "message" in val
)
if (nested) {
return nested.message
}
return undefined
})
.filter(Boolean)
.join("|")
trackFormValidationError(formType, errorString, nameSuffix)
previousErrors.current = { ...errors }
}
}, [formType, formState, nameSuffix])
useEffect(() => {
const unsubscribe = subscribe({
formState: { touchedFields: true },
callback: (data) => {
if ("name" in data) {
lastAccessedField.current = data.name as string
}
if (!formStarted) {
trackFormInputStarted(formType, nameSuffix)
setFormStarted(true)
}
},
})
return () => unsubscribe()
}, [subscribe, formType, nameSuffix, formStarted])
useEffect(() => {
if (!formStarted || !lastAccessedField.current) return
const lastField = lastAccessedField.current
function handleBeforeUnload() {
trackFormAbandonment(formType, lastField, nameSuffix)
}
function handleVisibilityChange() {
if (document.visibilityState === "hidden") {
trackFormAbandonment(formType, lastField, nameSuffix)
}
}
window.addEventListener("beforeunload", handleBeforeUnload)
window.addEventListener("visibilitychange", handleVisibilityChange)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
window.removeEventListener("visibilitychange", handleVisibilityChange)
}
}, [formStarted, formType, nameSuffix])
}