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:
@@ -24,6 +24,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import { useFormTracking } from "@/components/TrackingSDK/hooks"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import { type SignUpSchema, signUpSchema } from "./schema"
|
import { type SignUpSchema, signUpSchema } from "./schema"
|
||||||
@@ -99,6 +100,10 @@ export default function SignupForm({ title }: SignUpFormProps) {
|
|||||||
shouldFocusError: true,
|
shouldFocusError: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { control, subscribe } = methods
|
||||||
|
|
||||||
|
useFormTracking("signup", subscribe, control)
|
||||||
|
|
||||||
async function onSubmit(data: SignUpSchema) {
|
async function onSubmit(data: SignUpSchema) {
|
||||||
signup.mutate({ ...data, language: lang })
|
signup.mutate({ ...data, language: lang })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
|||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
|
import { useFormTracking } from "@/components/TrackingSDK/hooks"
|
||||||
import { useRoomContext } from "@/contexts/Details/Room"
|
import { useRoomContext } from "@/contexts/Details/Room"
|
||||||
|
|
||||||
import MemberPriceModal from "../MemberPriceModal"
|
import MemberPriceModal from "../MemberPriceModal"
|
||||||
@@ -84,11 +85,15 @@ export default function Details() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { isValid },
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
trigger,
|
trigger,
|
||||||
|
control,
|
||||||
|
subscribe,
|
||||||
|
formState: { isValid },
|
||||||
} = methods
|
} = methods
|
||||||
|
|
||||||
|
useFormTracking("checkout", subscribe, control, ` - room ${roomNr}`)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addPreSubmitCallback(`${idx}-details`, trigger)
|
addPreSubmitCallback(`${idx}-details`, trigger)
|
||||||
}, [addPreSubmitCallback, idx, trigger])
|
}, [addPreSubmitCallback, idx, trigger])
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
|||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
|
import { useFormTracking } from "@/components/TrackingSDK/hooks"
|
||||||
import { useRoomContext } from "@/contexts/Details/Room"
|
import { useRoomContext } from "@/contexts/Details/Room"
|
||||||
|
|
||||||
import MemberPriceModal from "../MemberPriceModal"
|
import MemberPriceModal from "../MemberPriceModal"
|
||||||
@@ -30,7 +31,8 @@ const formID = "enter-details"
|
|||||||
export default function Details({ user }: DetailsProps) {
|
export default function Details({ user }: DetailsProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const { addPreSubmitCallback } = useEnterDetailsStore((state) => ({
|
const { lastRoom, addPreSubmitCallback } = useEnterDetailsStore((state) => ({
|
||||||
|
lastRoom: state.lastRoom,
|
||||||
addPreSubmitCallback: state.actions.addPreSubmitCallback,
|
addPreSubmitCallback: state.actions.addPreSubmitCallback,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -77,11 +79,14 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const { formState, handleSubmit, trigger, control, subscribe } = methods
|
||||||
formState: { isValid },
|
|
||||||
handleSubmit,
|
useFormTracking(
|
||||||
trigger,
|
"checkout",
|
||||||
} = methods
|
subscribe,
|
||||||
|
control,
|
||||||
|
lastRoom === idx ? "" : " - room 1"
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addPreSubmitCallback(`${idx}-details`, trigger)
|
addPreSubmitCallback(`${idx}-details`, trigger)
|
||||||
@@ -95,12 +100,12 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const updateDetailsStore = useCallback(() => {
|
const updateDetailsStore = useCallback(() => {
|
||||||
if (isValid) {
|
if (formState.isValid) {
|
||||||
handleSubmit(onSubmit)()
|
handleSubmit(onSubmit)()
|
||||||
} else {
|
} else {
|
||||||
setIncomplete()
|
setIncomplete()
|
||||||
}
|
}
|
||||||
}, [handleSubmit, isValid, onSubmit, setIncomplete])
|
}, [handleSubmit, formState.isValid, onSubmit, setIncomplete])
|
||||||
|
|
||||||
useEffect(updateDetailsStore, [updateDetailsStore])
|
useEffect(updateDetailsStore, [updateDetailsStore])
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import isEqual from "fast-deep-equal"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { startTransition, useEffect, useRef, useState } from "react"
|
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 { trpc } from "@/lib/trpc/client"
|
||||||
import useRouterTransitionStore from "@/stores/router-transition"
|
import useRouterTransitionStore from "@/stores/router-transition"
|
||||||
@@ -11,6 +18,12 @@ import useLang from "@/hooks/useLang"
|
|||||||
import { useSessionId } from "@/hooks/useSessionId"
|
import { useSessionId } from "@/hooks/useSessionId"
|
||||||
import { promiseWithTimeout } from "@/utils/promiseWithTimeout"
|
import { promiseWithTimeout } from "@/utils/promiseWithTimeout"
|
||||||
import { createSDKPageObject, trackPageView } from "@/utils/tracking"
|
import { createSDKPageObject, trackPageView } from "@/utils/tracking"
|
||||||
|
import {
|
||||||
|
type FormType,
|
||||||
|
trackFormAbandonment,
|
||||||
|
trackFormInputStarted,
|
||||||
|
trackFormValidationError,
|
||||||
|
} from "@/utils/tracking/form"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TrackingSDKProps,
|
TrackingSDKProps,
|
||||||
@@ -261,3 +274,83 @@ const getPageLoadTimeEntry = () => {
|
|||||||
observer.observe({ type: "navigation", buffered: true })
|
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])
|
||||||
|
}
|
||||||
|
|||||||
81
apps/scandic-web/utils/tracking/form.ts
Normal file
81
apps/scandic-web/utils/tracking/form.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { trackEvent } from "./base"
|
||||||
|
|
||||||
|
export type FormType = "checkout" | "signup"
|
||||||
|
|
||||||
|
export function trackFormInputStarted(type: FormType, nameSuffix?: string) {
|
||||||
|
if (type === "checkout") {
|
||||||
|
trackEvent({
|
||||||
|
event: "formStart",
|
||||||
|
form: {
|
||||||
|
action: "checkout form start",
|
||||||
|
name: "checkout enter detail" + nameSuffix,
|
||||||
|
type: type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (type === "signup") {
|
||||||
|
trackEvent({
|
||||||
|
event: "formStart",
|
||||||
|
form: {
|
||||||
|
action: "signup form start",
|
||||||
|
name: "member registration" + nameSuffix,
|
||||||
|
type: type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackFormAbandonment(
|
||||||
|
type: FormType,
|
||||||
|
lastAccessedField: string,
|
||||||
|
nameSuffix?: string
|
||||||
|
) {
|
||||||
|
if (type === "checkout") {
|
||||||
|
trackEvent({
|
||||||
|
event: "formAbandonment",
|
||||||
|
form: {
|
||||||
|
action: "checkout form abandonment",
|
||||||
|
name: "checkout enter detail" + nameSuffix,
|
||||||
|
type: type,
|
||||||
|
lastAccessedField,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (type === "signup") {
|
||||||
|
trackEvent({
|
||||||
|
event: "formAbandonment",
|
||||||
|
form: {
|
||||||
|
action: "signup form abandonment",
|
||||||
|
name: "member registration" + nameSuffix,
|
||||||
|
type: type,
|
||||||
|
lastAccessedField,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackFormValidationError(
|
||||||
|
type: FormType,
|
||||||
|
errorMessage: string,
|
||||||
|
nameSuffix?: string
|
||||||
|
) {
|
||||||
|
if (type === "checkout") {
|
||||||
|
trackEvent({
|
||||||
|
event: "formError",
|
||||||
|
form: {
|
||||||
|
action: "checkout form error",
|
||||||
|
name: "checkout enter detail" + nameSuffix,
|
||||||
|
type: type,
|
||||||
|
errorMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (type === "signup") {
|
||||||
|
trackEvent({
|
||||||
|
event: "formError",
|
||||||
|
form: {
|
||||||
|
action: "signup form error",
|
||||||
|
name: "member registration" + nameSuffix,
|
||||||
|
type: type,
|
||||||
|
errorMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user