Marketing Context Capsule
Table of Contents
- Overview
- Architecture
- Key Components
- Data Flow
- Important Concepts
- Usage Guide
- Best Practices
- 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
useMemoto prevent unnecessary re-renders - Auto-enriched: Adds
referrer_url,page_full_url, andclient_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 changeinitial_*: 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:
-
storageevent (native browser event)- Fired when localStorage changes in another tab
- Standard browser API
-
storage_change_eventevent (custom event)- Fired by
refreshMarketingState() - Enables same-tab updates
- Fired by
6. MCC ID Cookie
- 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:
- Check this documentation first
- Review the test file for usage examples
- Reach out to @traveloka/content-frontend-web for support
End of MarketingContextProvider Technical Handover Documentation