Tracking WIP

This commit is contained in:
Linus Flood
2024-09-27 08:46:12 +02:00
parent 27159d739f
commit 54f094af86
19 changed files with 199 additions and 74 deletions

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { PropsWithChildren, useEffect } from "react" import { PropsWithChildren } from "react"
import { login } from "@/constants/routes/handleAuth" import { login } from "@/constants/routes/handleAuth"
@@ -31,17 +31,6 @@ export default function LoginButton({
? `${login[lang]}?redirectTo=${encodeURIComponent(pathName)}` ? `${login[lang]}?redirectTo=${encodeURIComponent(pathName)}`
: login[lang] : login[lang]
useEffect(() => {
document
.getElementById(trackingId)
?.addEventListener("click", () => trackLoginClick(position))
return () => {
document
.getElementById(trackingId)
?.removeEventListener("click", () => trackLoginClick(position))
}
}, [position, trackingId])
return ( return (
<Link <Link
className={className} className={className}
@@ -49,6 +38,7 @@ export default function LoginButton({
color={color} color={color}
href={href} href={href}
prefetch={false} prefetch={false}
onClick={() => trackLoginClick(position)}
> >
{children} {children}
</Link> </Link>

View File

@@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation"
import { startTransition, useCallback } from "react" import { startTransition, useCallback } from "react"
import useRouterTransitionStore from "@/stores/router-transition" import useRouterTransitionStore from "@/stores/router-transition"
import useTrackingStore from "@/stores/tracking"
import { trackClick, trackPageViewStart } from "@/utils/tracking" import { trackClick, trackPageViewStart } from "@/utils/tracking"
@@ -27,6 +28,7 @@ export default function Link({
...props ...props
}: LinkProps) { }: LinkProps) {
const currentPageSlug = usePathname() const currentPageSlug = usePathname()
const { setInitialPageLoadTime } = useTrackingStore()
let isActive = active || currentPageSlug === href let isActive = active || currentPageSlug === href
if (partialMatch && !isActive) { if (partialMatch && !isActive) {
@@ -72,6 +74,7 @@ export default function Link({
return return
} }
e.preventDefault() e.preventDefault()
setInitialPageLoadTime(Date.now())
trackPageViewStart() trackPageViewStart()
startTransition(() => { startTransition(() => {
startRouterTransition() startRouterTransition()

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { useCallback, useEffect } from "react" import { useCallback, useEffect, useRef } from "react"
import { webviews } from "@/constants/routes/webviews" import { webviews } from "@/constants/routes/webviews"
import useTrackingStore from "@/stores/tracking" import useTrackingStore from "@/stores/tracking"
@@ -10,11 +10,15 @@ import { createSDKPageObject } from "@/utils/tracking"
import { TrackingSDKProps } from "@/types/components/tracking" import { TrackingSDKProps } from "@/types/components/tracking"
export default function TrackingSDK({ pageData, userData }: TrackingSDKProps) { export default function TrackingSDK({
pageData,
userData,
hotelInfo,
}: TrackingSDKProps) {
const pathName = usePathname() const pathName = usePathname()
const isWebview = webviews.includes(pathName) const isWebview = webviews.includes(pathName)
const { hasRun, setHasRun } = useTrackingStore() const { hasRun, setHasRun, getPageLoadTime } = useTrackingStore()
const hasRunLocal = useRef<boolean>(false)
const CookiebotCallbackOnAccept = useCallback(() => { const CookiebotCallbackOnAccept = useCallback(() => {
const cookie = window._satellite.cookie.get("CookieConsent") const cookie = window._satellite.cookie.get("CookieConsent")
@@ -40,31 +44,36 @@ export default function TrackingSDK({ pageData, userData }: TrackingSDKProps) {
} }
useEffect(() => { useEffect(() => {
if (!hasRun) { if (!hasRun && !hasRunLocal.current) {
const perfObserver = new PerformanceObserver((observedEntries) => { hasRunLocal.current = true
const entry = observedEntries.getEntriesByType("navigation")[0]
if (entry && window.adobeDataLayer) {
const trackingData = { ...pageData, pathName }
const pageObject = createSDKPageObject(trackingData)
window.adobeDataLayer.push({
event: "pageView",
pageInfo: pageObject,
userInfo: userData,
})
perfObserver.disconnect()
}
})
perfObserver.observe({
type: "navigation",
buffered: true,
})
setHasRun() setHasRun()
if (window.adobeDataLayer) {
const trackingData = {
...pageData,
pathName,
pageLoadTime: getPageLoadTime(),
}
const pageObject = createSDKPageObject(trackingData)
console.log("TRACKING: Tracking pageView", pageObject, userData)
window.adobeDataLayer.push({
event: "pageView",
pageInfo: pageObject,
userInfo: userData,
hotelInfo: hotelInfo,
})
}
} }
}, [pathName, pageData, userData, hasRun, setHasRun]) }, [
pathName,
pageData,
userData,
hasRun,
setHasRun,
hotelInfo,
getPageLoadTime,
])
useEffect(() => { useEffect(() => {
// handle consent // handle consent

View File

@@ -4,8 +4,9 @@ import { usePathname } from "next/navigation"
import { startTransition, useEffect, useOptimistic, useState } from "react" import { startTransition, useEffect, useOptimistic, useState } from "react"
import useRouterTransitionStore from "@/stores/router-transition" import useRouterTransitionStore from "@/stores/router-transition"
import useTrackingStore from "@/stores/tracking"
import { createSDKPageObject } from "@/utils/tracking" import { createSDKPageObject, trackPageViewStart } from "@/utils/tracking"
import { TrackingSDKProps } from "@/types/components/tracking" import { TrackingSDKProps } from "@/types/components/tracking"
@@ -20,15 +21,28 @@ type TransitionStatus = keyof typeof TransitionStatusEnum
export default function RouterTransition({ export default function RouterTransition({
pageData, pageData,
userData, userData,
hotelInfo,
}: TrackingSDKProps) { }: TrackingSDKProps) {
const [loading, setLoading] = useOptimistic(false) const [loading, setLoading] = useOptimistic(false)
const [status, setStatus] = useState<TransitionStatus>( const [status, setStatus] = useState<TransitionStatus>(
TransitionStatusEnum.NotRun TransitionStatusEnum.NotRun
) )
const { getPageLoadTime } = useTrackingStore()
const pathName = usePathname() const pathName = usePathname()
const { isTransitioning, stopRouterTransition } = useRouterTransitionStore() const { isTransitioning, stopRouterTransition } = useRouterTransitionStore()
// useEffect(() => {
// const handleStart = () => {
// trackPageViewStart()
// setInitialPageLoadTime(Date.now())
// }
// return () => {
// handleStart()
// }
// }, [pathName])
useEffect(() => { useEffect(() => {
if (isTransitioning && status === TransitionStatusEnum.NotRun) { if (isTransitioning && status === TransitionStatusEnum.NotRun) {
startTransition(() => { startTransition(() => {
@@ -48,13 +62,21 @@ export default function RouterTransition({
status === TransitionStatusEnum.Done status === TransitionStatusEnum.Done
) { ) {
if (window.adobeDataLayer) { if (window.adobeDataLayer) {
const trackingData = { ...pageData, pathName } const trackingData = {
...pageData,
pathName,
pageLoadTime: getPageLoadTime(),
}
const pageObject = createSDKPageObject(trackingData) const pageObject = createSDKPageObject(trackingData)
console.log(
"TRACKING: Tracking RouterTransition pageViewEnd",
pageObject
)
window.adobeDataLayer.push({ window.adobeDataLayer.push({
event: "pageViewEnd", event: "pageViewEnd",
pageInfo: pageObject, pageInfo: pageObject,
userInfo: userData, userInfo: userData,
hotelInfo: hotelInfo,
}) })
} }
} }
@@ -67,6 +89,8 @@ export default function RouterTransition({
pageData, pageData,
pathName, pathName,
userData, userData,
hotelInfo,
getPageLoadTime,
]) ])
return null return null

View File

@@ -4,7 +4,10 @@ import RouterTransition from "@/components/TrackingSDK/RouterTransition"
import TrackingSDKClient from "./Client" import TrackingSDKClient from "./Client"
import { TrackingSDKPageData } from "@/types/components/tracking" import {
TrackingSDKHotelInfo,
TrackingSDKPageData,
} from "@/types/components/tracking"
export const preloadUserTracking = () => { export const preloadUserTracking = () => {
void serverClient().user.tracking() void serverClient().user.tracking()
@@ -12,15 +15,25 @@ export const preloadUserTracking = () => {
export default async function TrackingSDK({ export default async function TrackingSDK({
pageData, pageData,
hotelInfo,
}: { }: {
pageData: TrackingSDKPageData pageData: TrackingSDKPageData
hotelInfo?: TrackingSDKHotelInfo
}) { }) {
const userTrackingData = await serverClient().user.tracking() const userTrackingData = await serverClient().user.tracking()
return ( return (
<> <>
<TrackingSDKClient pageData={pageData} userData={userTrackingData} /> <TrackingSDKClient
<RouterTransition pageData={pageData} userData={userTrackingData} /> pageData={pageData}
userData={userTrackingData}
hotelInfo={hotelInfo}
/>
<RouterTransition
pageData={pageData}
userData={userTrackingData}
hotelInfo={hotelInfo}
/>
</> </>
) )
} }

View File

@@ -20,6 +20,9 @@ query GetAccountPage($locale: String!, $uid: String!) {
created_at created_at
updated_at updated_at
} }
page_settings {
tracking_page_name
}
} }
} }

View File

@@ -37,6 +37,9 @@ query GetContentPage($locale: String!, $uid: String!) {
created_at created_at
updated_at updated_at
} }
page_settings {
tracking_page_name
}
} }
} }

View File

@@ -31,6 +31,9 @@ query GetHotelPage($locale: String!, $uid: String!) {
} }
} }
} }
page_settings {
tracking_page_name
}
} }
} }

View File

@@ -33,6 +33,9 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
created_at created_at
updated_at updated_at
} }
page_settings {
tracking_page_name
}
} }
} }

View File

@@ -46,6 +46,9 @@ export const accountPageSchema = z.object({
heading: z.string().nullable(), heading: z.string().nullable(),
title: z.string(), title: z.string(),
url: z.string(), url: z.string(),
page_settings: z.object({
tracking_page_name: z.string().nullable(),
}),
system: systemSchema.merge( system: systemSchema.merge(
z.object({ z.object({
created_at: z.string(), created_at: z.string(),

View File

@@ -191,11 +191,16 @@ export const accountPageQueryRouter = router({
const tracking: TrackingSDKPageData = { const tracking: TrackingSDKPageData = {
pageId: validatedAccountPage.data.account_page.system.uid, pageId: validatedAccountPage.data.account_page.system.uid,
lang: validatedAccountPage.data.account_page.system.locale as Lang, domainLanguage: validatedAccountPage.data.account_page.system
.locale as Lang,
publishedDate: validatedAccountPage.data.account_page.system.updated_at, publishedDate: validatedAccountPage.data.account_page.system.updated_at,
createdDate: validatedAccountPage.data.account_page.system.created_at, createdDate: validatedAccountPage.data.account_page.system.created_at,
channel: TrackingChannelEnum["scandic-friends"], channel: TrackingChannelEnum["scandic-friends"],
pageType: `member${parsedtitle}page`, pageType: `member${parsedtitle}page`,
pageName:
validatedAccountPage.data.account_page.page_settings.tracking_page_name,
siteSections:
validatedAccountPage.data.account_page.page_settings.tracking_page_name, // Always the same as pageName for this page
} }
return { return {

View File

@@ -18,6 +18,7 @@ import {
shortcutsRefsSchema, shortcutsRefsSchema,
shortcutsSchema, shortcutsSchema,
} from "../schemas/blocks/shortcuts" } from "../schemas/blocks/shortcuts"
import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols"
import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { import {
contentRefsSchema as sidebarContentRefsSchema, contentRefsSchema as sidebarContentRefsSchema,
@@ -31,7 +32,6 @@ import {
import { systemSchema } from "../schemas/system" import { systemSchema } from "../schemas/system"
import { ContentPageEnum } from "@/types/enums/contentPage" import { ContentPageEnum } from "@/types/enums/contentPage"
import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols"
// Block schemas // Block schemas
export const contentPageCards = z export const contentPageCards = z
@@ -58,9 +58,11 @@ export const contentPageShortcuts = z
}) })
.merge(shortcutsSchema) .merge(shortcutsSchema)
export const contentPageTextCols = z.object({ export const contentPageTextCols = z
__typename: z.literal(ContentPageEnum.ContentStack.blocks.TextCols), .object({
}).merge(textColsSchema) __typename: z.literal(ContentPageEnum.ContentStack.blocks.TextCols),
})
.merge(textColsSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [ export const blocksSchema = z.discriminatedUnion("__typename", [
contentPageCards, contentPageCards,
@@ -113,6 +115,9 @@ export const contentPageSchema = z.object({
updated_at: z.string(), updated_at: z.string(),
}) })
), ),
page_settings: z.object({
tracking_page_name: z.string().nullable(),
}),
}), }),
}) })

View File

@@ -63,11 +63,14 @@ export const contentPageQueryRouter = router({
const tracking: TrackingSDKPageData = { const tracking: TrackingSDKPageData = {
pageId: contentPage.data.content_page.system.uid, pageId: contentPage.data.content_page.system.uid,
lang: contentPage.data.content_page.system.locale as Lang, domainLanguage: contentPage.data.content_page.system.locale as Lang,
publishedDate: contentPage.data.content_page.system.updated_at, publishedDate: contentPage.data.content_page.system.updated_at,
createdDate: contentPage.data.content_page.system.created_at, createdDate: contentPage.data.content_page.system.created_at,
channel: TrackingChannelEnum["static-content-page"], channel: TrackingChannelEnum["static-content-page"],
pageType: "staticcontentpage", pageType: "staticcontentpage",
pageName: contentPage.data.content_page.page_settings.tracking_page_name,
siteSections:
contentPage.data.content_page.page_settings.tracking_page_name, // Always the same as pageName for this page
} }
return { return {

View File

@@ -22,5 +22,8 @@ export const hotelPageSchema = z.object({
hotel_page_id: z.string(), hotel_page_id: z.string(),
title: z.string(), title: z.string(),
url: z.string(), url: z.string(),
page_settings: z.object({
tracking_page_name: z.string().nullable(),
}),
}), }),
}) })

View File

@@ -178,6 +178,9 @@ export const loyaltyPageSchema = z.object({
updated_at: z.string(), updated_at: z.string(),
}) })
), ),
page_settings: z.object({
tracking_page_name: z.string().nullable(),
}),
}) })
.transform((data) => { .transform((data) => {
return { return {
@@ -187,6 +190,7 @@ export const loyaltyPageSchema = z.object({
preamble: data.preamble, preamble: data.preamble,
sidebar: data.sidebar ? data.sidebar : [], sidebar: data.sidebar ? data.sidebar : [],
system: data.system, system: data.system,
pageSettings: data.page_settings,
} }
}), }),
}) })

View File

@@ -176,11 +176,13 @@ export const loyaltyPageQueryRouter = router({
const loyaltyTrackingData: TrackingSDKPageData = { const loyaltyTrackingData: TrackingSDKPageData = {
pageId: loyaltyPage.system.uid, pageId: loyaltyPage.system.uid,
lang: loyaltyPage.system.locale as Lang, domainLanguage: loyaltyPage.system.locale as Lang,
publishedDate: loyaltyPage.system.updated_at, publishedDate: loyaltyPage.system.updated_at,
createdDate: loyaltyPage.system.created_at, createdDate: loyaltyPage.system.created_at,
channel: TrackingChannelEnum["scandic-friends"], channel: TrackingChannelEnum["scandic-friends"],
pageType: "loyaltycontentpage", pageType: "loyaltycontentpage",
pageName: loyaltyPage.pageSettings.tracking_page_name,
siteSections: loyaltyPage.pageSettings.tracking_page_name, // Always the same as pageName for this page
} }
getLoyaltyPageSuccessCounter.add(1, metricsVariables) getLoyaltyPageSuccessCounter.add(1, metricsVariables)
console.info( console.info(

View File

@@ -5,11 +5,20 @@ import { create } from "zustand"
interface TrackingStoreState { interface TrackingStoreState {
hasRun: boolean hasRun: boolean
setHasRun: () => void setHasRun: () => void
initialStartTime: number
setInitialPageLoadTime: (time: number) => void
getPageLoadTime: () => number
} }
const useTrackingStore = create<TrackingStoreState>((set) => ({ const useTrackingStore = create<TrackingStoreState>((set, get) => ({
hasRun: false, hasRun: false,
initialStartTime: Date.now(),
setInitialPageLoadTime: (time) => set({ initialStartTime: time }),
setHasRun: () => set(() => ({ hasRun: true })), setHasRun: () => set(() => ({ hasRun: true })),
getPageLoadTime: () => {
const { initialStartTime } = get()
return (Date.now() - initialStartTime) / 1000
},
})) }))
export default useTrackingStore export default useTrackingStore

View File

@@ -13,9 +13,14 @@ export type TrackingSDKPageData = {
pageId: string pageId: string
createdDate: string createdDate: string
publishedDate: string publishedDate: string
lang: Lang domainLanguage: Lang
pageType: string pageType: string
channel: TrackingChannel channel: TrackingChannel
siteVersion?: "new-web"
pageName: string | null
domain?: string
siteSections: string | null
pageLoadTime?: number // Page load time in seconds
} }
export enum LoginTypeEnum { export enum LoginTypeEnum {
@@ -35,16 +40,59 @@ export type TrackingSDKUserData = {
loginAction?: "login success" loginAction?: "login success"
} }
export type TrackingSDKHotelInfo = {
hotelID?: string
arrivalData?: Date
departureDate?: Date
noAdults?: number
noChildren?: number
ageOfChildren?: string // "10", "2,5,10"
//rewardNight?: boolean
//bookingCode?: string
//bookingCodeAvailability?: boolean
leadTime?: number // Number of days from booking date until arrivalDate
noOfRoom?: number
//bonuscheque?: boolean
childBedPreference?: string
duration?: number // Number of nights to stay
availableResults?: number // Number of hotels to choose from after a city search
bookingTypeofDay?: string // Weekend or weekday
searchTerm?: string
roomPrice?: string
rateCode?: string
rateCodeCancellationRule?: string
rateCodeName?: string // Scandic Friends - full flex inkl. frukost
rateCodeType?: string // regular, promotion etc
revenueCurrencyCode?: string // SEK, DKK, NOK, EUR
roomTypeCode?: string
roomTypePosition?: number // Which position the room had in the list of available rooms
roomTypeName?: string
bedType?: string
bedTypePosition?: number // Which position the bed type had in the list of available bed types
breakfastOption?: string // "no breakfast" or "breakfast buffet"
bnr?: string // Booking number
analyticsrateCode?: string // flex, save, change
specialRoomType?: string // allergy room, pet-friendly, accesibillity room
//modifyValues?: string // <price:<value>,roomtype:value>,bed:<value,<breakfast:value>
}
export type TrackingSDKProps = { export type TrackingSDKProps = {
pageData: TrackingSDKPageData pageData: TrackingSDKPageData
userData: TrackingSDKUserData userData: TrackingSDKUserData
hotelInfo?: TrackingSDKHotelInfo
} }
export type TrackingSDKData = TrackingSDKPageData & { export type TrackingSDKData = TrackingSDKPageData & {
pathName: string pathName: string
} }
export type TrackingPosition =
| "top menu"
| "hamburger menu"
| "join scandic friends sidebar"
// Old tracking setup types: // Old tracking setup types:
// TODO: Remove this when we delete "current site"
export type TrackingProps = { export type TrackingProps = {
pageData: { pageData: {
pageId: string pageId: string
@@ -73,8 +121,3 @@ export type SiteSectionObject = {
sitesection5: string sitesection5: string
sitesection6: string sitesection6: string
} }
export type TrackingPosition =
| "top menu"
| "hamburger menu"
| "join scandic friends sidebar"

View File

@@ -2,6 +2,7 @@ import { TrackingPosition, TrackingSDKData } from "@/types/components/tracking"
export function trackClick(name: string) { export function trackClick(name: string) {
if (typeof window !== "undefined" && window.adobeDataLayer) { if (typeof window !== "undefined" && window.adobeDataLayer) {
console.log("TRACKING: Tracking click", name)
window.adobeDataLayer.push({ window.adobeDataLayer.push({
event: "linkClick", event: "linkClick",
cta: { cta: {
@@ -13,6 +14,7 @@ export function trackClick(name: string) {
export function trackPageViewStart() { export function trackPageViewStart() {
if (typeof window !== "undefined" && window.adobeDataLayer) { if (typeof window !== "undefined" && window.adobeDataLayer) {
console.log("TRACKING: Tracking pageViewStart")
window.adobeDataLayer.push({ window.adobeDataLayer.push({
event: "pageViewStart", event: "pageViewStart",
}) })
@@ -21,6 +23,7 @@ export function trackPageViewStart() {
export function trackLoginClick(position: TrackingPosition) { export function trackLoginClick(position: TrackingPosition) {
if (typeof window !== "undefined" && window.adobeDataLayer) { if (typeof window !== "undefined" && window.adobeDataLayer) {
console.log("TRACKING: Tracking loginStart, position", position)
const loginEvent = { const loginEvent = {
event: "loginStart", event: "loginStart",
login: { login: {
@@ -33,26 +36,20 @@ export function trackLoginClick(position: TrackingPosition) {
} }
} }
export function createSDKPageObject(trackingData: TrackingSDKData) { export function createSDKPageObject(
trackingData: TrackingSDKData
): TrackingSDKData {
const [lang, ...segments] = trackingData.pathName const [lang, ...segments] = trackingData.pathName
.split("/") .split("/")
.filter((seg: string) => seg) .filter((seg: string) => seg)
const joinedSegments = segments.join("|") const joinedSegments = segments.join("|")
const { host: domain } = window.location return {
const page_obj = { ...trackingData,
pageType: trackingData.pageType, domain: window.location.host,
pageName: joinedSegments, pageName: trackingData.pageName ?? joinedSegments,
pageId: trackingData.pageId, siteSections: trackingData.siteSections ?? joinedSegments,
channel: trackingData.channel, domainLanguage: trackingData.domainLanguage ?? lang,
siteSection: joinedSegments,
domain,
siteversion: "new-web",
domainlanguage: trackingData.lang ? trackingData.lang : lang,
createDate: trackingData.createdDate,
publishDate: trackingData.publishedDate,
// sessionid: "<unique identifier of session>", // base on what?
} }
return page_obj
} }