# Tag Manager

The Tag Manager lets you wire up integrations (sending events to third-party tools, modifying page content, firing pixels) without touching the customer's website. It's a Google Tag Manager replacement built on three primitives — **Tags**, **Triggers**, **Variables** — that compile to a single JSON file the tracker fetches alongside its main bundle.

Open at **Web Analytics > Tag Manager** inside a site.

## The Three Primitives

### Tags

A Tag is something that fires when a Trigger matches. Each Tag has a `type` that decides what it does:

| Tag Type | Behavior | Common Use |
| --- | --- | --- |
| `event` | Emits a new event with a fixed name + custom properties built from variables. | Promoting `click[selector=#buy-now]` to `add_to_cart`. |
| `gtm-pixel` | Inserts an `<img>` or `<script>` pointing at an external endpoint. | Facebook Pixel, Linkedin Insight, etc. |
| `gtag-event` | Calls `gtag('event', name, params)` if a Google Analytics tag is on the page. | Sending events to a parallel GA4 install during migration. |
| `custom-html` | Injects arbitrary HTML into the page. | Inline scripts customers can't safely deploy via their own CMS. |
| `dataLayer-push` | Calls `window.dataLayer.push(payload)`. | Integrations that already use the GTM `dataLayer` convention. |

Each Tag references one or more `triggerIds`. The Tag fires every time any of its Triggers matches.

### Triggers

A Trigger is a rule that matches against the live event stream client-side.

| Trigger Type | Matches When | Example |
| --- | --- | --- |
| `page-view` | A `page_view` event fires (optionally filtered by `page_path` regex). | Fire on `/pricing` and all subroutes. |
| `click` | A `click` event fires whose `selector_path` matches a CSS selector. | Fire when `#buy-now` is clicked. |
| `form-submit` | A `form_submit` event fires (optionally filtered by `form_id` / `form_name`). | Fire when the signup form is submitted. |
| `scroll-depth` | A `scroll_depth` event at a given threshold (`25` / `50` / `75` / `100`). | Fire at 75% scroll. |
| `custom-event` | A custom event with a specific name OR any `event_kind === 'custom'`. | Fire on `add_to_cart`. |

Triggers can have a list of **conditions** that filter further — equality, regex, contains, starts-with, etc. on any event field or any variable reference (e.g. `variables.deviceType === 'mobile'`).

### Variables

A Variable extracts a value from the event context or from the page DOM. Tags reference variables by `{{variableName}}` in their config.

| Variable Type | Source | Notes |
| --- | --- | --- |
| `event-field` | `event.<path>` (e.g. `properties.amount_usd`). | The most common type. |
| `url-param` | `URL(location.href).searchParams.get(name)`. | Reads ?utm_source= and similar. |
| `cookie` | `document.cookie` lookup. | Use for first-party cookies; respects DNT/GPC. |
| `js-expression` | A sandboxed JS expression evaluated with `event` + `window` in scope. | Powerful but use sparingly — auditing is harder. |
| `dom-selector` | `document.querySelector(s).innerText`. | Pull a price out of the DOM at fire time. |
| `data-layer` | Looks up a key in `window.dataLayer`. | Bridges GTM-style sites. |

Variables also have an optional `defaultValue` used when the source is missing or empty.

## Workspaces

By default, every Site has one workspace named **Main**. `Main` is what the tracker actually runs.

For larger teams, create additional workspaces (e.g. `Q3-conversion-experiment`). Each workspace forks the current state of `Main` — Tags, Triggers, Variables, and all their cross-references. Edits inside a workspace don't affect the live tracker.

When you're done:

1. **Publish** the workspace. This:
   - Deletes the existing `Main` Tags/Triggers/Variables.
   - Reparents the workspace's documents to `Main` (so cross-references stay valid by ObjectId).
   - Compiles the new container.
   - Increments `Site.publishedContainerVersion` and snapshots the old version for rollback.
2. **Archive** the workspace if you don't expect to use it again. Archived workspaces stop showing in the dropdown but the documents are preserved.

Behind the scenes, `workspaceService.ensureMainWorkspace()` is idempotent and backfills any null-workspaceId documents to Main on first run, so older sites migrate automatically.

## Versions & Rollback

Every publish snapshots the full container into the `containerModel` collection with a monotonically increasing `version`. `Site.publishedContainerVersion` points at the currently-live one.

To roll back:

1. Open **Tag Manager > Versions**.
2. Find the target version (each lists the publish actor, timestamp, and a diff summary).
3. Click **Restore** — sets `Site.publishedContainerVersion` back. The tracker picks up the change on its next container fetch (cached 60 s with `stale-while-revalidate=300`).

Rollback is non-destructive — the workspace state isn't reverted, only the published snapshot. To also revert the workspace, fork from the restored version.

## Preview Mode

To validate tag firing before publishing, use **Preview Mode**:

1. Open the workspace you want to preview.
2. Click **Preview**.
3. Set the preview cookie (UI prints the exact `karma_preview=<workspaceId>:<token>` snippet) on the customer site.
4. Visit the customer site. The tracker detects the cookie, loads the *workspace's draft* container instead of Main, and renders a Shadow-DOM overlay showing every event + every tag fire.
5. The cookie expires after 24 h.

Preview mode is also where the **AI Element Picker** lives — see the next doc.

## Publishing Flow

1. Open a workspace.
2. Edit Tags / Triggers / Variables (CRUD operations are scoped to the active workspace).
3. **Preview** (optional but recommended).
4. **Publish** — confirm in the modal. The compiled container goes live within ~60 s.
5. The `containerModel` collection records who published, when, the version number, and a hash of the payload for tamper detection.

## Anatomy of a Compiled Container

The compiled file served at `/p/:siteKey/container.json`:

```json
{
  "payloadVersion": 1,
  "siteKey": "16efa189f624da8c",
  "version": 47,
  "variables": [
    { "id": "v1", "name": "pricePaid", "type": "event-field", "config": { "path": "properties.amount_usd" } }
  ],
  "triggers": [
    { "id": "t1", "name": "addToCartClicks", "type": "click", "config": { "selector": "#buy-now" } }
  ],
  "tags": [
    { "id": "tg1", "name": "fbConversion", "type": "gtm-pixel", "triggerIds": ["t1"], "config": { "url": "https://facebook.com/...", "params": { "value": "{{pricePaid}}" } } }
  ]
}
```

If the site has never published, the endpoint returns the same shape with empty arrays so the tracker can always proceed without a 404.

## Common Patterns

- **Promote a click to a conversion**: Trigger of type `click` (selector `#buy-now`) → Tag of type `event` (event name `purchase`). The synthetic `purchase` event then participates in funnels and event-definitions without you writing any custom JS on the customer site.
- **Server-side dedupe**: If you also fire `purchase` from the backend via `/api/server-events`, set both events to the same `event_id` (use the order ID) so the BQ insert deduplicates them.
- **Per-page A/B testing**: A variable of type `url-param` reading `?variant=` plus Triggers conditioned on its value lets the same Tag fire with different payloads per experiment branch.
- **Outbound link tracking to ad networks**: Trigger of type `click` with selector `a[href*="adservice"]` → Tag of type `gtm-pixel` for the network's conversion URL.
