Merged in fix/tracking-pageload-issues (pull request #1630)

fix(tracking): fixes not sending pageview events when promise isn't resolving

* fix(tracking): fixes not sending pageview events when promise isn't resolving

* Refactor


Approved-by: Anton Gunnarsson
This commit is contained in:
Linus Flood
2025-03-25 12:33:04 +00:00
parent 53a610913a
commit 4cd6e7b55b
3 changed files with 105 additions and 71 deletions

View File

@@ -13,6 +13,7 @@ import useRouterTransitionStore from "@/stores/router-transition"
import useTrackingStore from "@/stores/tracking" import useTrackingStore from "@/stores/tracking"
import { useSessionId } from "@/hooks/useSessionId" import { useSessionId } from "@/hooks/useSessionId"
import { promiseWithTimeout } from "@/utils/promiseWithTimeout"
import { createSDKPageObject, trackPageView } from "@/utils/tracking" import { createSDKPageObject, trackPageView } from "@/utils/tracking"
import type { TrackingSDKProps } from "@/types/components/tracking" import type { TrackingSDKProps } from "@/types/components/tracking"
@@ -52,7 +53,7 @@ export default function RouterTransition({
hasRunInitial.current = true hasRunInitial.current = true
setHasRun() setHasRun()
const getPageLoadTime = () => { const getPageLoadTimeEntry = () => {
return new Promise<number>((resolve) => { return new Promise<number>((resolve) => {
const observer = new PerformanceObserver((entries) => { const observer = new PerformanceObserver((entries) => {
const navEntry = entries.getEntriesByType("navigation")[0] const navEntry = entries.getEntriesByType("navigation")[0]
@@ -65,23 +66,36 @@ export default function RouterTransition({
}) })
} }
const getLCPTime = () => { const getLCPTimeEntry = () => {
return new Promise<number>((resolve) => { return new Promise<number | undefined>((resolve) => {
const observer = new PerformanceObserver((entries) => { const observer = new PerformanceObserver((entries) => {
const lastEntry = entries.getEntries().pop() const lastEntry = entries.getEntries().at(-1)
if (lastEntry) { if (lastEntry) {
observer.disconnect() observer.disconnect()
resolve(lastEntry.startTime / 1000) resolve(lastEntry.startTime / 1000)
} }
}) })
observer.observe({ type: "largest-contentful-paint", buffered: true })
const lcpSupported =
PerformanceObserver.supportedEntryTypes?.includes(
"largest-contentful-paint"
)
if (lcpSupported) {
observer.observe({
type: "largest-contentful-paint",
buffered: true,
})
} else {
resolve(undefined)
}
}) })
} }
const trackPerformance = async () => { const trackPerformance = async () => {
const [pageLoadTime, lcpTime] = await Promise.all([ const [pageLoadTime, lcpTime] = await Promise.all([
getPageLoadTime(), promiseWithTimeout(getPageLoadTimeEntry(), 3000),
getLCPTime(), promiseWithTimeout(getLCPTimeEntry(), 3000),
]) ])
const trackingData = { const trackingData = {

View File

@@ -367,79 +367,87 @@ export const userQueryRouter = router({
if (!isValidSession(ctx.session)) { if (!isValidSession(ctx.session)) {
return notLoggedInUserTrackingData return notLoggedInUserTrackingData
} }
const verifiedUserData = await getVerifiedUser({ session: ctx.session })
if (!verifiedUserData || "error" in verifiedUserData) { let verifiedUserData
return notLoggedInUserTrackingData
}
const params = new URLSearchParams() try {
params.set("limit", "1") verifiedUserData = await getVerifiedUser({ session: ctx.session })
getPreviousStaysCounter.add(1, { query: JSON.stringify({ params }) })
console.info( if (!verifiedUserData || "error" in verifiedUserData) {
"api.booking.stays.past start", return notLoggedInUserTrackingData
JSON.stringify({ query: { params } }) }
)
const previousStaysResponse = await api.get( const params = new URLSearchParams()
api.endpoints.v1.Booking.Stays.past, params.set("limit", "1")
{ getPreviousStaysCounter.add(1, { query: JSON.stringify({ params }) })
headers: { console.info(
Authorization: `Bearer ${ctx.session.token.access_token}`, "api.booking.stays.past start",
JSON.stringify({ query: { params } })
)
const previousStaysResponse = await api.get(
api.endpoints.v1.Booking.Stays.past,
{
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
}, },
}, params
params )
)
if (!previousStaysResponse.ok) { if (!previousStaysResponse.ok) {
getPreviousStaysFailCounter.add(1, { getPreviousStaysFailCounter.add(1, {
error_type: "http_error", error_type: "http_error",
error: JSON.stringify({ error: JSON.stringify({
status: previousStaysResponse.status,
statusText: previousStaysResponse.statusText,
}),
})
console.error(
"api.booking.stays.past error",
JSON.stringify({
error: {
status: previousStaysResponse.status, status: previousStaysResponse.status,
statusText: previousStaysResponse.statusText, statusText: previousStaysResponse.statusText,
}, }),
}) })
) console.error(
"api.booking.stays.past error",
JSON.stringify({
error: {
status: previousStaysResponse.status,
statusText: previousStaysResponse.statusText,
},
})
)
return notLoggedInUserTrackingData
}
const previousStaysApiJson = await previousStaysResponse.json()
const verifiedPreviousStaysData =
getStaysSchema.safeParse(previousStaysApiJson)
if (!verifiedPreviousStaysData.success) {
getPreviousStaysFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedPreviousStaysData.error),
})
console.error(
"api.booking.stays.past validation error, ",
JSON.stringify({ error: verifiedPreviousStaysData.error })
)
return notLoggedInUserTrackingData
}
getPreviousStaysSuccessCounter.add(1)
console.info("api.booking.stays.past success", JSON.stringify({}))
const membership = getFriendsMembership(verifiedUserData.data.memberships)
const loggedInUserTrackingData: TrackingSDKUserData = {
loginStatus: "logged in",
loginType: ctx.session.token.loginType as LoginType,
memberId: verifiedUserData.data.profileId,
membershipNumber: membership?.membershipNumber,
memberLevel: membership?.membershipLevel as MembershipLevel,
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
totalPointsAvailableToSpend: membership?.currentPoints,
loginAction: "login success",
}
return loggedInUserTrackingData
} catch (error) {
console.error("Error in userTrackingInfo:", error)
return notLoggedInUserTrackingData return notLoggedInUserTrackingData
} }
const previousStaysApiJson = await previousStaysResponse.json()
const verifiedPreviousStaysData =
getStaysSchema.safeParse(previousStaysApiJson)
if (!verifiedPreviousStaysData.success) {
getPreviousStaysFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedPreviousStaysData.error),
})
console.error(
"api.booking.stays.past validation error, ",
JSON.stringify({ error: verifiedPreviousStaysData.error })
)
return notLoggedInUserTrackingData
}
getPreviousStaysSuccessCounter.add(1)
console.info("api.booking.stays.past success", JSON.stringify({}))
const membership = getFriendsMembership(verifiedUserData.data.memberships)
const loggedInUserTrackingData: TrackingSDKUserData = {
loginStatus: "logged in",
loginType: ctx.session.token.loginType as LoginType,
memberId: verifiedUserData.data.profileId,
membershipNumber: membership?.membershipNumber,
memberLevel: membership?.membershipLevel as MembershipLevel,
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
totalPointsAvailableToSpend: membership?.currentPoints,
loginAction: "login success",
}
return loggedInUserTrackingData
}), }),
stays: router({ stays: router({

View File

@@ -0,0 +1,12 @@
export const promiseWithTimeout = <T>(
promise: Promise<T>,
timeoutMs: number,
fallbackValue: T | undefined = undefined
) => {
return Promise.race([
promise,
new Promise<T | undefined>((resolve) =>
setTimeout(() => resolve(fallbackValue), timeoutMs)
),
])
}