Skip to content

Commit ceeded5

Browse files
bkboothclaude
andcommitted
docs(pixel): add integration guide, CSP docs, and CDN validation script
Provide a complete README for the tracking pixel covering the Game Page snippet, consent modes, auto-tracked events, cookie reference, CSP directives, and a browser compatibility validation checklist. Also adds a shell script to verify the CDN bundle is deployed and within the 10KB gzip budget. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 05eafd9 commit ceeded5

2 files changed

Lines changed: 279 additions & 0 deletions

File tree

packages/audience/pixel/README.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# @imtbl/pixel — Immutable Tracking Pixel
2+
3+
A drop-in JavaScript snippet that captures device signals, page views, and attribution data for Immutable's events pipeline. Zero configuration beyond a publishable key.
4+
5+
## Quick Start
6+
7+
Paste this snippet into your site's `<head>` tag:
8+
9+
```html
10+
<script>
11+
(function(){
12+
var w=window,i="__imtbl";
13+
w[i]=w[i]||[];
14+
w[i].push(["init",{"key":"YOUR_PUBLISHABLE_KEY"}]);
15+
var s=document.createElement("script");s.async=1;
16+
s.src="https://cdn.immutable.com/pixel/v1/imtbl.js";
17+
document.head.appendChild(s);
18+
})();
19+
</script>
20+
```
21+
22+
Replace `YOUR_PUBLISHABLE_KEY` with your project's publishable key.
23+
24+
The script loads asynchronously and does not block page rendering. The default consent level is `none` — the pixel loads but does not collect until consent is explicitly set (see [Consent Modes](#consent-modes)). To start collecting anonymous device signals immediately, add `"consent":"anonymous"` to the init object.
25+
26+
## Consent Modes
27+
28+
The `consent` option controls what the pixel collects. **Default is `none`** (no events fire until consent is set).
29+
30+
| Level | What's collected | Cookies set | Use case |
31+
|-------|-----------------|-------------|----------|
32+
| `none` | Nothing — pixel loads but is inert | None | Before consent banner interaction |
33+
| `anonymous` | Device signals, attribution, page views, form submissions, link clicks (no PII) | `imtbl_anon_id`, `_imtbl_sid` | Anonymous analytics without PII |
34+
| `full` | Everything in `anonymous` + user identity (email hash, userId) | `imtbl_anon_id`, `_imtbl_sid` | After explicit user consent |
35+
36+
### Updating consent at runtime
37+
38+
```javascript
39+
// After cookie banner interaction — upgrade to full
40+
window.__imtbl.push(['consent', 'full']);
41+
42+
// Or downgrade (purges PII from queue)
43+
window.__imtbl.push(['consent', 'none']);
44+
```
45+
46+
## Auto-Tracked Events
47+
48+
All events fire automatically with no instrumentation required.
49+
50+
| Event | When it fires | Key properties |
51+
|-------|--------------|----------------|
52+
| `page` | Every page load | UTMs, click IDs (`gclid`, `fbclid`, `ttclid`, `msclkid`, `dclid`, `li_fat_id`), `referral_code`, `landing_page` |
53+
| `session_start` | New session (no active `_imtbl_sid` cookie) | `sessionId` |
54+
| `session_end` | Page unload (`visibilitychange` / `pagehide`) | `sessionId`, `duration` (seconds) |
55+
| `form_submitted` | HTML form submission | `formAction`, `formId`, `formName`, `fieldNames`. `emailHash` at `full` consent only. |
56+
| `link_clicked` | Outbound link click (external domains only) | `linkUrl`, `linkText`, `elementId`, `outbound: true` |
57+
58+
### Disabling specific auto-capture
59+
60+
```html
61+
<script>
62+
(function(){
63+
var w=window,i="__imtbl";
64+
w[i]=w[i]||[];
65+
w[i].push(["init",{
66+
"key":"YOUR_KEY",
67+
"consent":"anonymous",
68+
"autocapture":{"forms":false,"clicks":true}
69+
}]);
70+
var s=document.createElement("script");s.async=1;
71+
s.src="https://cdn.immutable.com/pixel/v1/imtbl.js";
72+
document.head.appendChild(s);
73+
})();
74+
</script>
75+
```
76+
77+
## Identity (Optional)
78+
79+
For sites with user accounts, identify known users at `full` consent:
80+
81+
```javascript
82+
window.__imtbl.push(['identify', 'user-123', 'passport', { email: 'player@example.com' }]);
83+
```
84+
85+
Note: traits passed via `identify` are sent as-is. Email values are only automatically SHA-256 hashed when captured from form submissions via auto-capture (see [Auto-Tracked Events](#auto-tracked-events)). If you pass an email in identify traits, hash it yourself before calling identify if that is required for your use case.
86+
87+
## Cookies
88+
89+
| Cookie | Lifetime | Purpose |
90+
|--------|----------|---------|
91+
| `imtbl_anon_id` | 2 years | Anonymous device ID (shared with web SDK) |
92+
| `_imtbl_sid` | 30 minutes (rolling) | Session ID — resets on inactivity |
93+
94+
Both cookies are first-party (`SameSite=Lax`, `Secure` on HTTPS).
95+
96+
## Content Security Policy (CSP)
97+
98+
If your site uses a Content-Security-Policy header, add these origins to the relevant directives:
99+
100+
```
101+
script-src ... https://cdn.immutable.com;
102+
connect-src ... https://api.immutable.com;
103+
```
104+
105+
These must be added alongside your existing policy values, not replace them.
106+
107+
For nonce-based CSP, add the nonce to the inline `<script>` tag in the snippet:
108+
109+
```html
110+
<script nonce="YOUR_NONCE">
111+
(function(){
112+
var w=window,i="__imtbl";
113+
w[i]=w[i]||[];
114+
w[i].push(["init",{"key":"YOUR_KEY","consent":"anonymous"}]);
115+
var s=document.createElement("script");s.async=1;
116+
s.src="https://cdn.immutable.com/pixel/v1/imtbl.js";
117+
document.head.appendChild(s);
118+
})();
119+
</script>
120+
```
121+
122+
Note: the nonce covers the inline snippet only. The CDN-loaded script (`imtbl.js`) is covered by the `script-src https://cdn.immutable.com` directive.
123+
124+
## Browser Support
125+
126+
| Browser | Minimum Version |
127+
|---------|----------------|
128+
| Chrome | 80+ |
129+
| Firefox | 78+ |
130+
| Safari | 14+ |
131+
| Edge | 80+ |
132+
133+
## Game Page Integration
134+
135+
The Game Page uses the pixel with `consent: 'anonymous'` (no PII, device signals only):
136+
137+
```html
138+
<script>
139+
(function(){
140+
var w=window,i="__imtbl";
141+
w[i]=w[i]||[];
142+
w[i].push(["init",{"key":"GAME_PAGE_PUBLISHABLE_KEY","consent":"anonymous"}]);
143+
var s=document.createElement("script");s.async=1;
144+
s.src="https://cdn.immutable.com/pixel/v1/imtbl.js";
145+
document.head.appendChild(s);
146+
})();
147+
</script>
148+
```
149+
150+
### Validation Checklist
151+
152+
After installing on Game Page, verify:
153+
154+
- [ ] Snippet is in `<head>` and does not block rendering (async load confirmed)
155+
- [ ] Page load impact under 50ms (measure with Lighthouse)
156+
- [ ] `PageMessage` events visible in events pipeline with `surface: 'pixel'`
157+
- [ ] Attribution context (UTMs, referrer) correctly captured on campaign-linked visits
158+
- [ ] Session cookie (`_imtbl_sid`) set and rolling on navigation
159+
- [ ] Anonymous ID cookie (`imtbl_anon_id`) set with 2-year expiry
160+
- [ ] No console errors across Chrome 80+, Firefox 78+, Safari 14+, Edge 80+
161+
- [ ] CSP (if any) allows `script-src cdn.immutable.com` and `connect-src api.immutable.com`
162+
- [ ] 100% of events pass backend schema validation (check rejected count in API logs)
163+
- [ ] Event volume within expected range — no duplicate events, no runaway listeners
164+
- [ ] Monitor for 24 hours post-deployment before clearing for external rollout
165+
166+
### Browser Compatibility Matrix
167+
168+
| Check | Chrome 80+ | Firefox 78+ | Safari 14+ | Edge 80+ |
169+
|-------|-----------|-------------|------------|---------|
170+
| Pixel loads (no network errors) | | | | |
171+
| `page` event fires (POST 200 in Network tab) | | | | |
172+
| `surface: 'pixel'` in request body | | | | |
173+
| `_imtbl_sid` cookie set (30min expiry) | | | | |
174+
| `imtbl_anon_id` cookie set (2yr expiry) | | | | |
175+
| UTM params captured in properties | | | | |
176+
| `form_submitted` fires on form submit | | | | |
177+
| `link_clicked` fires on outbound click | | | | |
178+
| No console errors | | | | |
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Validate the pixel CDN bundle is deployed and within budget.
4+
#
5+
# Usage:
6+
# ./scripts/validate-cdn.sh [URL]
7+
#
8+
# Defaults to the production CDN URL if no argument is provided.
9+
10+
set -euo pipefail
11+
12+
CDN_URL="${1:-https://cdn.immutable.com/pixel/v1/imtbl.js}"
13+
MAX_GZIP_BYTES=10240 # 10 KB — must match bundlebudget.json
14+
WARN_GZIP_BYTES=8192 # 8 KB
15+
16+
PASS=0
17+
FAIL=0
18+
19+
TMPFILE=$(mktemp)
20+
TMPHEADERS=$(mktemp)
21+
trap 'rm -f "$TMPFILE" "$TMPHEADERS"' EXIT
22+
23+
pass() { echo "$1"; PASS=$((PASS + 1)); }
24+
fail() { echo "$1"; FAIL=$((FAIL + 1)); }
25+
26+
echo "Validating pixel bundle: $CDN_URL"
27+
echo ""
28+
29+
# --- Fetch the bundle (single request for body + headers) ---
30+
HTTP_CODE=$(curl -s --connect-timeout 10 --max-time 30 -D "$TMPHEADERS" -o "$TMPFILE" -w '%{http_code}' "$CDN_URL")
31+
CONTENT_TYPE=$(grep -i '^content-type:' "$TMPHEADERS" | tr -d '\r' | awk '{print $2}')
32+
33+
# --- HTTP status ---
34+
echo "HTTP Response:"
35+
if [ "$HTTP_CODE" = "200" ]; then
36+
pass "Status: $HTTP_CODE"
37+
else
38+
fail "Status: $HTTP_CODE (expected 200)"
39+
fi
40+
41+
# --- Content-Type ---
42+
if echo "$CONTENT_TYPE" | grep -qi 'javascript'; then
43+
pass "Content-Type: $CONTENT_TYPE"
44+
else
45+
fail "Content-Type: $CONTENT_TYPE (expected application/javascript)"
46+
fi
47+
48+
# --- Bundle size ---
49+
RAW_BYTES=$(wc -c < "$TMPFILE" | tr -d ' ')
50+
GZIP_BYTES=$(gzip -c "$TMPFILE" | wc -c | tr -d ' ')
51+
52+
echo ""
53+
echo "Bundle Size:"
54+
echo " Raw: $RAW_BYTES bytes ($(awk -v b="$RAW_BYTES" 'BEGIN{printf "%.1f", b/1024}') KB)"
55+
echo " Gzip: $GZIP_BYTES bytes ($(awk -v b="$GZIP_BYTES" 'BEGIN{printf "%.1f", b/1024}') KB)"
56+
57+
if [ "$GZIP_BYTES" -le "$MAX_GZIP_BYTES" ]; then
58+
if [ "$GZIP_BYTES" -le "$WARN_GZIP_BYTES" ]; then
59+
pass "Under budget ($GZIP_BYTES / $MAX_GZIP_BYTES bytes gzipped)"
60+
else
61+
pass "Under max budget but above warning threshold ($GZIP_BYTES / $WARN_GZIP_BYTES warn, $MAX_GZIP_BYTES max)"
62+
fi
63+
else
64+
fail "Over budget! $GZIP_BYTES bytes gzipped exceeds $MAX_GZIP_BYTES limit"
65+
fi
66+
67+
# --- Content markers ---
68+
# These patterns are chosen to avoid false positives in minified code.
69+
echo ""
70+
echo "Content Checks:"
71+
if grep -q '__imtbl' "$TMPFILE"; then
72+
pass "Contains __imtbl global"
73+
else
74+
fail "Missing __imtbl global"
75+
fi
76+
77+
if grep -q '"pixel"' "$TMPFILE" || grep -q "'pixel'" "$TMPFILE"; then
78+
pass "Contains 'pixel' surface string literal"
79+
else
80+
fail "Missing 'pixel' surface string literal"
81+
fi
82+
83+
if grep -q 'session_start' "$TMPFILE"; then
84+
pass "Contains session_start event"
85+
else
86+
fail "Missing session_start event"
87+
fi
88+
89+
if grep -q 'form_submitted' "$TMPFILE"; then
90+
pass "Contains form_submitted event"
91+
else
92+
fail "Missing form_submitted event"
93+
fi
94+
95+
# --- Summary ---
96+
echo ""
97+
echo "Results: $PASS passed, $FAIL failed"
98+
99+
if [ "$FAIL" -gt 0 ]; then
100+
exit 1
101+
fi

0 commit comments

Comments
 (0)