diff --git a/packages/audience/pixel/README.md b/packages/audience/pixel/README.md new file mode 100644 index 0000000000..f801ab50da --- /dev/null +++ b/packages/audience/pixel/README.md @@ -0,0 +1,122 @@ +# @imtbl/pixel — Immutable Tracking Pixel + +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. + +## Quick Start + +Paste this snippet into your site's `
` tag: + +```html + +``` + +Replace `YOUR_PUBLISHABLE_KEY` with your project's publishable key. + +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. + +## Consent Modes + +The `consent` option controls what the pixel collects. **Default is `none`** (no events fire until consent is set). + +| Level | What's collected | Cookies set | Use case | +|-------|-----------------|-------------|----------| +| `none` | Nothing — pixel loads but is inert | None | Before consent banner interaction | +| `anonymous` | Device signals, attribution, page views, form submissions, link clicks (no PII) | `imtbl_anon_id`, `_imtbl_sid` | Anonymous analytics without PII | +| `full` | Everything in `anonymous` + email hash from form submissions | `imtbl_anon_id`, `_imtbl_sid` | After explicit user consent | + +### Updating consent at runtime + +```javascript +// After cookie banner interaction — upgrade to full +window.__imtbl.push(['consent', 'full']); + +// Or downgrade (purges PII from queue) +window.__imtbl.push(['consent', 'none']); +``` + +## Auto-Tracked Events + +All events fire automatically with no instrumentation required. + +| Event | When it fires | Key properties | +|-------|--------------|----------------| +| `page` | Every page load | UTMs, click IDs (`gclid`, `fbclid`, `ttclid`, `msclkid`, `dclid`, `li_fat_id`), `referral_code`, `landing_page` | +| `session_start` | New session (no active `_imtbl_sid` cookie) | `sessionId` | +| `session_end` | Page unload (`visibilitychange` / `pagehide`) | `sessionId`, `duration` (seconds) | +| `form_submitted` | HTML form submission | `formAction`, `formId`, `formName`, `fieldNames`. `emailHash` at `full` consent only. | +| `link_clicked` | Outbound link click (external domains only) | `linkUrl`, `linkText`, `elementId`, `outbound: true` | + +### Disabling specific auto-capture + +```html + +``` + +## Cookies + +| Cookie | Lifetime | Purpose | +|--------|----------|---------| +| `imtbl_anon_id` | 2 years | Anonymous device ID (shared with web SDK) | +| `_imtbl_sid` | 30 minutes (rolling) | Session ID — resets on inactivity | + +Both cookies are first-party (`SameSite=Lax`, `Secure` on HTTPS). + +## Content Security Policy (CSP) + +If your site uses a Content-Security-Policy header, add these origins to the relevant directives: + +``` +script-src ... https://cdn.immutable.com; +connect-src ... https://api.immutable.com; +``` + +These must be added alongside your existing policy values, not replace them. + +For nonce-based CSP, add the nonce to the inline ` +``` + +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. + +## Browser Support + +| Browser | Minimum Version | +|---------|----------------| +| Chrome | 80+ | +| Firefox | 78+ | +| Safari | 14+ | +| Edge | 80+ | + diff --git a/packages/audience/pixel/scripts/validate-cdn.sh b/packages/audience/pixel/scripts/validate-cdn.sh new file mode 100755 index 0000000000..e3a45f5439 --- /dev/null +++ b/packages/audience/pixel/scripts/validate-cdn.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# +# Validate the pixel CDN bundle is deployed and within budget. +# +# Usage: +# ./scripts/validate-cdn.sh [URL] +# +# Defaults to the production CDN URL if no argument is provided. + +set -euo pipefail + +CDN_URL="${1:-https://cdn.immutable.com/pixel/v1/imtbl.js}" +MAX_GZIP_BYTES=10240 # 10 KB — must match bundlebudget.json +WARN_GZIP_BYTES=8192 # 8 KB + +PASS=0 +FAIL=0 + +TMPFILE=$(mktemp) +TMPHEADERS=$(mktemp) +trap 'rm -f "$TMPFILE" "$TMPHEADERS"' EXIT + +pass() { echo " ✓ $1"; PASS=$((PASS + 1)); } +fail() { echo " ✗ $1"; FAIL=$((FAIL + 1)); } + +echo "Validating pixel bundle: $CDN_URL" +echo "" + +# --- Fetch the bundle (single request for body + headers) --- +HTTP_CODE=$(curl -s --connect-timeout 10 --max-time 30 -D "$TMPHEADERS" -o "$TMPFILE" -w '%{http_code}' "$CDN_URL") +CONTENT_TYPE=$(grep -i '^content-type:' "$TMPHEADERS" | tr -d '\r' | awk '{print $2}') + +# --- HTTP status --- +echo "HTTP Response:" +if [ "$HTTP_CODE" = "200" ]; then + pass "Status: $HTTP_CODE" +else + fail "Status: $HTTP_CODE (expected 200)" +fi + +# --- Content-Type --- +if echo "$CONTENT_TYPE" | grep -qi 'javascript'; then + pass "Content-Type: $CONTENT_TYPE" +else + fail "Content-Type: $CONTENT_TYPE (expected application/javascript)" +fi + +# --- Bundle size --- +RAW_BYTES=$(wc -c < "$TMPFILE" | tr -d ' ') +GZIP_BYTES=$(gzip -c "$TMPFILE" | wc -c | tr -d ' ') + +echo "" +echo "Bundle Size:" +echo " Raw: $RAW_BYTES bytes ($(awk -v b="$RAW_BYTES" 'BEGIN{printf "%.1f", b/1024}') KB)" +echo " Gzip: $GZIP_BYTES bytes ($(awk -v b="$GZIP_BYTES" 'BEGIN{printf "%.1f", b/1024}') KB)" + +if [ "$GZIP_BYTES" -le "$MAX_GZIP_BYTES" ]; then + if [ "$GZIP_BYTES" -le "$WARN_GZIP_BYTES" ]; then + pass "Under budget ($GZIP_BYTES / $MAX_GZIP_BYTES bytes gzipped)" + else + pass "Under max budget but above warning threshold ($GZIP_BYTES / $WARN_GZIP_BYTES warn, $MAX_GZIP_BYTES max)" + fi +else + fail "Over budget! $GZIP_BYTES bytes gzipped exceeds $MAX_GZIP_BYTES limit" +fi + +# --- Content markers --- +# These patterns are chosen to avoid false positives in minified code. +echo "" +echo "Content Checks:" +if grep -q '__imtbl' "$TMPFILE"; then + pass "Contains __imtbl global" +else + fail "Missing __imtbl global" +fi + +if grep -q '"pixel"' "$TMPFILE" || grep -q "'pixel'" "$TMPFILE"; then + pass "Contains 'pixel' surface string literal" +else + fail "Missing 'pixel' surface string literal" +fi + +if grep -q 'session_start' "$TMPFILE"; then + pass "Contains session_start event" +else + fail "Missing session_start event" +fi + +if grep -q 'form_submitted' "$TMPFILE"; then + pass "Contains form_submitted event" +else + fail "Missing form_submitted event" +fi + +# --- Summary --- +echo "" +echo "Results: $PASS passed, $FAIL failed" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi