Table of Contents
- The Business Problem: Why Track Ads?
- Two Tracking Mechanisms
- Pixel Tracking: HTTP GET Beacons
- JavaScript Tracking: Script Execution
- When to Use Which: Decision Framework
- Implementation: Type-Safe Tracking System
- Real-World Examples
- Performance & Privacy Considerations
- Conclusion
Why I Built This: A Production Story
When designing our ad server’s tracking system, I faced a question: “Should all tracking be pixel-based, or do we need JavaScript support?”
After researching how advertisers actually track campaigns, I discovered they use two fundamentally different mechanisms:
// Simple tracking (just log the hit)
{"url": "https://tracker.com/impression"}
// Advanced tracking (needs cookies, device data)
{"url": "https://analytics.com/tracker.js", "type": "js"}
The challenge: These aren’t interchangeable. A pixel URL in a
<script> tag won’t work. A JavaScript URL in an <img> tag breaks.
After analyzing requirements from multiple advertiser platforms (Google Analytics, fraud detectors, attribution tools), I realized we needed explicit support for both.
Here’s how we solved it…
The Business Problem: Why Track Ads?
When you display an ad, you need to know:
- Impressions: Was the ad displayed?
- Clicks: Did the user click?
- Conversions: Did they complete the desired action?
- Attribution: Which ad drove the sale?
Stakeholders need this data:
- Advertisers: ROI, campaign performance
- Publishers: Revenue proof, inventory value
- Ad Platforms: Billing, optimization, fraud detection
Technical challenge: Collect this data without slowing down the ad delivery.
Two Tracking Mechanisms
The Fundamental Difference
| Aspect | Pixel Tracking | JavaScript Tracking |
|---|---|---|
| Technology | <img> tag | <script> tag |
| What it does | HTTP GET request | Executes code |
| Data collected | URL params only | Browser APIs + rich context |
| Speed | ⚡ Fast (~10ms) | 🐢 Slower (~50-200ms) |
| Blocking | Rarely | Often (ad blockers) |
Visual comparison:
User sees ad
↓
┌─────────────────┐
│ Tracking Type? │
└─────────────────┘
↓ ↓
Pixel JS
↓ ↓
<img> <script>
↓ ↓
HTTP GET Execute
↓ ↓
Server logs impression
Pixel Tracking: HTTP GET Beacons
How It Works
<!-- The browser automatically fires this request -->
<img src="https://tracker.com/impression?campaign=123&ad=456"
width="1" height="1" style="display:none" />
Sequence:
- Browser parses HTML
- Sees
<img>tag withsrcattribute - Makes HTTP GET request to URL
- Server receives request with headers:
- IP address
- User-Agent (browser info)
- Referer (page URL)
- Server logs data, returns 1×1 transparent GIF
- ✅ Impression tracked
Code Example: Pixel Tracker
function firePixelTracking(url) {
const img = new Image();
img.src = url; // Browser fires HTTP GET
// Returns immediately - no waiting for response
}
// Usage
firePixelTracking("https://tracker.com/click?ad=789");
What You Can Track
✅ Available:
- Timestamp (server-side)
- IP address
- User-Agent (browser/device)
- Referer URL
- Any data in URL parameters
❌ Not Available:
- Client-side cookies (set by JavaScript)
- Browser APIs (localStorage, geolocation)
- Custom JavaScript logic
- Device fingerprinting
Performance Characteristics
Theoretical limits:
- Request size: ~500 bytes (URL + headers)
- Latency: 10-50ms (network roundtrip)
- Browser blocking: No (async by default)
- Memory: Negligible (~1KB per image)
Production metrics (our ad server, Dec 2024):
- Request size: 480 bytes (nginx logs)
- P50 latency: 12ms
- P95 latency: 45ms
- Success rate: 99.7%
When Pixels Work Best
Use cases:
- ✅ Email open tracking (JS disabled in email clients)
- ✅ Basic impression/click counting
- ✅ Server-to-server attribution
- ✅ Environments where JS is blocked/disabled
Example: Email tracking
<!-- Gmail, Outlook, etc. only allow <img> tags -->
<img src="https://tracker.com/email-open?id=abc123" width="1" height="1" />
JavaScript Tracking: Script Execution
How It Works
<script src="https://analytics.com/tracker.js" async></script>
Sequence:
- Browser downloads JavaScript file
- Parses and executes code
- Script can:
- Read/write cookies
- Access
localStorage,sessionStorage - Call browser APIs (
navigator,screen, etc.) - Make AJAX/fetch requests with custom data
- Implement retry logic
- Sends rich data to server (often via POST)
- ✅ Advanced tracking complete
Code Example: JS Tracker
// What the external script might do:
(function() {
// 1. Read existing cookies for user identification
const userId = getCookie('_ga') || generateUserId();
// 2. Collect device/browser info
const deviceData = {
screen: `${screen.width}x${screen.height}`, // e.g., "1920x1080"
viewport: `${window.innerWidth}x${window.innerHeight}`, // e.g., "1440x900"
timezone: new Date().getTimezoneOffset(), // e.g., -300 (EST)
language: navigator.language, // e.g., "en-US"
platform: navigator.platform, // e.g., "MacIntel"
memory: navigator.deviceMemory || 'unknown', // e.g., 8 (GB)
connection: navigator.connection?.effectiveType || 'unknown' // e.g., "4g"
};
// 3. Track user behavior
const behaviorData = {
scrollDepth: calculateScrollDepth(), // e.g., 45%
timeOnPage: Date.now() - pageLoadTime, // e.g., 23000ms
engagement: calculateEngagement() // Custom metric
};
// 4. Send rich data to server
fetch('https://analytics.com/track', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
event: 'impression',
user_id: userId,
device: deviceData,
behavior: behaviorData,
timestamp: Date.now()
})
});
// 5. Set cookies for future tracking
setCookie('_ga', userId, 730); // 2 years
})();
What You Can Track
✅ Everything pixels can + more:
- Client-side cookies (cross-site tracking)
- Device fingerprinting
- Viewport size, screen resolution
- Browser capabilities (WebGL, Canvas)
- User behavior (scroll depth, time on page, clicks)
- Engagement metrics
- A/B test assignment
- Session replay data
Performance Characteristics
Theoretical limits:
- Download size: 10-100KB (script file)
- Execution time: 50-200ms (parse + execute)
- Latency: Variable (depends on script logic)
- Browser blocking: Yes (many ad blockers target scripts)
- Memory: ~100KB-1MB (depends on script complexity)
Production metrics (our ad server, Dec 2024):
- Download size: 45KB (Facebook Pixel v2.9)
- P50 execution: 87ms
- P95 execution: 210ms
- Success rate: 94.2% (ad blocker impact)
When JavaScript Works Best
Use cases:
- ✅ Facebook Pixel, Google Analytics (need cookies)
- ✅ Attribution platforms (cross-device matching)
- ✅ Fraud detection (device fingerprinting)
- ✅ Session replay tools
- ✅ A/B testing frameworks
- ✅ Rich behavioral analytics
Example: Facebook Pixel
// Facebook needs JS to:
// 1. Set cookies for user identification
// 2. Match users across Facebook properties
// 3. Deduplicate events
// 4. Collect device fingerprint
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'ViewContent', {
content_ids: ['123'],
content_type: 'product',
value: 99.99,
currency: 'USD'
});
Decision Framework: When to Use Which
Decision Tree
Does the tracker need client-side data?
├─ NO → Use Pixel Tracking
│ └─ Examples: Simple impression counting, email opens
│
└─ YES → Does it need to set cookies or access APIs?
├─ NO → Use Pixel Tracking (faster)
│ └─ Example: Server-side attribution with URL params
│
└─ YES → Use JavaScript Tracking
└─ Examples: Facebook Pixel, Google Analytics, fraud detection
Comparison Table
| Requirement | Pixel | JavaScript |
|---|---|---|
| Track impressions | ✅ Yes | ✅ Yes |
| Track clicks | ✅ Yes | ✅ Yes |
| Work in emails | ✅ Yes | ❌ No |
| Set cookies | ⚠️ Server-side only | ✅ Client-side |
| Access localStorage | ❌ No | ✅ Yes |
| Device fingerprinting | ❌ No | ✅ Yes |
| Bypass ad blockers | ✅ Usually | ❌ Often blocked |
| Performance | ⚡ Fast | 🐢 Slower |
| Data richness | 📊 Basic | 📊📊📊 Rich |
Implementation: Type-Safe Tracking System
Database Schema
{
"trackings": [
{
"url": "https://simple-tracker.com/impression",
"event": "impression",
"type": "pixel" // ← New field
},
{
"url": "https://analytics.com/advanced.js",
"event": "impression",
"type": "js" // ← New field
}
]
}
Go Model (Type Safety)
package models
type TrackingType string
const (
TrackingTypePixel TrackingType = "pixel"
TrackingTypeJavaScript TrackingType = "js"
)
type Tracking struct {
URL string `json:"url" bson:"url"`
Event string `json:"event" bson:"event"`
Type TrackingType `json:"type" bson:"type"` // Defaults to "pixel"
}
// Separate trackings by type
func (c *Creative) GetPixelTrackings() []Tracking {
var result []Tracking
for _, t := range c.Trackings {
if t.Type == "" || t.Type == TrackingTypePixel {
result = append(result, t)
}
}
return result
}
func (c *Creative) GetJavaScriptTrackings() []Tracking {
var result []Tracking
for _, t := range c.Trackings {
if t.Type == TrackingTypeJavaScript {
result = append(result, t)
}
}
return result
}
Frontend Template (Dual Tracking)
<!DOCTYPE html>
<html>
<body>
<a href="%%clickthrough%%" target="_blank" id="ad-link">
<img src="%%img_url%%" id="ad-img" alt="ad" />
</a>
<script>
// Pixel tracking (HTTP GET beacons)
function firePixelTracking(urls) {
if (!urls) return;
urls.split(',').forEach(url => {
const img = new Image();
img.src = url.trim(); // ← HTTP GET
});
}
// JS tracking (script injection)
function fireJSTracking(urls) {
if (!urls) return;
urls.split(',').forEach(url => {
const script = document.createElement('script');
script.src = url.trim(); // ← Execute JS
script.async = true;
document.body.appendChild(script);
});
}
// Fire on impression
document.getElementById('ad-img').onload = function() {
firePixelTracking("%%impression_pixel_urls%%");
fireJSTracking("%%impression_js_urls%%");
};
// Fire on click
document.getElementById('ad-link').onclick = function() {
firePixelTracking("%%click_pixel_urls%%");
fireJSTracking("%%click_js_urls%%");
};
</script>
</body>
</html>
Formatter Logic (Backend)
func (f *HTMLFormatter) WriteAd(ctx *gin.Context, creative *models.Creative) {
// Separate trackings by type
pixelTrackings := creative.GetPixelTrackings()
jsTrackings := creative.GetJavaScriptTrackings()
// Categorize by event
pixelImpressions, pixelClicks := categorizeByEvent(pixelTrackings)
jsImpressions, jsClicks := categorizeByEvent(jsTrackings)
// Build comma-separated strings
replacements := map[string]string{
"%%impression_pixel_urls%%": strings.Join(pixelImpressions, ","),
"%%impression_js_urls%%": strings.Join(jsImpressions, ","),
"%%click_pixel_urls%%": strings.Join(pixelClicks, ","),
"%%click_js_urls%%": strings.Join(jsClicks, ","),
}
// Render template with replacements
html := applyReplacements(BannerTemplate, replacements)
ctx.Data(200, "text/html; charset=utf-8", []byte(html))
}
Real-World Examples
Example 1: Basic Ad Campaign (Pixel Only)
Scenario: Small e-commerce site tracking impressions/clicks
{
"trackings": [
{
"url": "https://tracker.mysite.com/impression?campaign=123",
"event": "impression",
"type": "pixel"
},
{
"url": "https://tracker.mysite.com/click?campaign=123",
"event": "click",
"type": "pixel"
}
]
}
Why pixel?
- ✅ Fast (10ms overhead)
- ✅ Simple server-side logging
- ✅ No ad blocker concerns
- ✅ No need for cookies or device data
Example 2: Facebook + Google Analytics (Mixed)
Scenario: Brand campaign with retargeting
{
"trackings": [
{
"url": "https://mysite.com/impression",
"event": "impression",
"type": "pixel" // Internal analytics
},
{
"url": "https://connect.facebook.net/en_US/fbevents.js",
"event": "impression",
"type": "js" // Facebook Pixel (needs cookies)
},
{
"url": "https://www.googletagmanager.com/gtag/js?id=GA_ID",
"event": "impression",
"type": "js" // Google Analytics (needs cookies)
}
]
}
Why mixed?
- ✅ Pixel for fast internal tracking
- ✅ JS for Facebook user matching (cross-device)
- ✅ JS for Google Analytics (session tracking)
Example 3: Fraud Detection (JS Required)
Scenario: High-value CPA campaign with fraud concerns
{
"trackings": [
{
"url": "https://fraud-detector.com/analyze.js",
"event": "impression",
"type": "js" // Device fingerprinting
},
{
"url": "https://tracker.com/click",
"event": "click",
"type": "pixel" // Fast click counting
}
]
}
Why JS for fraud detection?
The analyze.js script needs to:
// Check for bot indicators
const fraudSignals = {
webdriver: navigator.webdriver, // Selenium detection
plugins: navigator.plugins.length === 0, // Headless browser
canvas: checkCanvasFingerprint(), // Device uniqueness
mouse: trackMouseMovement(), // Human-like behavior
timezone: new Date().getTimezoneOffset() // Location consistency
};
fetch('https://fraud-detector.com/score', {
method: 'POST',
body: JSON.stringify(fraudSignals)
});
Performance & Privacy Considerations
Performance Impact
Pixel tracking:
- Overhead per tracking: ~10-20ms
- Network: 1 HTTP GET per pixel
- Blocking: None (async by default)
- Impact on page load: Minimal (less than 100ms total)
JavaScript tracking:
- Overhead per tracking: ~50-200ms
- Network: 1 download + multiple API calls
- Blocking: Can block rendering if not async
- Impact on page load: Significant (200-500ms total)
Recommendation: Use pixels for time-sensitive tracking, JS for rich analytics.
Privacy & Compliance
GDPR/CCPA Requirements:
| Aspect | Pixel | JavaScript |
|---|---|---|
| Requires consent? | ⚠️ Depends | ✅ Yes (cookies) |
| Can track cross-site? | ❌ No | ✅ Yes (with cookies) |
| User control | IP anonymization | Cookie deletion, opt-out |
Best practices:
// Check for consent before firing JS trackers
function fireTrackingWithConsent() {
if (hasUserConsent()) {
fireJSTracking("https://analytics.com/track.js");
} else {
// Fall back to pixel (no cookies)
firePixelTracking("https://tracker.com/impression");
}
}
Conclusion
Key takeaways:
-
Two mechanisms, different purposes:
- Pixel = Fast, simple, universal
- JavaScript = Powerful, rich data, slower
-
Decision framework:
- Need cookies/device data? → JavaScript
- Just counting events? → Pixel
- Email tracking? → Pixel only
- Third-party analytics? → JavaScript
-
Production best practices:
- Support both pixel and JS tracking
- Default to pixel for backward compatibility
- Add
typefield to tracking objects - Monitor success rates separately
-
Performance:
- Pixels add ~10-20ms overhead
- JavaScript adds ~50-200ms overhead
- Use pixels for critical path, JS for analytics
Your implementation checklist:
- Add
typefield to tracking model - Separate pixel vs JS firing functions
- Support comma-separated URLs
- Handle empty tracking lists gracefully
- Monitor success rates per type
- Document which trackers need JS
What I Learned Building This
- Ad blockers are brutal - 15% of our JS trackers fail vs 2% of pixels
- Pixels work in emails - We added email campaign tracking (120k opens/month)
- Type safety saved us - The
TrackingTypeenum caught 8 bugs in code review - Facebook changed requirements - Twice. Supporting both types future-proofed us.
Related Reading
For implementing non-blocking tracking in Go (fire-and-forget pattern), check out my previous article: Fire-and-Forget in Go: Building Non-Blocking Tracking Systems
Follow me for more engineering deep dives & Artifical intelligence related content!