# Media Streaming & Listener Analytics

Operational guide for “I’m Not Done Yet” delivery, privacy-conscious event collection, and the private analytics dashboard.

## Public URLs (preserve these)

| Purpose | URL |
|---------|-----|
| AI Media page | `https://www.techforceholdings.com/ai-media` |
| Direct song file | `https://www.techforceholdings.com/assets/media/im-not-done-yet.mp3` |
| Admin dashboard | `https://www.techforceholdings.com/admin/media-analytics` |

Unique sessions use `count(DISTINCT blob2)` — Analytics Engine rejects `uniqExact` with HTTP 422. The **Video clip** track hides downloads in the dashboard UI.


On the dashboard, switch **Full song** (`im-not-done-yet`) vs **Video clip** (`im-not-done-video-clip`), and **DISCO CATz and DOG** (`disco-catz`). Same metrics layout for each track.

Do **not** rename the MP3 filename — LinkedIn, email, and SMS posts may deep-link it.

## Architecture

- **MP3** is a Cloudflare Pages **static asset**. It is never loaded into Pages Function memory.
- Production already returns `Content-Type: audio/mpeg` and **`206 Partial Content`** for `Range` requests.
- Cache headers are tuned via [`_headers`](../_headers) (`max-age=86400`, **no** `immutable` on the unversioned file).
- Events: `POST /api/media-event` → Analytics Engine binding `MEDIA_ANALYTICS` / dataset `techforce_media_events`.
- Dashboard: `/admin/media-analytics` + `GET /api/admin/media-analytics` — **fail closed** without Cloudflare Access.

## Wrangler binding

[`wrangler.toml`](../wrangler.toml):

```toml
[[analytics_engine_datasets]]
binding = "MEDIA_ANALYTICS"
dataset = "techforce_media_events"
```

After deploy, confirm in **Workers & Pages → techforceweb → Settings → Bindings**.

## Secrets & variables (Pages)

| Name | Type | Required for |
|------|------|----------------|
| `CF_ACCOUNT_ID` | In [`wrangler.toml`](../wrangler.toml) `[vars]` | Dashboard SQL queries (not a secret) |
| `CF_AE_API_TOKEN` | **Secret** (dashboard only) | Dashboard SQL queries |
| `CF_ACCESS_TEAM_DOMAIN` | Variable | Admin auth (e.g. `https://<team>.cloudflareaccess.com`) |
| `CF_ACCESS_AUD` | Variable | Admin Access application AUD tag |

If the Pages UI only lets you add secrets, that is fine: keep `CF_ACCOUNT_ID` in `wrangler.toml` and add only `CF_AE_API_TOKEN` as a Production secret.

### Least-privilege API token

Create a **Custom Token**:

- Permission: **Account → Account Analytics → Read**
- Account resources: include only this account
- Optional: IP allowlist / TTL

Never put this token in HTML, JS, screenshots, or git.

## Cloudflare Access (Gene must configure)

1. Zero Trust → Access → Applications → Add **Self-hosted**.
2. Application domain: `www.techforceholdings.com` (and apex if used).
3. Path: `/admin/media-analytics*` (also protect `/api/admin/media-analytics*` if path rules are separate).
4. Policy: Allow emails / IdP group for Gene (and trusted operators only).
5. Copy the application **AUD** into `CF_ACCESS_AUD`.
6. Set `CF_ACCESS_TEAM_DOMAIN` to the team’s Access domain.

Repo middleware **denies all admin traffic** if Access env vars are missing **and** temporary unlock is off (fail closed). Edge Access is still required for production hardening.

### Temporary public unlock (current)

The dashboard is **temporarily public** so Gene can review it before Access is ready:

1. Code flag: `TEMP_PUBLIC_UNLOCK = true` in `functions/lib/access-auth.js`
2. Or Pages var: `MEDIA_ANALYTICS_TEMP_PUBLIC=true`

**Re-lock:** set `TEMP_PUBLIC_UNLOCK = false`, remove the Pages unlock var, then finish Cloudflare Access setup (AUD + team domain). Until then, anyone with the URL can open the page.

### Redirect-loop note

Do **not** add `_redirects` rules that rewrite `/admin/media-analytics` → `/admin/media-analytics.html`. That conflicts with Pages’ extensionless URL handling and causes a `308` redirect loop. The page lives at `admin/media-analytics/index.html`.

## Privacy

Collected (allowlisted):

- Song id, event name, playback position/duration/percent
- Anonymous `sessionStorage` session id (tab-scoped)
- UTM source/medium/campaign (session-captured)
- Referring **domain** only
- Page path
- Coarse device category
- Cloudflare `country` (server-side only)

**Not** collected: names, emails, raw IPs, Authorization headers, Access JWTs, full user-agent strings, full referrer URLs.

Analytics failures never interrupt audio playback.

## Optional Zaraz

Zaraz is **not** installed in the site today. The client calls `zaraz.track` only if `window.zaraz` exists (`song_play`, `song_complete`, `song_share`).

To enable:

1. Cloudflare dashboard → Zaraz for the zone.
2. Add tools / triggers as needed for marketing.
3. No code change required for basic forwarding.

## Example promotional URLs

```
https://www.techforceholdings.com/ai-media?utm_source=linkedin&utm_medium=social&utm_campaign=im-not-done-yet
https://www.techforceholdings.com/ai-media?utm_source=email&utm_medium=email&utm_campaign=im-not-done-yet
https://www.techforceholdings.com/ai-media?utm_source=sms&utm_medium=sms&utm_campaign=im-not-done-yet
https://www.techforceholdings.com/assets/media/im-not-done-yet.mp3
```

Share / LinkedIn direct file:

```
https://www.techforceholdings.com/assets/media/im-not-done-yet.mp3
```

## Local development

```bash
npm test
npx wrangler pages dev .
```

Notes:

- Analytics Engine **writes do not work locally** (Cloudflare limitation). The ingest endpoint still validates and returns `202`.
- Admin routes return **403** without Access env + JWT (fail closed).
- Local `wrangler pages dev` and `*.pages.dev` previews may return full `200` bodies for `Range` requests. **Custom-domain production** (`www.techforceholdings.com`) already returns **`206 Partial Content`** with `Content-Range` — verify Range there (or after production deploy), not only on pages.dev.

## Preview verification checklist

- [ ] `HEAD/GET` MP3 → `audio/mpeg`
- [ ] `Range: bytes=0-1023` → `206` + `Content-Range`
- [ ] Player play / seek / replay / share
- [ ] `POST /api/media-event` → `202` for valid payload; `400` for unknown event
- [ ] `/admin/media-analytics` → Access challenge or **403** unauthenticated
- [ ] Dashboard responsive layout

## Production rollout

1. Merge PR → Pages production deploy.
2. Confirm AE binding + secrets on **Production**.
3. Confirm Access application paths.
4. Smoke-test URLs above.

## Rollback

1. Rollback to previous Pages deployment in the Cloudflare dashboard, **or**
2. Revert the merge commit and redeploy.
3. Ingestion degrades safely if `MEDIA_ANALYTICS` is removed; player keeps working.

## Events

`song_impression`, `song_play`, `song_25`, `song_50`, `song_75`, `song_complete`, `song_replay`, `song_seek`, `song_share`, `song_download`

Approximate unique listeners = distinct session ids in aggregates (no separate unique-listener event).
