Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions packages/audience/pixel/README.md
Original file line number Diff line number Diff line change
@@ -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 `<head>` tag:

```html
<script>
(function(){
var w=window,i="__imtbl";
w[i]=w[i]||[];
w[i].push(["init",{"key":"YOUR_PUBLISHABLE_KEY"}]);
var s=document.createElement("script");s.async=1;
s.src="https://cdn.immutable.com/pixel/v1/imtbl.js";
document.head.appendChild(s);
})();
</script>
```

Replace `YOUR_PUBLISHABLE_KEY` with your project's publishable key.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention that you need to get it from Hub?


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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to include who we try to auto detect consent? and what to do if it fails?


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
<script>
(function(){
var w=window,i="__imtbl";
w[i]=w[i]||[];
w[i].push(["init",{
"key":"YOUR_KEY",
"consent":"anonymous",
"autocapture":{"forms":false,"clicks":true}
}]);
var s=document.createElement("script");s.async=1;
s.src="https://cdn.immutable.com/pixel/v1/imtbl.js";
document.head.appendChild(s);
})();
</script>
```

## 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 `<script>` tag in the snippet:

```html
<script nonce="YOUR_NONCE">
(function(){
var w=window,i="__imtbl";
w[i]=w[i]||[];
w[i].push(["init",{"key":"YOUR_KEY","consent":"anonymous"}]);
var s=document.createElement("script");s.async=1;
s.src="https://cdn.immutable.com/pixel/v1/imtbl.js";
document.head.appendChild(s);
})();
</script>
```

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+ |

101 changes: 101 additions & 0 deletions packages/audience/pixel/scripts/validate-cdn.sh
Original file line number Diff line number Diff line change
@@ -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
Loading