From c96008fb785003dcb022dd2548e4fb535ffb61ff Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Mon, 15 Jul 2024 09:13:20 +0200 Subject: [PATCH] fix: track user on page load --- .../(protected)/my-pages/[...path]/page.tsx | 4 +- .../my-pages/profile/edit/page.tsx | 13 +--- .../(protected)/my-pages/profile/page.tsx | 5 +- app/[lang]/webview/layout.tsx | 9 +++ auth.ts | 21 +++++- .../ContentType/LoyaltyPage/LoyaltyPage.tsx | 4 +- .../LoyaltyPage/loyaltyPage.module.css | 1 + components/Current/Header/LoginButton.tsx | 35 +++++++-- components/Current/Header/MainMenu/index.tsx | 39 ++++++++-- components/Current/Header/TopMenu/index.tsx | 9 ++- components/Current/TrackingSDK.tsx | 69 +----------------- .../Loyalty/Sidebar/JoinLoyalty/index.tsx | 13 ++-- components/TempDesignSystem/Link/index.tsx | 9 ++- constants/membershipLevels.ts | 2 + server/routers/user/query.ts | 72 +++++++++++++++++++ types/auth.d.ts | 3 + types/components/tracking.ts | 35 ++++----- utils/tracking.ts | 26 +++++++ 18 files changed, 247 insertions(+), 122 deletions(-) create mode 100644 utils/tracking.ts diff --git a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx index 3d3bb20e2..491a4e536 100644 --- a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx @@ -22,6 +22,8 @@ export default async function MyPages({ const accountPageTracking = await serverClient().contentstack.accountPage.tracking() + const userTrackingData = await serverClient().user.tracking() + return (
{accountPage.heading} @@ -30,7 +32,7 @@ export default async function MyPages({ ) : (

{formatMessage({ id: "No content published" })}

)} - +
) } diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/edit/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/edit/page.tsx index 6cd9cf94d..4ca8dd681 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/edit/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/edit/page.tsx @@ -1,12 +1,3 @@ -import "../profileLayout.css" +import ProfilePage from "../page" -import { serverClient } from "@/lib/trpc/server" - -import TrackingSDK from "@/components/Current/TrackingSDK" - -export default async function EditProfilePage() { - const accountPageTracking = - await serverClient().contentstack.accountPage.tracking() - - return -} +export default ProfilePage diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx index d548f8fc1..e21eee7c1 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx @@ -7,6 +7,9 @@ import TrackingSDK from "@/components/Current/TrackingSDK" export default async function ProfilePage() { const accountPageTracking = await serverClient().contentstack.accountPage.tracking() + const userTrackingData = await serverClient().user.tracking() - return + return ( + + ) } diff --git a/app/[lang]/webview/layout.tsx b/app/[lang]/webview/layout.tsx index eb7d7ed04..400f8cf32 100644 --- a/app/[lang]/webview/layout.tsx +++ b/app/[lang]/webview/layout.tsx @@ -1,8 +1,11 @@ import "@/app/globals.css" import "@scandic-hotels/design-system/style.css" +import Script from "next/script" + import TrpcProvider from "@/lib/trpc/Provider" +import AdobeSDKScript from "@/components/Current/AdobeSDKScript" import { getIntl } from "@/i18n" import ServerIntlProvider from "@/i18n/Provider" @@ -23,6 +26,12 @@ export default async function RootLayout({ const { defaultLocale, locale, messages } = await getIntl() return ( + + + + {children} diff --git a/auth.ts b/auth.ts index 5c75c9720..a24975032 100644 --- a/auth.ts +++ b/auth.ts @@ -2,9 +2,23 @@ import NextAuth from "next-auth" import { env } from "@/env/server" +import { LoginTypeEnum } from "./types/components/tracking" + import type { NextAuthConfig, User } from "next-auth" import type { OIDCConfig } from "next-auth/providers" +function getLoginType(user: User) { + if (user?.nonce) { + return LoginTypeEnum.MagicLink + } + + if (user?.login_with.includes("@")) { + return LoginTypeEnum.Email + } else { + return LoginTypeEnum.MembershipNumber + } +} + const customProvider = { clientId: env.CURITY_CLIENT_ID_USER, clientSecret: env.CURITY_CLIENT_SECRET_USER, @@ -39,6 +53,8 @@ const customProvider = { id: profile.id, sub: profile.sub, given_name: profile.given_name, + login_with: profile.login_with, + nonce: profile.nonce, } }, } satisfies OIDCConfig @@ -96,7 +112,8 @@ export const config = { async authorized({ auth, request }) { return true }, - async jwt({ account, session, token, trigger }) { + async jwt({ account, session, token, trigger, user }) { + const loginType = getLoginType(user) if (account) { return { access_token: account.access_token, @@ -104,6 +121,7 @@ export const config = { ? account.expires_at * 1000 : undefined, refresh_token: account.refresh_token, + loginType, } } else if (Date.now() < token.expires_at) { return token @@ -158,6 +176,7 @@ export const config = { access_token: new_tokens.access_token, expires_at: new_tokens.expires_at, refresh_token: new_tokens.refresh_token ?? token.refresh_token, + loginType, } } catch (error) { console.log("token-debug Error thrown when trying to refresh", { diff --git a/components/ContentType/LoyaltyPage/LoyaltyPage.tsx b/components/ContentType/LoyaltyPage/LoyaltyPage.tsx index c8230d818..7d1e8e43f 100644 --- a/components/ContentType/LoyaltyPage/LoyaltyPage.tsx +++ b/components/ContentType/LoyaltyPage/LoyaltyPage.tsx @@ -19,6 +19,8 @@ export default async function LoyaltyPage({ lang }: LangParams) { const loyaltyPageTracking = await serverClient().contentstack.loyaltyPage.tracking() + const userTracking = await serverClient().user.tracking() + return (
{loyaltyPage.sidebar.length ? ( @@ -31,7 +33,7 @@ export default async function LoyaltyPage({ lang }: LangParams) { ) : null} - +
) } diff --git a/components/ContentType/LoyaltyPage/loyaltyPage.module.css b/components/ContentType/LoyaltyPage/loyaltyPage.module.css index b20fa33a6..412944a27 100644 --- a/components/ContentType/LoyaltyPage/loyaltyPage.module.css +++ b/components/ContentType/LoyaltyPage/loyaltyPage.module.css @@ -27,6 +27,7 @@ .content:has(> aside) .blocks { grid-column: 2 / -1; + height: fit-content; } .blocks { diff --git a/components/Current/Header/LoginButton.tsx b/components/Current/Header/LoginButton.tsx index 46bdb6058..f8400ae77 100644 --- a/components/Current/Header/LoginButton.tsx +++ b/components/Current/Header/LoginButton.tsx @@ -1,30 +1,53 @@ "use client" import { usePathname } from "next/navigation" -import { useIntl } from "react-intl" +import { PropsWithChildren, useEffect } from "react" import { login } from "@/constants/routes/handleAuth" import Link from "@/components/TempDesignSystem/Link" +import { LinkProps } from "@/components/TempDesignSystem/Link/link" +import { trackLoginClick } from "@/utils/tracking" -import type { TrackableLoginId } from "@/types/components/tracking" +import { TrackingPosition } from "@/types/components/tracking" import { LangParams } from "@/types/params" export default function LoginButton({ className, + position, trackingId, lang, -}: LangParams & { className: string; trackingId: TrackableLoginId }) { - const { formatMessage } = useIntl() + children, + color = "black", +}: PropsWithChildren< + LangParams & { + className: string + trackingId: string + position: TrackingPosition + color?: LinkProps["color"] + } +>) { const pathName = usePathname() + useEffect(() => { + document + .getElementById(trackingId) + ?.addEventListener("click", () => trackLoginClick(position)) + return () => { + document + .getElementById(trackingId) + ?.removeEventListener("click", () => trackLoginClick(position)) + } + }, [position, trackingId]) + return ( - {formatMessage({ id: "Log in" })} + {children} ) } diff --git a/components/Current/Header/MainMenu/index.tsx b/components/Current/Header/MainMenu/index.tsx index 8d2532956..707a50253 100644 --- a/components/Current/Header/MainMenu/index.tsx +++ b/components/Current/Header/MainMenu/index.tsx @@ -8,6 +8,7 @@ import useDropdownStore from "@/stores/main-menu" import Image from "@/components/Image" import Avatar from "@/components/MyPages/Avatar" import Link from "@/components/TempDesignSystem/Link" +import { trackClick } from "@/utils/tracking" import BookingButton from "../BookingButton" import LoginButton from "../LoginButton" @@ -37,6 +38,11 @@ export function MainMenu({ toggleMyPagesMobileMenu, } = useDropdownStore() + function handleMyPagesMobileMenuClick() { + trackClick("profile picture icon") + toggleMyPagesMobileMenu() + } + return (
  • + > + {intl.formatMessage({ id: "Log in" })} +
  • )} @@ -118,9 +127,17 @@ export function MainMenu({ @@ -128,9 +145,17 @@ export function MainMenu({ @@ -159,7 +184,7 @@ export function MainMenu({ {myPagesMobileDropdown && user ? (
    toggleMyPagesMobileMenu()} + onClick={handleMyPagesMobileMenuClick} className={styles.avatarButton} > diff --git a/components/Current/Header/TopMenu/index.tsx b/components/Current/Header/TopMenu/index.tsx index b2850616b..dd1861267 100644 --- a/components/Current/Header/TopMenu/index.tsx +++ b/components/Current/Header/TopMenu/index.tsx @@ -65,10 +65,13 @@ export default async function TopMenu({ ) : ( + lang={lang} + > + {formatMessage({ id: "Log in" })} + )} diff --git a/components/Current/TrackingSDK.tsx b/components/Current/TrackingSDK.tsx index 1452f62db..eb3820456 100644 --- a/components/Current/TrackingSDK.tsx +++ b/components/Current/TrackingSDK.tsx @@ -5,8 +5,6 @@ import { useEffect } from "react" import { SiteSectionObject, - TrackableClickIdEnum, - TrackingPosition, TrackingSDKData, TrackingSDKProps, } from "@/types/components/tracking" @@ -63,7 +61,7 @@ function createSDKPageObject(trackingData: TrackingSDKData) { return page_obj } -export default function TrackingSDK({ pageData }: TrackingSDKProps) { +export default function TrackingSDK({ pageData, userData }: TrackingSDKProps) { const pathName = usePathname() function CookiebotCallbackOnAccept() { @@ -91,9 +89,9 @@ export default function TrackingSDK({ pageData }: TrackingSDKProps) { if (window.adobeDataLayer) { const trackingData = { ...pageData, pathName } const pageObject = createSDKPageObject(trackingData) - window.adobeDataLayer.push(pageObject) + window.adobeDataLayer.push({ ...pageObject, userInfo: userData }) } - }, [pathName, pageData]) + }, [pathName, pageData, userData]) useEffect(() => { // handle consent @@ -109,66 +107,5 @@ export default function TrackingSDK({ pageData }: TrackingSDKProps) { } }, []) - function loginClick(position: TrackingPosition) { - if (window.adobeDataLayer) { - const loginEvent = { - event: "loginStart", - login: { - position, - action: "login start", - ctaName: "login", - }, - } - window.adobeDataLayer.push(loginEvent) - } - } - - function linkClick(name: string) { - if (window.adobeDataLayer) { - window.adobeDataLayer.push({ - event: "linkClick", - cta: { - name: name, - }, - }) - } - } - - useEffect(() => { - // Handle clickable events - document - .getElementById(TrackableClickIdEnum.LoginStartTopMenu) - ?.addEventListener("click", () => loginClick("top menu")) - document - .getElementById(TrackableClickIdEnum.LoginStartJoinScandicFriends) - ?.addEventListener("click", () => - loginClick("join scandic friends sidebar") - ) - document - .getElementById(TrackableClickIdEnum.LoginStartHamburgerMenu) - ?.addEventListener("click", () => loginClick("hamburger menu")) - - document - .getElementById(TrackableClickIdEnum.ProfilePictureLink) - ?.addEventListener("click", () => linkClick("profile picture link")) - - return () => { - document - .getElementById(TrackableClickIdEnum.LoginStartTopMenu) - ?.removeEventListener("click", () => loginClick("top menu")) - document - .getElementById(TrackableClickIdEnum.LoginStartJoinScandicFriends) - ?.removeEventListener("click", () => - loginClick("join scandic friends sidebar") - ) - document - .getElementById(TrackableClickIdEnum.LoginStartHamburgerMenu) - ?.removeEventListener("click", () => loginClick("hamburger menu")) - document - .getElementById(TrackableClickIdEnum.ProfilePictureLink) - ?.removeEventListener("click", () => linkClick("profile picture link")) - } - }, []) - return null } diff --git a/components/Loyalty/Sidebar/JoinLoyalty/index.tsx b/components/Loyalty/Sidebar/JoinLoyalty/index.tsx index ed1be157b..0718abd31 100644 --- a/components/Loyalty/Sidebar/JoinLoyalty/index.tsx +++ b/components/Loyalty/Sidebar/JoinLoyalty/index.tsx @@ -1,5 +1,6 @@ import { serverClient } from "@/lib/trpc/server" +import LoginButton from "@/components/Current/Header/LoginButton" import ArrowRight from "@/components/Icons/ArrowRight" import { ScandicFriends } from "@/components/Levels" import Button from "@/components/TempDesignSystem/Button" @@ -13,7 +14,6 @@ import Contact from "./Contact" import styles from "./joinLoyalty.module.css" import type { JoinLoyaltyContactProps } from "@/types/components/loyalty/sidebar" -import { TrackableClickIdEnum } from "@/types/components/tracking" import { LangParams } from "@/types/params" export default async function JoinLoyaltyContact({ @@ -53,13 +53,12 @@ export default async function JoinLoyaltyContact({ ) : null}
    {formatMessage({ id: "Already a friend?" })} - {formatMessage({ id: "Log in here" })} - +
    {block.contact ? : null} diff --git a/components/TempDesignSystem/Link/index.tsx b/components/TempDesignSystem/Link/index.tsx index c23801014..885543bef 100644 --- a/components/TempDesignSystem/Link/index.tsx +++ b/components/TempDesignSystem/Link/index.tsx @@ -3,6 +3,8 @@ import NextLink from "next/link" import { usePathname } from "next/navigation" import { useEffect } from "react" +import { trackClick } from "@/utils/tracking" + import { linkVariants } from "./variants" import type { LinkProps } from "./link" @@ -36,11 +38,13 @@ export default function Link({ useEffect(() => { if (trackingId) { - document.getElementById(trackingId)?.addEventListener("click", () => {}) + document + .getElementById(trackingId) + ?.addEventListener("click", () => trackClick(trackingId)) return () => { document .getElementById(trackingId) - ?.removeEventListener("click", () => {}) + ?.removeEventListener("click", () => trackClick(trackingId)) } } }, [trackingId]) @@ -51,6 +55,7 @@ export default function Link({ prefetch={prefetch} className={classNames} href={href} + id={trackingId} {...props} /> ) diff --git a/constants/membershipLevels.ts b/constants/membershipLevels.ts index 2dda9d05c..3ae0f4650 100644 --- a/constants/membershipLevels.ts +++ b/constants/membershipLevels.ts @@ -17,3 +17,5 @@ export enum MembershipLevelEnum { L6 = "L6", L7 = "L7", } + +export type MembershipLevel = keyof typeof MembershipLevelEnum diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 8dc506be7..0fdcf5701 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -25,6 +25,11 @@ import { benefits, extendedUser, nextLevelPerks } from "./temp" import type { Session } from "next-auth" +import type { + LoginType, + TrackingSDKUserData, +} from "@/types/components/tracking" + async function getVerifiedUser({ session }: { session: Session }) { const apiResponse = await api.get(api.endpoints.v1.profile, { cache: "no-store", @@ -215,6 +220,73 @@ export const userQueryRouter = router({ const membershipLevel = getMembership(verifiedData.data.memberships) return membershipLevel }), + tracking: safeProtectedProcedure.query(async function ({ ctx }) { + const notLoggedInUserTrackingData: TrackingSDKUserData = { + loginStatus: false, + } + + if (!ctx.session) { + return notLoggedInUserTrackingData + } + const verifiedUserData = await getVerifiedUser({ session: ctx.session }) + + if (!verifiedUserData) { + return notLoggedInUserTrackingData + } + + const params = new URLSearchParams() + params.set("limit", "1") + + const previousStaysResponse = await api.get( + api.endpoints.v1.previousStays, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + }, + params + ) + + if (!previousStaysResponse.ok) { + // switch (apiResponse.status) { + // case 400: + // throw badRequestError(apiResponse) + // case 401: + // throw unauthorizedError(apiResponse) + // case 403: + // throw forbiddenError(apiResponse) + // default: + // throw internalServerError(apiResponse) + // } + console.info(`API Response Failed - Getting Previous Stays`) + console.info(`User: (${JSON.stringify(ctx.session.user)})`) + console.error(previousStaysResponse) + return notLoggedInUserTrackingData + } + + const previousStaysApiJson = await previousStaysResponse.json() + const verifiedPreviousStaysData = + getStaysSchema.safeParse(previousStaysApiJson) + if (!verifiedPreviousStaysData.success) { + console.info(`Failed to validate Previous Stays Data`) + console.info(`User: (${JSON.stringify(ctx.session.user)})`) + console.error(verifiedPreviousStaysData.error) + return notLoggedInUserTrackingData + } + + const membership = getMembership(verifiedUserData.data.memberships) + + const loggedInUserTrackingData: TrackingSDKUserData = { + loginStatus: true, + loginType: ctx.session.token.loginType as LoginType, + memberId: membership?.membershipNumber, + memberLevel: membership?.membershipLevel, + noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0, + totalPointsAvailableToSpend: membership?.currentPoints, + } + + return loggedInUserTrackingData + }), benefits: router({ current: protectedProcedure.query(async function (opts) { // TODO: Make request to get user data from Scandic API diff --git a/types/auth.d.ts b/types/auth.d.ts index 3cf582207..debb444a1 100644 --- a/types/auth.d.ts +++ b/types/auth.d.ts @@ -25,5 +25,8 @@ declare module "next-auth" { interface User { given_name: string sub: string + email?: string + login_with: string + nonce?: string } } diff --git a/types/components/tracking.ts b/types/components/tracking.ts index 2bd285d60..20718c132 100644 --- a/types/components/tracking.ts +++ b/types/components/tracking.ts @@ -1,3 +1,5 @@ +import { MembershipLevel } from "@/constants/membershipLevels" + import type { Lang } from "@/constants/languages" export enum TrackingChannelEnum { @@ -14,8 +16,25 @@ export type TrackingSDKPageData = { channel: TrackingChannel } +export enum LoginTypeEnum { + Email = "email", + MembershipNumber = "membership number", + MagicLink = "magic link", +} +export type LoginType = keyof typeof LoginTypeEnum + +export type TrackingSDKUserData = { + loginStatus: boolean + loginType?: LoginType + memberId?: string + memberLevel?: MembershipLevel + noOfNightsStayed?: number + totalPointsAvailableToSpend?: number +} + export type TrackingSDKProps = { pageData: TrackingSDKPageData + userData: TrackingSDKUserData } export type TrackingSDKData = { @@ -56,22 +75,6 @@ export type SiteSectionObject = { sitesection6: string } -export enum TrackableClickIdEnum { - LoginStartTopMenu = "LoginStartTopMenu", - LoginStartHamburgerMenu = "LoginStartHamburgerMenu", - LoginStartJoinScandicFriends = "LoginStartJoinScandicFriends", - LoginFail = "LoginFail", - HamburgerLink = "HamburgerLink", - ProfilePictureLink = "ProfilePictureLink", -} - -type TrackableClickId = keyof typeof TrackableClickIdEnum - -export type TrackableLoginId = Exclude< - TrackableClickId, - "HamburgerLink" | "ProfilePictureLink" | "LoginFail" -> - export type TrackingPosition = | "top menu" | "hamburger menu" diff --git a/utils/tracking.ts b/utils/tracking.ts new file mode 100644 index 000000000..d6bacd8a7 --- /dev/null +++ b/utils/tracking.ts @@ -0,0 +1,26 @@ +import { TrackingPosition } from "@/types/components/tracking" + +export function trackClick(name: string) { + if (window.adobeDataLayer) { + window.adobeDataLayer.push({ + event: "linkClick", + cta: { + name, + }, + }) + } +} + +export function trackLoginClick(position: TrackingPosition) { + if (window.adobeDataLayer) { + const loginEvent = { + event: "loginStart", + login: { + position, + action: "login start", + ctaName: "login", + }, + } + window.adobeDataLayer.push(loginEvent) + } +}