Overview
While Amplitude doesn't use a traditional data layer like Google Tag Manager, establishing a structured data architecture is essential for consistent, maintainable tracking. A well-designed data model ensures event properties are standardized, user attributes are enriched properly, and downstream analysis remains reliable as your implementation scales.
This guide covers how to prepare, structure, and govern data before sending it to Amplitude.
Data Architecture Principles
Event-Driven Design
Amplitude is built on an event-based model where each user action is captured as a discrete event with associated properties:
amplitude.track('product_viewed', {
product_id: 'SKU-12345',
product_name: 'Wireless Headphones',
category: 'Electronics',
price: 79.99,
currency: 'USD',
view_source: 'search_results'
});
User Properties vs. Event Properties
- User Properties: Attributes that describe the user (plan type, signup date, region)
- Event Properties: Contextual data specific to the event (product details, interaction type)
// Set user properties (persist across events)
amplitude.setUserProperties({
plan: 'premium',
signup_date: '2024-01-15',
account_type: 'business',
region: 'US-West'
});
// Track event with event properties (specific to this action)
amplitude.track('purchase_completed', {
order_id: 'ORD-98765',
total_amount: 156.99,
item_count: 3,
payment_method: 'credit_card'
});
Required Data Fields
Attribution Data
Capture marketing attribution on initial page load:
// Extract UTM parameters from URL
const urlParams = new URLSearchParams(window.location.search);
const utmData = {
utm_source: urlParams.get('utm_source'),
utm_medium: urlParams.get('utm_medium'),
utm_campaign: urlParams.get('utm_campaign'),
utm_content: urlParams.get('utm_content'),
utm_term: urlParams.get('utm_term'),
referrer: document.referrer,
initial_referrer: document.referrer
};
// Store as user properties for attribution
amplitude.setUserProperties(utmData);
// Also attach to first event
amplitude.track('page_viewed', {
...utmData,
page_path: window.location.pathname,
page_title: document.title
});
Device and Session Information
Amplitude automatically captures device data, but you can enrich it:
amplitude.init('YOUR_API_KEY', null, {
includeReferrer: true,
includeUtm: true,
deviceId: getStoredDeviceId(), // Restore from storage if needed
sessionTimeout: 30 * 60 * 1000 // 30 minutes
});
// Add custom device/session context
const identify = new amplitude.Identify()
.set('browser_language', navigator.language)
.set('screen_resolution', `${screen.width}x${screen.height}`)
.set('viewport_size', `${window.innerWidth}x${window.innerHeight}`);
amplitude.identify(identify);
User Identity Data
Set user ID upon authentication:
// On login
function handleUserLogin(userData) {
amplitude.setUserId(userData.user_id);
const identify = new amplitude.Identify()
.set('email', userData.email)
.set('name', userData.name)
.set('plan', userData.subscription_plan)
.set('signup_date', userData.created_at)
.set('account_id', userData.account_id);
amplitude.identify(identify);
amplitude.track('user_logged_in', {
login_method: 'email_password'
});
}
Ecommerce Data Schema
Define a consistent structure for commerce events:
Product Object
const productData = {
product_id: 'SKU-12345',
product_name: 'Wireless Headphones',
sku: 'SKU-12345',
category: 'Electronics',
subcategory: 'Audio',
brand: 'AudioTech',
variant: 'Black',
price: 79.99,
currency: 'USD',
quantity: 1,
position: 0, // In list
list_name: 'Search Results'
};
amplitude.track('product_viewed', productData);
Cart Operations
// Add to cart
amplitude.track('product_added_to_cart', {
product_id: 'SKU-12345',
product_name: 'Wireless Headphones',
price: 79.99,
quantity: 1,
currency: 'USD',
cart_total: 79.99,
cart_item_count: 1
});
// Cart viewed
amplitude.track('cart_viewed', {
cart_total: 142.97,
cart_item_count: 3,
currency: 'USD',
cart_items: ['SKU-12345', 'SKU-67890', 'SKU-11111']
});
Checkout Flow
// Begin checkout
amplitude.track('checkout_started', {
cart_total: 142.97,
cart_item_count: 3,
currency: 'USD',
coupon_code: 'SUMMER10',
discount_amount: 14.30
});
// Add shipping info
amplitude.track('shipping_info_added', {
shipping_method: 'ground',
shipping_cost: 5.99,
estimated_delivery: '2024-02-15'
});
// Add payment info
amplitude.track('payment_info_added', {
payment_method: 'credit_card',
payment_type: 'Visa'
});
Purchase Confirmation
// Revenue tracking
const revenue = new amplitude.Revenue()
.setProductId('SKU-12345')
.setPrice(79.99)
.setQuantity(1)
.setRevenueType('purchase')
.setEventProperties({
order_id: 'ORD-98765',
category: 'Electronics'
});
amplitude.logRevenueV2(revenue);
// Purchase event
amplitude.track('purchase_completed', {
order_id: 'ORD-98765',
total_amount: 156.96,
subtotal: 142.97,
tax: 8.00,
shipping: 5.99,
discount: 14.30,
currency: 'USD',
coupon_code: 'SUMMER10',
item_count: 3,
payment_method: 'credit_card',
items: [
{
product_id: 'SKU-12345',
product_name: 'Wireless Headphones',
price: 79.99,
quantity: 1
},
{
product_id: 'SKU-67890',
product_name: 'USB-C Cable',
price: 12.99,
quantity: 2
}
]
});
Data Layer Abstraction Pattern
Centralized Data Management
Create a data layer abstraction to normalize data before sending to Amplitude:
// dataLayer.js
class AnalyticsDataLayer {
constructor() {
this.pageData = {};
this.userData = {};
this.sessionData = {};
}
setPageData(data) {
this.pageData = { ...this.pageData, ...data };
}
setUserData(data) {
this.userData = { ...this.userData, ...data };
// Sync to Amplitude
const identify = new amplitude.Identify();
Object.entries(data).forEach(([key, value]) => {
identify.set(key, value);
});
amplitude.identify(identify);
}
trackEvent(eventName, properties = {}) {
// Merge all context
const enrichedProperties = {
...this.sessionData,
...this.pageData,
...properties,
timestamp: new Date().toISOString()
};
amplitude.track(eventName, enrichedProperties);
}
trackPageView() {
this.trackEvent('page_viewed', {
page_path: window.location.pathname,
page_title: document.title,
page_url: window.location.href,
referrer: document.referrer
});
}
}
// Initialize
window.dataLayer = new AnalyticsDataLayer();
Usage Example
// Set page context
dataLayer.setPageData({
page_type: 'product',
category: 'electronics',
language: 'en'
});
// Set user context
dataLayer.setUserData({
plan: 'premium',
account_id: 'ACC-123'
});
// Track events with automatic enrichment
dataLayer.trackEvent('button_clicked', {
button_name: 'add_to_cart',
product_id: 'SKU-12345'
});
Property Naming Conventions
Naming Standards
- Use
snake_casefor all property names - Be descriptive but concise:
product_idnotprod_id - Use consistent terminology:
user_ideverywhere, notuserIdoruid - Prefix related properties:
utm_source,utm_medium,utm_campaign
Property Type Consistency
Maintain consistent data types for properties:
| Property | Type | Example | Notes |
|---|---|---|---|
product_id |
string | "SKU-12345" |
Always string, never number |
price |
number | 79.99 |
Always number, never string |
quantity |
number | 1 |
Integer |
currency |
string | "USD" |
ISO 4217 format |
timestamp |
string | "2024-01-15T10:30:00Z" |
ISO 8601 format |
is_premium |
boolean | true |
Never strings like "true" |
Reserved Property Names
Avoid using Amplitude's reserved properties:
user_iddevice_idevent_idsession_idtimeplatformos_nameos_versiondevice_branddevice_manufacturerdevice_model
User Properties Management
Setting Properties
// Set property
const identify = new amplitude.Identify().set('plan', 'premium');
amplitude.identify(identify);
// Set once (only if not already set)
const identifyOnce = new amplitude.Identify().setOnce('first_visit_date', '2024-01-15');
amplitude.identify(identifyOnce);
// Append to array
const identifyAppend = new amplitude.Identify().append('purchased_products', 'SKU-12345');
amplitude.identify(identifyAppend);
// Increment numeric value
const identifyIncrement = new amplitude.Identify().add('purchase_count', 1);
amplitude.identify(identifyIncrement);
User Property Schema
Define core user properties:
| Property | Type | Description | Example |
|---|---|---|---|
user_id |
string | Unique user identifier | "user_12345" |
email |
string | User email (hashed if needed) | "user@example.com" |
plan |
string | Subscription plan | "premium", "free" |
signup_date |
string | Account creation date | "2024-01-15" |
account_type |
string | Account classification | "individual", "business" |
region |
string | Geographic region | "US-West" |
language |
string | Preferred language | "en" |
total_purchases |
number | Lifetime purchase count | 5 |
ltv |
number | Lifetime value | 499.99 |
Group Properties (B2B)
For B2B products, track account-level properties:
// Set group
amplitude.setGroup('company', 'CompanyID-123');
// Set group properties
const groupIdentify = new amplitude.Identify()
.set('company_name', 'Acme Corp')
.set('company_plan', 'enterprise')
.set('seat_count', 50)
.set('industry', 'technology')
.set('company_created_date', '2020-05-01');
amplitude.groupIdentify('company', 'CompanyID-123', groupIdentify);
// Events automatically include group context
amplitude.track('feature_used', {
feature_name: 'advanced_analytics'
});
Amplitude Govern Integration
Schema Validation
Use Amplitude Govern to enforce data quality:
- Define expected events and properties
- Mark events as "Expected" or "Planned"
- Set verification status (Verified, Requires Review, Blocked)
- Enable schema validation to reject malformed events
Tracking Plan Definition
// Example tracking plan configuration
const trackingPlan = {
events: {
product_viewed: {
description: 'User viewed a product detail page',
properties: {
product_id: { type: 'string', required: true },
product_name: { type: 'string', required: true },
price: { type: 'number', required: true },
category: { type: 'string', required: false }
}
},
purchase_completed: {
description: 'User completed a purchase',
properties: {
order_id: { type: 'string', required: true },
total_amount: { type: 'number', required: true },
currency: { type: 'string', required: true },
item_count: { type: 'number', required: true }
}
}
}
};
Property Blocking
Configure Govern to drop disallowed properties:
- Blocked: Properties that should never be tracked (PII, sensitive data)
- Unexpected: Properties not in the tracking plan (flag for review)
Schema Versioning
Version your tracking plan for change management:
amplitude.track('purchase_completed', {
schema_version: '2.1.0',
order_id: 'ORD-98765',
total_amount: 156.99
});
Communicate schema changes:
- Minor version bump (2.0.0 → 2.1.0): New optional properties
- Major version bump (2.1.0 → 3.0.0): Breaking changes (removed or renamed properties)
Server-Side Data Enrichment
Enrich events with server-side data before sending to Amplitude:
// Server-side (Node.js)
const Amplitude = require('@amplitude/node');
const client = Amplitude.init('YOUR_API_KEY');
app.post('/api/track-purchase', async (req, res) => {
const { userId, orderId } = req.body;
// Fetch additional data from database
const order = await db.orders.findById(orderId);
const user = await db.users.findById(userId);
// Send enriched event
client.logEvent({
event_type: 'purchase_completed',
user_id: userId,
event_properties: {
order_id: orderId,
total_amount: order.total,
item_count: order.items.length,
// Server-side enrichment
user_ltv: user.lifetimeValue,
user_segment: user.segment,
is_repeat_customer: user.purchaseCount > 1,
days_since_signup: dateDiff(user.createdAt, new Date())
}
});
res.json({ success: true });
});
Session Data Management
Track session-level context:
class SessionManager {
constructor() {
this.sessionData = this.initializeSession();
}
initializeSession() {
return {
session_start_time: new Date().toISOString(),
initial_referrer: document.referrer,
initial_utm_source: new URLSearchParams(window.location.search).get('utm_source'),
page_view_count: 0,
event_count: 0
};
}
incrementPageView() {
this.sessionData.page_view_count++;
}
incrementEvent() {
this.sessionData.event_count++;
}
getSessionData() {
return {
...this.sessionData,
session_duration_seconds: Math.floor(
(new Date() - new Date(this.sessionData.session_start_time)) / 1000
)
};
}
}
const sessionManager = new SessionManager();
// Include session data in events
amplitude.track('purchase_completed', {
...sessionManager.getSessionData(),
order_id: 'ORD-98765'
});
Validation and Testing
Event Stream Monitoring
- Navigate to Data > Events in Amplitude dashboard
- Use Event Stream to view real-time events
- Inspect payloads for:
- Correct property names and casing
- Expected data types
- Required properties present
- No unexpected properties
Property Validation Checklist
| Check | Expected Result |
|---|---|
| Property names use snake_case | No camelCase or PascalCase |
| Data types consistent | price always number, never string |
| Required properties present | All required fields included |
| Currency codes valid | ISO 4217 format (USD, EUR, GBP) |
| Timestamps in ISO 8601 | "2024-01-15T10:30:00Z" |
| No PII in properties | No email, phone, address unless hashed |
Volume and Quality Monitoring
// Track data quality metrics
amplitude.track('data_quality_check', {
check_type: 'schema_validation',
events_validated: 1000,
events_passed: 995,
events_failed: 5,
failure_rate: 0.5
});
Monitor in Amplitude:
- Event Volume: Track event counts per day/week
- Property Coverage: Ensure key properties appear on expected events
- Error Rates: Monitor validation failures via Govern
Debug Mode
Enable debug mode to see events in the console:
amplitude.init('YOUR_API_KEY', null, {
logLevel: 'DEBUG'
});
// Logs will appear in console for every event
amplitude.track('test_event', { test_property: 'test_value' });
Troubleshooting
| Symptom | Likely Cause | Solution |
|---|---|---|
| Properties not appearing in Amplitude | Property name typo or casing issue | Verify exact property name in Event Stream |
| Inconsistent data types | Sending number as string or vice versa | Enforce type checking in data layer abstraction |
| User properties not persisting | Using event properties instead | Use amplitude.identify() with Identify object |
| Revenue not tracking | Missing Revenue API call | Use amplitude.logRevenueV2() for purchase events |
| Events missing context | Not including page/session data | Create data layer abstraction to auto-enrich |
| High unexpected property rate | No tracking plan enforcement | Enable Govern schema validation |
| Attribution data lost | Not capturing UTM on first page load | Store UTM params immediately on landing |
Best Practices
- Define tracking plan early: Document all events and properties before implementation
- Use data layer abstraction: Centralize data management for consistency
- Enforce naming conventions: Use linters or validation to ensure snake_case
- Version your schema: Track changes and communicate to downstream consumers
- Enrich server-side: Add authoritative data from backend systems
- Test thoroughly: Validate in Event Stream before production release
- Monitor data quality: Set up alerts for schema violations or volume drops
- Minimize PII exposure: Hash or exclude sensitive user data
- Document everything: Maintain a central tracking plan document
- Review regularly: Audit property usage quarterly and deprecate unused properties