Merged in feat/SW-1472-destination-tracking (pull request #1474)

Feat/SW-1472 destination tracking

* feat(SW-1472): Added default tracking for destination overview page

* feat(SW-1472): Added default tracking for destination country/city page

* feat(SW-1472): moved tracking functions to different files for better overview

* feat(SW-1472): added destination page tracking


Approved-by: Fredrik Thorsson
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-03-06 10:15:33 +00:00
parent 47785aa07a
commit 230b56b3bd
25 changed files with 502 additions and 373 deletions

View File

@@ -7,7 +7,6 @@ import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests"
import Blocks from "@/components/Blocks"
import Breadcrumbs from "@/components/Breadcrumbs"
import Body from "@/components/TempDesignSystem/Text/Body"
import TrackingSDK from "@/components/TrackingSDK"
import ExperienceList from "../ExperienceList"
import HotelDataContainer, { preload } from "../HotelDataContainer"
@@ -16,6 +15,7 @@ import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap"
import TopImages from "../TopImages"
import DestinationTracking from "../Tracking"
import CityMap from "./CityMap"
import DestinationCityPageSkeleton from "./DestinationCityPageSkeleton"
@@ -65,6 +65,7 @@ export default async function DestinationCityPage() {
<DestinationPageSidePeek
buttonText={sidepeek_button_text}
sidePeekContent={sidepeek_content}
location={city.name}
/>
)}
@@ -85,7 +86,7 @@ export default async function DestinationCityPage() {
/>
</HotelDataContainer>
</Suspense>
<TrackingSDK pageData={tracking} />
<DestinationTracking pageData={tracking} />
</>
)
}

View File

@@ -7,7 +7,6 @@ import Blocks from "@/components/Blocks"
import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Body from "@/components/TempDesignSystem/Text/Body"
import TrackingSDK from "@/components/TrackingSDK"
import CityDataContainer, { preload } from "../CityDataContainer"
import CityListing from "../CityListing"
@@ -16,6 +15,7 @@ import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap"
import TopImages from "../TopImages"
import DestinationTracking from "../Tracking"
import CountryMap from "./CountryMap"
import DestinationCountryPageSkeleton from "./DestinationCountryPageSkeleton"
@@ -69,6 +69,7 @@ export default async function DestinationCountryPage() {
<DestinationPageSidePeek
buttonText={sidepeek_button_text}
sidePeekContent={sidepeek_content}
location={translatedCountry}
/>
)}
<StaticMap
@@ -86,7 +87,7 @@ export default async function DestinationCountryPage() {
/>
</CityDataContainer>
</Suspense>
<TrackingSDK pageData={tracking} />
<DestinationTracking pageData={tracking} />
</>
)
}

View File

@@ -8,6 +8,8 @@ import { useCallback } from "react"
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
import { trackMapClick } from "@/utils/tracking/destinationPage"
import styles from "./clusterMarker.module.css"
interface ClusterMarkerProps {
@@ -34,7 +36,8 @@ export default function ClusterMarker({
if (onMarkerClick) {
onMarkerClick(position)
}
}, [onMarkerClick, position])
trackMapClick(`cluster with hotelIds: ${hotelIds.join(",")}`)
}, [onMarkerClick, position, hotelIds])
return (
<AdvancedMarker

View File

@@ -10,6 +10,7 @@ import { useCallback } from "react"
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
import HotelMarkerByType from "@/components/Maps/Markers"
import { trackMapClick } from "@/utils/tracking/destinationPage"
import HotelMapCard from "../../../HotelMapCard"
@@ -28,6 +29,7 @@ export default function Marker({ position, properties }: MarkerProps) {
const handleClick = useCallback(() => {
setClickedHotel(properties.id)
trackMapClick(properties.name)
}, [setClickedHotel, properties])
function handleCloseCard() {

View File

@@ -6,6 +6,7 @@ import { ChevronRightSmallIcon } from "@/components/Icons"
import JsonToHtml from "@/components/JsonToHtml"
import Button from "@/components/TempDesignSystem/Button"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { trackOpenSidePeekOnDestinationPagesEvent } from "@/utils/tracking/destinationPage"
import type { DestinationCityPageData } from "@/types/trpc/routers/contentstack/destinationCityPage"
import type { DestinationCountryPageData } from "@/types/trpc/routers/contentstack/destinationCountryPage"
@@ -16,18 +17,25 @@ interface DestinationPageSidepeekProps {
| DestinationCityPageData["sidepeek_content"]
| DestinationCountryPageData["sidepeek_content"]
>
location: string
}
export default function DestinationPageSidepeek({
buttonText,
sidePeekContent,
location,
}: DestinationPageSidepeekProps) {
const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false)
const { heading, content } = sidePeekContent
function handleButtonClick() {
setSidePeekIsOpen(true)
trackOpenSidePeekOnDestinationPagesEvent(location)
}
return (
<div>
<Button
onPress={() => setSidePeekIsOpen(true)}
onPress={handleButtonClick}
theme="base"
variant="icon"
intent="text"
@@ -40,6 +48,7 @@ export default function DestinationPageSidepeek({
<SidePeek
title={heading}
isOpen={sidePeekIsOpen}
openInRoot
handleClose={() => setSidePeekIsOpen(false)}
>
<JsonToHtml

View File

@@ -0,0 +1,28 @@
"use client"
import { useSearchParams } from "next/navigation"
import { useEffect } from "react"
import TrackingSDK from "@/components/TrackingSDK"
import { trackOpenMapView } from "@/utils/tracking/destinationPage"
import type { TrackingSDKPageData } from "@/types/components/tracking"
interface DestinationTrackingProps {
pageData: TrackingSDKPageData
}
export default function DestinationTracking({
pageData,
}: DestinationTrackingProps) {
const searchParams = useSearchParams()
const isMapView = searchParams.get("view") === "map"
useEffect(() => {
if (isMapView) {
trackOpenMapView(pageData.pageName, pageData.pageType)
}
}, [isMapView, pageData.pageName, pageData.pageType])
return <TrackingSDK pageData={pageData} />
}

View File

@@ -19,10 +19,6 @@ import {
import { systemSchema } from "../schemas/system"
import type { ImageVaultAsset } from "@/types/components/imageVault"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { DestinationCityPageEnum } from "@/types/enums/destinationCityPage"
const destinationCityPageDestinationSettingsSchema = z
@@ -119,80 +115,57 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
destinationCityPageContent,
])
export const destinationCityPageSchema = z
.object({
destination_city_page: z.object({
title: z.string(),
destination_settings: destinationCityPageDestinationSettingsSchema,
export const destinationCityPageSchema = z.object({
destination_city_page: z.object({
title: z.string(),
destination_settings: destinationCityPageDestinationSettingsSchema,
heading: z.string(),
preamble: z.string(),
experiences: z
.object({
destination_experiences: z.array(z.string()),
})
.transform(({ destination_experiences }) => destination_experiences),
images: z
.array(z.object({ image: tempImageVaultAssetSchema }))
.transform((images) =>
images
.map((image) => image.image)
.filter((image): image is ImageVaultAsset => !!image)
),
has_sidepeek: z.boolean().default(false),
sidepeek_button_text: z.string().default(""),
sidepeek_content: z.object({
heading: z.string(),
preamble: z.string(),
experiences: z
.object({
destination_experiences: z.array(z.string()),
})
.transform(({ destination_experiences }) => destination_experiences),
images: z
.array(z.object({ image: tempImageVaultAssetSchema }))
.transform((images) =>
images
.map((image) => image.image)
.filter((image): image is ImageVaultAsset => !!image)
),
has_sidepeek: z.boolean().default(false),
sidepeek_button_text: z.string().default(""),
sidepeek_content: z.object({
heading: z.string(),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
.transform((data) => {
const destinationCityPage = data.destination_city_page
const system = destinationCityPage.system
const trackingUrl = data.trackingProps.url
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: system.locale,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["destination-page"],
pageType: "staticcontentpage",
pageName: trackingUrl,
siteSections: trackingUrl,
siteVersion: "new-web",
}
return {
destinationCityPage,
tracking,
}
})
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
export const cityPageCountSchema = z
.object({

View File

@@ -23,6 +23,10 @@ import {
} from "./telemetry"
import { generatePageTags } from "./utils"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type {
GetDestinationCityPageData,
GetDestinationCityPageRefsSchema,
@@ -147,8 +151,8 @@ export const destinationCityPageQueryRouter = router({
)
return null
}
const cityIdentifier =
validatedResponse.data.destinationCityPage.destination_settings.city
const destinationCityPage = validatedResponse.data.destination_city_page
const cityIdentifier = destinationCityPage.destination_settings.city
const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
if (!city) {
@@ -177,10 +181,26 @@ export const destinationCityPageQueryRouter = router({
})
)
const system = destinationCityPage.system
const pageName = `hotels|${city.country ? `${city.country}|` : ""}${city.name}`
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: system.locale,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum.hotels,
pageType: "citypage",
pageName,
siteSections: pageName,
siteVersion: "new-web",
}
return {
...validatedResponse.data,
destinationCityPage,
cityIdentifier,
city,
tracking,
}
}),
})

View File

@@ -19,10 +19,6 @@ import {
import { systemSchema } from "../schemas/system"
import type { ImageVaultAsset } from "@/types/components/imageVault"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { Country } from "@/types/enums/country"
import { DestinationCountryPageEnum } from "@/types/enums/destinationCountryPage"
@@ -47,82 +43,60 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
destinationCountryPageContent,
])
export const destinationCountryPageSchema = z
.object({
destination_country_page: z.object({
title: z.string(),
destination_settings: z.object({
country: z.nativeEnum(Country),
location: mapLocationSchema,
}),
export const destinationCountryPageSchema = z.object({
destination_country_page: z.object({
title: z.string(),
destination_settings: z.object({
country: z.nativeEnum(Country),
location: mapLocationSchema,
}),
heading: z.string(),
preamble: z.string(),
experiences: z
.object({
destination_experiences: z.array(z.string()),
})
.transform(({ destination_experiences }) => destination_experiences),
images: z
.array(z.object({ image: tempImageVaultAssetSchema }))
.transform((images) =>
images
.map((image) => image.image)
.filter((image): image is ImageVaultAsset => !!image)
),
has_sidepeek: z.boolean().default(false),
sidepeek_button_text: z.string().default(""),
sidepeek_content: z.object({
heading: z.string(),
preamble: z.string(),
experiences: z
.object({
destination_experiences: z.array(z.string()),
})
.transform(({ destination_experiences }) => destination_experiences),
images: z
.array(z.object({ image: tempImageVaultAssetSchema }))
.transform((images) =>
images
.map((image) => image.image)
.filter((image): image is ImageVaultAsset => !!image)
),
has_sidepeek: z.boolean().default(false),
sidepeek_button_text: z.string().default(""),
sidepeek_content: z.object({
heading: z.string(),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
.transform((data) => {
const countryPageData = data.destination_country_page
const system = countryPageData.system
const trackingUrl = data.trackingProps.url
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: system.locale,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["destination-page"],
pageType: "staticcontentpage",
pageName: trackingUrl,
siteSections: trackingUrl,
siteVersion: "new-web",
}
return {
destinationCountryPage: countryPageData,
tracking,
}
})
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
export const countryPageUrlsSchema = z
.object({

View File

@@ -6,10 +6,9 @@ import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import {
contentStackBaseWithServiceProcedure,
contentStackUidWithServiceProcedure,
contentstackExtendedProcedureUID,
router,
} from "@/server/trpc"
import { toApiLang } from "@/server/utils"
import { generateTag } from "@/utils/generateTag"
@@ -28,6 +27,10 @@ import {
} from "./telemetry"
import { generatePageTags, getCityPages } from "./utils"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { ApiCountry } from "@/types/enums/country"
import type {
GetDestinationCountryPageData,
@@ -35,9 +38,8 @@ import type {
} from "@/types/trpc/routers/contentstack/destinationCountryPage"
export const destinationCountryPageQueryRouter = router({
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
const { lang, uid, serviceToken } = ctx
const apiLang = toApiLang(lang)
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const { lang, uid } = ctx
getDestinationCountryPageRefsCounter.add(1, { lang, uid })
console.info(
@@ -156,8 +158,9 @@ export const destinationCountryPageQueryRouter = router({
)
return null
}
const country =
validatedResponse.data.destinationCountryPage.destination_settings.country
const destinationCountryPage =
validatedResponse.data.destination_country_page
const country = destinationCountryPage.destination_settings.country
getDestinationCountryPageSuccessCounter.add(1, { lang, uid: `${uid}` })
console.info(
@@ -167,9 +170,25 @@ export const destinationCountryPageQueryRouter = router({
})
)
const system = destinationCountryPage.system
const pageName = `hotels|${country}`
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: system.locale,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum.hotels,
pageType: "countrypage",
pageName,
siteSections: pageName,
siteVersion: "new-web",
}
return {
...validatedResponse.data,
destinationCountryPage,
translatedCountry: ApiCountry[lang][country],
tracking,
}
}),
cityPages: contentStackBaseWithServiceProcedure

View File

@@ -189,10 +189,10 @@ export const destinationOverviewPageQueryRouter = router({
domainLanguage: lang,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["destination-overview-page"],
pageType: "staticcontentpage",
pageName: destinationOverviewPage.data.trackingProps.url,
siteSections: destinationOverviewPage.data.trackingProps.url,
channel: TrackingChannelEnum.hotels,
pageType: "destinationoverviewpage",
pageName: "destination|overview",
siteSections: "destination|overview",
siteVersion: "new-web",
}

View File

@@ -3,6 +3,10 @@ import { useContext } from "react"
import { create, useStore } from "zustand"
import { DestinationDataContext } from "@/contexts/DestinationData"
import {
trackFilterChangeEvent,
trackSortingChangeEvent,
} from "@/utils/tracking/destinationPage"
import {
getBasePathNameWithoutFilters,
@@ -50,6 +54,30 @@ export function createDestinationDataStore({
: []
const sortedCities = getSortedCities(filteredCities, newSort)
// Tracking
if (newSort !== state.activeSort) {
trackSortingChangeEvent(newSort)
}
if (
JSON.stringify(filters) !== JSON.stringify(state.activeFilters)
) {
const facilityFiltersUsed = filters.filter((f) =>
state.allFilters.facilityFilters
.map((ff) => ff.slug)
.includes(f)
)
const surroundingsFiltersUsed = filters.filter((f) =>
state.allFilters.surroundingsFilters
.map((sf) => sf.slug)
.includes(f)
)
trackFilterChangeEvent(
facilityFiltersUsed,
surroundingsFiltersUsed
)
}
state.activeSort = newSort
state.activeFilters = filters
state.activeHotels = sortedHotels

View File

@@ -6,8 +6,6 @@ export enum TrackingChannelEnum {
"static-content-page" = "static-content-page",
"hotelreservation" = "hotelreservation",
"collection-page" = "collection-page",
"destination-overview-page" = "destination-overview-page",
"destination-page" = "destination-page",
"hotels" = "hotels",
"start-page" = "start-page",
}

View File

@@ -13,7 +13,8 @@ export interface GetDestinationCityPageData
extends z.input<typeof destinationCityPageSchema> {}
export interface DestinationCityPage
extends z.output<typeof destinationCityPageSchema> {}
export type DestinationCityPageData = DestinationCityPage["destinationCityPage"]
export type DestinationCityPageData =
DestinationCityPage["destination_city_page"]
export interface GetDestinationCityListDataResponse
extends z.input<typeof destinationCityListDataSchema> {}

View File

@@ -12,7 +12,7 @@ export interface GetDestinationCountryPageData
interface DestinationCountryPage
extends z.output<typeof destinationCountryPageSchema> {}
export type DestinationCountryPageData =
DestinationCountryPage["destinationCountryPage"]
DestinationCountryPage["destination_country_page"]
export type Block = z.output<typeof blocksSchema>
export interface GetDestinationCountryPageRefsSchema

View File

@@ -1,198 +0,0 @@
import type {
LowestRoomPriceEvent,
PaymentEvent,
PaymentFailEvent,
TrackingPosition,
TrackingSDKData,
} from "@/types/components/tracking"
export function trackClick(
name: string,
additionalParams?: Record<string, string>
) {
trackEvent({
event: "linkClick",
cta: {
...additionalParams,
name,
},
})
}
export function trackPageViewStart() {
trackEvent({
event: "pageViewStart",
})
}
export function trackLoginClick(position: TrackingPosition) {
const event = {
event: "loginStart",
login: {
position,
action: "login start",
ctaName: "login",
},
}
trackEvent(event)
}
export function trackSocialMediaClick(socialMediaName: string) {
const event = {
event: "social media",
social: {
socialIconClicked: socialMediaName,
},
}
trackEvent(event)
}
export function trackFooterClick(group: string, name: string) {
const event = {
event: "footer link",
footer: {
footerLinkClicked: `${group}:${name}`,
},
}
trackEvent(event)
}
export function trackHotelMapClick() {
const event = {
event: "map click",
map: {
action: "map click - open/explore mearby",
},
}
trackEvent(event)
}
export function trackAccordionClick(option: string) {
const event = {
event: "accordionClick",
accordion: {
action: "accordion open click",
option,
},
}
trackEvent(event)
}
export function trackHotelTabClick(name: string) {
trackEvent({
event: "linkClick",
link: {
action: "hotel menu click",
option: `hotel menu:${name}`,
},
})
}
export function trackUpdatePaymentMethod(hotelId: string, method: string) {
const paymentSelectionEvent = {
event: "paymentSelection",
hotelInfo: {
hotelId: hotelId,
},
cta: {
name: method,
},
}
trackEvent(paymentSelectionEvent)
}
export function trackOpenSidePeekEvent(
name: string | null,
hotelId: string,
pathName?: string,
roomTypeCode?: string | null
) {
const openSidePeekEvent = {
event: "openSidePeek",
hotelInfo: {
hotelId: hotelId,
},
cta: {
name,
...(roomTypeCode ? { roomTypeCode } : {}),
...(pathName ? { pathName } : {}),
},
}
trackEvent(openSidePeekEvent)
}
export function trackPaymentEvent(paymentEvent: PaymentEvent) {
const paymentAttempt = {
event: paymentEvent.event,
hotelInfo: {
hotelId: paymentEvent.hotelId,
},
paymentInfo: {
isSavedCard: paymentEvent.isSavedCreditCard,
status: paymentEvent.status,
type: paymentEvent.method,
smsEnable: paymentEvent.smsEnable,
errorMessage: isPaymentFailEvent(paymentEvent)
? paymentEvent.errorMessage
: undefined,
},
}
trackEvent(paymentAttempt)
}
export function trackLowestRoomPrice(event: LowestRoomPriceEvent) {
const lowestRoomPrice = {
event: "lowestRoomPrice",
hotelInfo: {
hotelId: event.hotelId,
arrivalDate: event.arrivalDate,
departureDate: event.departureDate,
},
viewItemInfo: {
lowestPrice: event.lowestPrice,
currency: event.currency,
},
}
trackEvent(lowestRoomPrice)
}
function trackEvent(data: any) {
if (typeof window !== "undefined" && window.adobeDataLayer) {
data = { ...data, siteVersion: "new-web" }
window.adobeDataLayer.push(data)
}
}
export function trackPageView(data: any) {
if (typeof window !== "undefined" && window.adobeDataLayer) {
window.adobeDataLayer.push(data)
}
}
export function createSDKPageObject(
trackingData: TrackingSDKData
): TrackingSDKData {
let pageName = convertSlashToPipe(trackingData.pageName)
let siteSections = convertSlashToPipe(trackingData.siteSections)
if (trackingData.pathName.indexOf("/webview/") > -1) {
pageName = "webview|" + pageName
siteSections = "webview|" + siteSections
}
return {
...trackingData,
domain: typeof window !== "undefined" ? window.location.host : "",
pageName: pageName,
siteSections: siteSections,
}
}
function convertSlashToPipe(url: string) {
const formattedUrl = url.startsWith("/") ? url.slice(1) : url
return formattedUrl.replaceAll("/", "|")
}
function isPaymentFailEvent(event: PaymentEvent): event is PaymentFailEvent {
return "errorMessage" in event
}

View File

@@ -0,0 +1,19 @@
export function trackEvent(data: any) {
if (typeof window !== "undefined" && window.adobeDataLayer) {
data = { ...data, siteVersion: "new-web" }
window.adobeDataLayer.push(data)
}
}
export function trackClick(
name: string,
additionalParams?: Record<string, string>
) {
trackEvent({
event: "linkClick",
cta: {
...additionalParams,
name,
},
})
}

View File

@@ -0,0 +1,18 @@
import { trackEvent } from "./base"
import type { LowestRoomPriceEvent } from "@/types/components/tracking"
export function trackLowestRoomPrice(event: LowestRoomPriceEvent) {
trackEvent({
event: "lowestRoomPrice",
hotelInfo: {
hotelId: event.hotelId,
arrivalDate: event.arrivalDate,
departureDate: event.departureDate,
},
viewItemInfo: {
lowestPrice: event.lowestPrice,
currency: event.currency,
},
})
}

View File

@@ -0,0 +1,30 @@
import { trackEvent } from "./base"
export function trackAccordionClick(option: string) {
trackEvent({
event: "accordionClick",
accordion: {
action: "accordion open click",
option,
},
})
}
export function trackOpenSidePeekEvent(
name: string | null,
hotelId: string,
pathName?: string,
roomTypeCode?: string | null
) {
trackEvent({
event: "openSidePeek",
hotelInfo: {
hotelId: hotelId,
},
cta: {
name,
...(roomTypeCode ? { roomTypeCode } : {}),
...(pathName ? { pathName } : {}),
},
})
}

View File

@@ -0,0 +1,59 @@
import { trackEvent } from "./base"
export function trackSortingChangeEvent(sortOption: string) {
trackEvent({
event: "sortOptionClick",
filter: {
sortOptions: sortOption,
},
})
}
export function trackFilterChangeEvent(
facilityFilters: string[],
surroundingsFilters: string[]
) {
const filtersUsed = []
if (facilityFilters.length) {
filtersUsed.push(`hotelfacilities:${facilityFilters.join(",")}`)
}
if (surroundingsFilters.length) {
filtersUsed.push(`hotelsurroundings:${surroundingsFilters.join(",")}`)
}
trackEvent({
event: "filterUsed",
filter: {
filtersUsed: filtersUsed.join("|"),
},
})
}
export function trackOpenSidePeekOnDestinationPagesEvent(location: string) {
trackEvent({
event: "trackOpenSidePeekEvent",
cta: {
pageName: `explore${location}|sidepeek`,
},
})
}
export function trackMapClick(name: string) {
trackEvent({
event: "map click",
map: {
action: "map click",
clickedItemName: name,
},
})
}
export function trackOpenMapView(pageName: string, pageType: string) {
trackEvent({
event: "openMapView",
map: {
pageName,
pageType,
},
})
}

View File

@@ -0,0 +1,20 @@
import { trackEvent } from "./base"
export function trackHotelMapClick() {
trackEvent({
event: "map click",
map: {
action: "map click - open/explore mearby",
},
})
}
export function trackHotelTabClick(name: string) {
trackEvent({
event: "linkClick",
link: {
action: "hotel menu click",
option: `hotel menu:${name}`,
},
})
}

View File

@@ -0,0 +1,15 @@
export { trackClick } from "./base"
export { trackLowestRoomPrice } from "./booking"
export { trackAccordionClick, trackOpenSidePeekEvent } from "./componentEvents"
export { trackHotelMapClick, trackHotelTabClick } from "./hotelPage"
export {
trackFooterClick,
trackLoginClick,
trackSocialMediaClick,
} from "./navigation"
export {
createSDKPageObject,
trackPageView,
trackPageViewStart,
} from "./pageview"
export { trackPaymentEvent, trackUpdatePaymentMethod } from "./payment"

View File

@@ -0,0 +1,32 @@
import { trackEvent } from "./base"
import type { TrackingPosition } from "@/types/components/tracking"
export function trackFooterClick(group: string, name: string) {
trackEvent({
event: "footer link",
footer: {
footerLinkClicked: `${group}:${name}`,
},
})
}
export function trackSocialMediaClick(socialMediaName: string) {
trackEvent({
event: "social media",
social: {
socialIconClicked: socialMediaName,
},
})
}
export function trackLoginClick(position: TrackingPosition) {
trackEvent({
event: "loginStart",
login: {
position,
action: "login start",
ctaName: "login",
},
})
}

View File

@@ -0,0 +1,39 @@
import { trackEvent } from "./base"
import type { TrackingSDKData } from "@/types/components/tracking"
function convertSlashToPipe(url: string) {
const formattedUrl = url.startsWith("/") ? url.slice(1) : url
return formattedUrl.replaceAll("/", "|")
}
export function trackPageViewStart() {
trackEvent({
event: "pageViewStart",
})
}
export function trackPageView(data: any) {
if (typeof window !== "undefined" && window.adobeDataLayer) {
window.adobeDataLayer.push(data)
}
}
export function createSDKPageObject(
trackingData: TrackingSDKData
): TrackingSDKData {
let pageName = convertSlashToPipe(trackingData.pageName)
let siteSections = convertSlashToPipe(trackingData.siteSections)
if (trackingData.pathName.indexOf("/webview/") > -1) {
pageName = "webview|" + pageName
siteSections = "webview|" + siteSections
}
return {
...trackingData,
domain: typeof window !== "undefined" ? window.location.host : "",
pageName: pageName,
siteSections: siteSections,
}
}

View File

@@ -0,0 +1,38 @@
import { trackEvent } from "./base"
import type { PaymentEvent, PaymentFailEvent } from "@/types/components/tracking"
function isPaymentFailEvent(event: PaymentEvent): event is PaymentFailEvent {
return "errorMessage" in event
}
export function trackUpdatePaymentMethod(hotelId: string, method: string) {
trackEvent({
event: "paymentSelection",
hotelInfo: {
hotelId: hotelId,
},
cta: {
name: method,
},
})
}
export function trackPaymentEvent(paymentEvent: PaymentEvent) {
const paymentAttempt = {
event: paymentEvent.event,
hotelInfo: {
hotelId: paymentEvent.hotelId,
},
paymentInfo: {
isSavedCard: paymentEvent.isSavedCreditCard,
status: paymentEvent.status,
type: paymentEvent.method,
smsEnable: paymentEvent.smsEnable,
errorMessage: isPaymentFailEvent(paymentEvent)
? paymentEvent.errorMessage
: undefined,
},
}
trackEvent(paymentAttempt)
}