Skip to main content

Marketing Context Capsule

Table of Contents

  1. Overview
  2. Architecture
  3. Key Components
  4. Data Flow
  5. Important Concepts
  6. Usage Guide
  7. Best Practices
  8. Contact & Support

Overview

Purpose

The MarketingContextProvider is a React Context-based solution for managing marketing attribution data across Traveloka's web platform. It captures, stores, and provides access to marketing parameters such as UTM tags, campaign IDs, click IDs from various advertising platforms, and other tracking identifiers.

Key Responsibilities

  • Capture marketing parameters from URL query strings
  • Store marketing data persistently in browser storage
  • Sync marketing state across browser tabs
  • Rotate Marketing Context Cookie ID (MCC ID) for session tracking
  • Provide easy access to marketing data throughout the application

Location

  • Main File: packages/core/tracking/MarketingContext.tsx
  • Types: packages/core/tracking/external/types.ts
  • Tests: packages/core/tracking/__tests__/MarketingContext.test.tsx

Architecture

High-Level Design

┌─────────────────────────────────────────────────────────────┐
│ Browser Environment │
├─────────────────────────────────────────────────────────────┤
│ │
│ URL Query Params ──────────┐ │
│ (utm_id, fbclid, etc.) │ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ MarketingProvider │ │
│ │ (React Context) │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ │
│ │ Reducer │ │ Local Storage │ │ Cookie │ │
│ │ (State) │ │ (Marketing │ │ (MCC ID) │ │
│ │ │ │ Context) │ │ │ │
│ └──────────────┘ └────────────────┘ └──────────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Components via │ │
│ │ - useMarketing() │ │
│ │ - getMarketing() │ │
│ └───────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

State Management Flow

Initial Load:
1. Check ExpirableLocalStorage for existing marketing state
2. Parse URL query parameters for whitelisted keys
3. Initialize React state with merged data

URL Parameter Updates:
1. URL params detected in useEffect
2. Validate against WHITELISTED_MARKETING_KEYS
3. Check if values differ from current state
4. Update timestamp if needed
5. Dispatch BULK_UPDATE_WHITELISTED_VALUES action
6. Reducer updates state
7. State persisted to ExpirableLocalStorage
8. MCC ID rotated in cookie

Cross-Tab Sync:
1. Tab A updates marketing state
2. Tab A dispatches custom 'storage_change_event'
3. Tab B listens to both 'storage' and 'storage_change_event'
4. Tab B reloads from ExpirableLocalStorage
5. Tab B dispatches LOAD_FROM_STORAGE action
6. Tab B's state synchronized

Key Components

1. MarketingProvider

Location: Main export from packages/core/tracking/MarketingContext.tsx

Purpose: Top-level context provider that wraps the application

Key Features:

  • Initializes marketing state from storage
  • Monitors URL parameters for marketing keys
  • Syncs state across browser tabs
  • Rotates MCC ID cookie
  • Persists state to ExpirableLocalStorage

Usage:

import { MarketingProvider } from '@traveloka/core/tracking';

function App() {
return (
<MarketingProvider>
<YourApp />
</MarketingProvider>
);
}

2. useMarketing Hook

Purpose: Primary hook for accessing marketing data in components

Behavior:

  • Without key: Returns all marketing data as an object
  • With key: Returns the specific value for that key
  • Memoized: Uses useMemo to prevent unnecessary re-renders
  • Auto-enriched: Adds referrer_url, page_full_url, and client_user_agent

Examples:

// Get all marketing data
const marketingData = useMarketing();
// { utm_id: 'test123', fbclid: 'abc', ... }

// Get specific value
const utmSource = useMarketing('utm_source');
// 'google' or null

// Get campaign ID
const campaignId = useMarketing('campaign_id');
// '12345' or null

3. getMarketing Function

Purpose: Non-hook alternative for accessing marketing data

When to Use:

  • Outside React components
  • In utility functions
  • In API calls or middleware
  • Server-side rendering contexts

Examples:

// In a utility function
function buildAPIRequest() {
const marketingData = getMarketing();
return {
...otherData,
marketing: marketingData,
};
}

// Get specific value
const utmId = getMarketing('utm_id');

4. refreshMarketingState Function

Purpose: Programmatically update marketing state

When to Use:

  • When marketing data comes from sources other than URL params
  • When you need to bulk update multiple keys
  • When integrating with third-party tracking systems

Behavior:

  • Merges new state with existing state
  • Respects immutable keys (timestamp, initial_page_full_url, initial_timestamp)
  • Only updates if values actually changed
  • Updates timestamp if significant changes occurred
  • Dispatches storage change event for cross-tab sync

Example:

import { refreshMarketingState } from '@traveloka/core/tracking';

// Update marketing data from API response
refreshMarketingState({
utm_id: 'campaign_123',
campaign_id: 'summer_sale',
partner_id: 'partner_xyz',
});

Data Flow

1. Initial Page Load

User visits: https://www.traveloka.com/?utm_id=test123&fbclid=abc&utm_source=google

┌─ MarketingProvider Mount ───────────────────────────────────┐
│ │
│ 1. checkLocalStorage() │
│ ├─ Reads from ExpirableLocalStorage │
│ └─ Returns existing state or {} │
│ │
│ 2. useReducer initializes with state │
│ │
│ 3. First useEffect (URL params) │
│ ├─ Parse URL: { utm_id, fbclid, utm_source } │
│ ├─ Compare with current state │
│ ├─ Build updatedState = { utm_id, fbclid, utm_source } │
│ ├─ Set timestamp = Date.now() │
│ └─ Dispatch BULK_UPDATE_WHITELISTED_VALUES │
│ │
│ 4. Reducer processes action │
│ └─ state = { ...state, ...updatedState } │
│ │
│ 5. Second useEffect (persist & rotate MCC) │
│ ├─ Detect state changed (timestamp differs) │
│ ├─ Rotate MCC ID cookie │
│ └─ ExpirableLocalStorage.set(state) │
│ │
│ 6. Third useEffect (cross-tab sync listener) │
│ └─ Add event listeners │
│ │
└─────────────────────────────────────────────────────────────┘

Result: State available to all components via useMarketing()

2. URL Parameter Update (Same Session)

User navigates: https://www.traveloka.com/flight?utm_campaign=flash_sale

┌─ URL Change Detection ────────────────────────────────────┐
│ │
│ 1. First useEffect does NOT run (runs only on mount) │
│ │
│ 2. State persists from previous page │
│ Current state: { utm_id, fbclid, utm_source } │
│ │
│ 3. New page has different URL params │
│ ├─ Must use refreshMarketingState() │
│ └─ Or rely on initial page load capture │
│ │
└───────────────────────────────────────────────────────────┘

3. Cross-Tab Synchronization

Tab A: User updates marketing state
Tab B: Needs to sync

┌─ Tab A ────────────────────────────────────────────────────┐
│ │
│ 1. refreshMarketingState({ utm_id: 'new_value' }) │
│ ├─ Merge with current state │
│ ├─ Update timestamp │
│ ├─ ExpirableLocalStorage.set() │
│ └─ window.dispatchEvent(STORAGE_CHANGE_EVENT) │
│ │
└────────────────────────────────────────────────────────────┘

┌─ Tab B ────────────────────────────────────────────────────┐
│ │
│ 1. Event listener catches STORAGE_CHANGE_EVENT │
│ │
│ 2. checkLocalStorageChange() │
│ ├─ Read new state from ExpirableLocalStorage │
│ ├─ Compare with current state │
│ └─ Different? Dispatch LOAD_FROM_STORAGE │
│ │
│ 3. Reducer updates state │
│ │
│ 4. Components re-render with new data │
│ │
└────────────────────────────────────────────────────────────┘

4. MCC ID Rotation

Marketing Context Cookie (MCC) ID Lifecycle

┌─ Rotation Logic ───────────────────────────────────────────┐
│ │
│ Conditions for rotation: │
│ 1. No existing MCC ID cookie │
│ 2. Marketing state changed │
│ │
│ Process: │
│ 1. Generate new ULID │
│ 2. Set cookie with 30-minute expiration │
│ 3. Store reference in currentMccId.current │
│ │
│ Cookie Details: │
│ - Name: cookieCmnMarketingContextId │
│ - Lifetime: 30 minutes (MARKETING_CONTEXT_LIFETIME) │
│ - Generated using: ULID |
│ │
└────────────────────────────────────────────────────────────┘

Important Concepts

1. Whitelisted Marketing Keys

Only specific keys are tracked to prevent data bloat and ensure data quality:

const WHITELISTED_MARKETING_KEYS = [
// Campaign identifiers
'utm_id', // UTM Campaign ID
'campaign_id', // Generic campaign ID
'metasearchId', // Metasearch campaign ID

// Click IDs (various platforms)
'fbclid', // Facebook Click ID
'ttoclid', // TikTok Click ID (old)
'ttclid', // TikTok Click ID
'gclid', // Google Click ID
'gbraid', // Google Brand ID
'wbraid', // Google Web to App Brand ID
'yclid', // Yahoo Japan Click ID
'clickref', // Generic click reference

// Analytics IDs
'amplitude_session_id', // Amplitude session
'amplitude_device_id', // Amplitude device
'ga_session_id', // Google Analytics session
'ga_client_id', // Google Analytics client

// Facebook tracking
'fb_browser_id_fbp', // Facebook Browser ID
'fb_click_id_fbc', // Facebook Click ID

// Affiliate tracking
'involve_asia_click_id', // Involve Asia
'impactradius_click_id', // Impact Radius
'irclickid', // Impact Radius Click ID
'aff_sid', // Affiliate SID
'skyscanner_redirectid', // Skyscanner

// Page tracking
'referrer_url', // HTTP Referrer
'page_full_url', // Current page URL
'client_user_agent', // Browser user agent

// Coupons and promotions
'couponCode', // Coupon code
'limit_id', // Limit/restriction ID

// Partners
'partner_id', // Partner identifier
'adref', // Ad reference
'sc', // Source code
'cid', // Campaign/Content ID

// Internal tracking
'timestamp', // Last update time
'initial_page_full_url', // First page URL
'initial_timestamp', // First capture time

// UI control
'hide-appbanner', // Hide app banner flag
];

2. Immutable Keys

Certain keys cannot be manually updated to preserve data integrity:

const IMMUTABLE_WHITELISTED_KEYS = [
'timestamp', // Automatically managed
'initial_page_full_url', // Set once on first capture
'initial_timestamp', // Set once on first capture
];

Why Immutable?

  • timestamp: System-managed, reflects last state change
  • initial_*: Historical record of user's entry point

3. Storage Lifetime

const MARKETING_CONTEXT_LIFETIME = 30 * 60 * 1000; // 30 minutes

Rationale:

  • Marketing attribution typically valid for 30 minutes
  • Prevents stale data from affecting new sessions
  • Aligns with typical user session duration
  • Balances attribution accuracy with user privacy

4. Auto-Enrichment

When accessing marketing data, certain fields are automatically added:

// Automatically included when calling useMarketing() or getMarketing()
{
referrer_url: document.referrer, // HTTP Referrer
page_full_url: window.location.href, // Current URL
client_user_agent: navigator.userAgent, // Browser UA
}

Note: Localhost values are filtered out to prevent WAF errors in backend.

5. State Synchronization

Two event types enable cross-tab sync:

  1. storage event (native browser event)

    • Fired when localStorage changes in another tab
    • Standard browser API
  2. storage_change_event event (custom event)

    • Fired by refreshMarketingState()
    • Enables same-tab updates

  • Name: cookieCmnMarketingContextId
  • Purpose: Unique identifier for marketing context sessions
  • Generation: ULID format for uniqueness and time-ordering
  • Rotation: Every time marketing state changes or on first set

Usage Guide

Basic Setup

// app.tsx or _app.tsx
import { MarketingProvider } from '@traveloka/core/tracking';

export default function App({ Component, pageProps }) {
return (
<MarketingProvider>
<Component {...pageProps} />
</MarketingProvider>
);
}

Reading Marketing Data in Components

import { useMarketing } from '@traveloka/core/tracking';

function MyComponent() {
// Get all marketing data
const allMarketing = useMarketing();

// Get specific values
const utmId = useMarketing('utm_id');
const campaignId = useMarketing('campaign_id');
const fbclid = useMarketing('fbclid');

return (
<div>
<p>UTM ID: {utmId || 'Not set'}</p>
<p>Campaign: {campaignId || 'Not set'}</p>
<p>Facebook Click ID: {fbclid || 'Not set'}</p>
</div>
);
}

Passing Marketing Data to API

import { getMarketing } from '@traveloka/core/tracking';

async function createBooking(bookingData) {
const marketingContext = getMarketing();

const response = await fetch('/api/bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...bookingData,
marketing: marketingContext,
}),
});

return response.json();
}

Updating Marketing State Programmatically

import { refreshMarketingState } from '@traveloka/core/tracking';

function handleExternalTrackingData(trackingData) {
// Update marketing state from external source
refreshMarketingState({
campaign_id: trackingData.campaignId,
partner_id: trackingData.partnerId,
utm_id: trackingData.utmId,
});
}

Best Practices

1. Use Hooks in Components, Functions Elsewhere

// ✅ Good: Hook in component
function MyComponent() {
const marketing = useMarketing();
return <div>{marketing.utm_id}</div>;
}

// ✅ Good: Function outside component
function sendToAPI() {
const marketing = getMarketing();
return fetch('/api', { body: JSON.stringify({ marketing }) });
}

// ❌ Bad: Hook outside component
const marketing = useMarketing(); // Error: not in component
function sendToAPI() { ... }

2. Avoid Excessive Re-renders

// ✅ Good: Get specific values
const utmId = useMarketing('utm_id');
const campaignId = useMarketing('campaign_id');

// ⚠️ Acceptable: Get all data if you need it
const allMarketing = useMarketing();

// ❌ Bad: Getting all data when you only need one value
const allMarketing = useMarketing();
const utmId = allMarketing.utm_id; // Unnecessary re-renders

3. Handle Null Values

// ✅ Good: Handle null explicitly
const utmId = useMarketing('utm_id');
const displayValue = utmId ?? 'No campaign';

// ✅ Good: Conditional rendering
if (utmId) {
return <CampaignBanner id={utmId} />;
}

// ❌ Bad: Assuming value exists
const utmId = useMarketing('utm_id');
const uppercased = utmId.toUpperCase(); // Error if null

4. Don't Modify State Directly

// ❌ Bad: Direct state modification
const marketing = useMarketing();
marketing.utm_id = 'new_value'; // Won't work!

// ✅ Good: Use refreshMarketingState
refreshMarketingState({ utm_id: 'new_value' });

Contact & Support

For questions or issues related to MarketingContextProvider:

  1. Check this documentation first
  2. Review the test file for usage examples
  3. Reach out to @traveloka/content-frontend-web for support

End of MarketingContextProvider Technical Handover Documentation