16 — Analytics and Tracking
PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-04
Related docs:15_Security_and_Data_Protection.md,04_Frontend_Architecture.md
1. Purpose
Section titled “1. Purpose”This document defines the analytics and tracking strategy for the Savoy Signature platform. It covers the Google Tag Manager (GTM) architecture, the dataLayer specification, consent-aware script loading via Cookiebot, and the events taxonomy used across all 8 sites.
2. Analytics Architecture
Section titled “2. Analytics Architecture”2.1 Stack
Section titled “2.1 Stack”| Layer | Tool | Purpose |
|---|---|---|
| Tag Management | Google Tag Manager (GTM) | Central hub for all marketing and analytics tags |
| Web Analytics | Google Analytics 4 (GA4) | Traffic, engagement, conversions |
| Consent Management | Cookiebot | GDPR-compliant consent gate for all tracking scripts |
| Server-Side | Azure Application Insights | Backend performance, API errors, server health |
2.2 Data Flow
Section titled “2.2 Data Flow”3. GTM Configuration
Section titled “3. GTM Configuration”3.1 Container Strategy — Global Default + Per-Hotel Override
Section titled “3.1 Container Strategy — Global Default + Per-Hotel Override”The platform supports a global default GTM container and GA4 Measurement ID, with the ability to override per hotel. Each hotel may have its own GTM container and/or its own independent GA4 property.
| Config Level | GTM Container ID | GA4 Measurement ID | Defined In |
|---|---|---|---|
| Global Default | GTM-XXXXXXX | G-XXXXXXX | Site Registry (packages/config/sites.ts) |
| Per-Hotel Override | GTM-YYYYYYY (optional) | G-YYYYYYY (optional) | Umbraco siteRoot properties |
Resolution Logic
Section titled “Resolution Logic”export function getAnalyticsConfig(siteConfig: SiteConfig): AnalyticsConfig { return { // Per-hotel override takes precedence over global default gtmId: siteConfig.gtmContainerId || process.env.NEXT_PUBLIC_GTM_ID || null, ga4Id: siteConfig.ga4MeasurementId || process.env.NEXT_PUBLIC_GA4_ID || null, };}Umbraco siteRoot Properties (Analytics Tab)
Section titled “Umbraco siteRoot Properties (Analytics Tab)”| Property | Type | Description |
|---|---|---|
gtmContainerId | Textstring | Hotel-specific GTM container ID. Leave empty to use global default. |
ga4MeasurementId | Textstring | Hotel-specific GA4 Measurement ID. Leave empty to use global default. |
enableGtm | Toggle | Enable/disable GTM for this site (default: true) |
enableGa4Direct | Toggle | Load GA4 gtag.js independently, outside GTM (default: false) |
[!IMPORTANT] GA4 can be loaded in two ways:
- Via GTM (default): GA4 is configured as a tag inside the GTM container. This is the recommended approach.
- Directly via
gtag.js(independent): WhenenableGa4Directistrue, the GA4 script is loaded directly in the page<head>, independently of GTM. This is useful when the hotel’s GA4 is not configured inside GTM.Both can coexist — a hotel may have GTM for marketing tags AND a separate direct GA4 for a different analytics property.
dataLayer Variables
Section titled “dataLayer Variables”| GTM Variable | Source | Value Example |
|---|---|---|
siteKey | dataLayer | savoy-palace |
locale | dataLayer | pt |
pageType | dataLayer | roomDetailPage |
environment | dataLayer | production |
3.2 Script Loading via Next.js
Section titled “3.2 Script Loading via Next.js”GTM is loaded with the afterInteractive strategy to avoid blocking LCP:
import Script from 'next/script';
export default function RootLayout({ children }) { const analytics = getAnalyticsConfig(siteConfig); const isProduction = process.env.APP_ENV === 'production';
return ( <html> <head> {/* Cookiebot must load BEFORE any tracking scripts */} <script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid={process.env.NEXT_PUBLIC_COOKIEBOT_ID} data-blockingmode="auto" type="text/javascript" />
{/* Direct GA4 (independent of GTM) — only if enabled for this hotel */} {isProduction && analytics.ga4Id && siteConfig.enableGa4Direct && ( <> <Script src={`https://www.googletagmanager.com/gtag/js?id=${analytics.ga4Id}`} strategy="afterInteractive" /> <Script id="ga4-config" strategy="afterInteractive"> {`window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '${analytics.ga4Id}');`} </Script> </> )} </head> <body> {children}
{/* GTM — uses per-hotel ID or global default */} {isProduction && analytics.gtmId && siteConfig.enableGtm !== false && ( <Script id="gtm" strategy="afterInteractive" dangerouslySetInnerHTML={{ __html: ` (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','${analytics.gtmId}'); `, }} /> )} </body> </html> );}[!IMPORTANT] GTM script must not load on non-production environments (DEV, STAGE, QA). Use
process.env.APP_ENV === 'production'to conditionally render the GTM<Script>tag.
4. dataLayer Specification
Section titled “4. dataLayer Specification”4.1 Page View Event (Auto-fired on every navigation)
Section titled “4.1 Page View Event (Auto-fired on every navigation)”// Pushed on every page render in the [locale]/layout.tsxwindow.dataLayer.push({ event: 'page_view', siteKey: 'savoy-palace', locale: 'pt', pageType: 'roomDetailPage', pagePath: '/pt/alojamento/quartos/deluxe-ocean', pageTitle: 'Deluxe Ocean | Savoy Palace', environment: 'production',});4.2 Custom Events Taxonomy
Section titled “4.2 Custom Events Taxonomy”| Event Name | Trigger | Key Parameters |
|---|---|---|
page_view | Every page navigation | siteKey, locale, pageType, pagePath, pageTitle |
booking_bar_open | User clicks “Book Now” or interacts with Booking Bar | siteKey, locale, variant (inline/sticky/overlay) |
booking_bar_submit | User submits dates → redirect to Synxis | siteKey, locale, checkIn, checkOut, guests, promoCode |
form_submit | Contact/enquiry form submitted successfully | siteKey, locale, formId, formName |
newsletter_signup | Newsletter form submitted successfully | siteKey, locale, email_domain (anonymized) |
gallery_open | User opens the lightbox gallery | siteKey, locale, galleryName |
cta_click | User clicks a CTA button (hero, card, etc.) | siteKey, locale, ctaLabel, ctaDestination |
language_switch | User changes language via switcher | siteKey, fromLocale, toLocale |
scroll_depth | User scrolls 25% / 50% / 75% / 100% | siteKey, locale, depth |
file_download | User downloads a PDF (menu, brochure) | siteKey, locale, fileName, fileType |
4.3 React Hook for Event Tracking
Section titled “4.3 React Hook for Event Tracking”'use client';
export function useTrackEvent() { return function trackEvent(eventName: string, params: Record<string, string | number>) { if (typeof window !== 'undefined' && window.dataLayer) { window.dataLayer.push({ event: eventName, ...params, }); } };}
// Usage:const trackEvent = useTrackEvent();trackEvent('booking_bar_submit', { siteKey, locale, checkIn, checkOut, guests: 2 });5. Consent-Aware Tracking
Section titled “5. Consent-Aware Tracking”All non-essential tracking is blocked until user opt-in via Cookiebot.
5.1 Cookie Categories
Section titled “5.1 Cookie Categories”| Category | Examples | Blocked Before Consent? |
|---|---|---|
| Necessary | Locale cookie, CSRF token, Cloudflare __cf_bm | ❌ No (always active) |
| Statistics | GA4, scroll depth, page_view events | ✅ Yes |
| Marketing | Meta Pixel, Google Ads remarketing | ✅ Yes |
| Preferences | Language preference cookie (if separate from locale URL) | ✅ Yes |
5.2 Consent Check in Code
Section titled “5.2 Consent Check in Code”// Only fire analytics event if user has consented to statistics cookiesif (window.Cookiebot?.consent?.statistics) { trackEvent('page_view', { ... });}6. Acceptance Criteria
Section titled “6. Acceptance Criteria”- GTM container loads only on production, after Cookiebot consent is initialized.
- Per-hotel GTM/GA4 overrides work correctly (hotel-specific ID takes precedence over global default).
- Direct GA4 (
gtag.js) loads independently whenenableGa4Directis enabled, even without GTM. -
page_viewevent fires on every client-side navigation with correctsiteKey,locale, andpageType. - All custom events defined in the taxonomy are implemented and testable in GTM Preview Mode.
- GA4 correctly receives events segmented by site property.
- No tracking cookies are set before user consent via Cookiebot.
-
dataLayeris populated server-side on initial render (for SSR pages) and client-side on subsequent navigations. - Meta Pixel and other marketing tags are gated behind “Marketing” consent category.
Next document: 17_AB_Testing_and_Experimentation.md