# Descrybe web — GTM container import Importable Google Tag Manager workspace for `apps/web` analytics (`$lib/analytics`, Consent Mode v2, SPA `page_view`). | File | Purpose | |------|---------| | [`descrybe-web-container.json`](./descrybe-web-container.json) | GTM export format version 2 — tags, triggers, variables | No real secrets. Placeholders only: `G-XXXXXXXX`, `GTM-XXXXXXX`. ## Import fix notes ### Invalid `Parameter.type` enum (`STRING`) GTM import failed with: > File format is invalid. Error deserializing enum type [Type]. Unrecognized value [STRING]. **Root cause:** a previous edit set `consentSettings.consentType` list items to `"type": "STRING"`, matching outdated API prose (“list item is of type STRING”). That value is **not** in the real Parameter type enum. Official GTM API `Parameter.type` values (camelCase in the API; **SNAKE_CASE** in container import/export JSON): | API | Import/export JSON | |-----|--------------------| | `template` | `TEMPLATE` | | `integer` | `INTEGER` | | `boolean` | `BOOLEAN` | | `list` | `LIST` | | `map` | `MAP` | | `triggerReference` | `TRIGGER_REFERENCE` | | `tagReference` | `TAG_REFERENCE` | There is **no** `string` / `STRING`. Text values (including consent type names like `analytics_storage`) use **`TEMPLATE`**. This file now uses: ```json "consentSettings": { "consentStatus": "NEEDED", "consentType": { "type": "LIST", "list": [{ "type": "TEMPLATE", "value": "analytics_storage" }] } } ``` ### Invalid `{` / `}` in entity names GTM rejects `{` / `}` in **tag / trigger / variable names**. An earlier draft named a tag `GA4 Event - Custom Event ({{Event}})`, which failed import with *“The name contains invalid character: `{`”*. That tag is now **`GA4 Event - Catch-all Custom Events`**. The GA4 **event name parameter** still uses `{{Event}}` (built-in Event variable) — macro syntax is valid in parameter **values**, not in entity names. ## Prerequisites 1. Create a **GA4** property and note its Measurement ID (`G-…`). 2. Create a **GTM Web** container (or use an existing empty workspace). Note the container public ID (`GTM-…`). 3. App loads GTM only when `PUBLIC_GTM_ID` is a valid `GTM-…` id (see root `.env.example` and `apps/web/src/lib/analytics.ts`). ## Import steps (re-import) 1. Open [Google Tag Manager](https://tagmanager.google.com/) → your **Web** container. 2. **Admin** → **Import Container**. 3. Choose `docs/gtm/descrybe-web-container.json`. 4. Choose a **workspace** (new or existing). 5. Import option: - Prefer **Merge** into an empty/new workspace, or - **Overwrite** only if you intend to replace the workspace contents. - If you previously failed mid-import or have a partial copy, use a **fresh workspace** (or Overwrite) so you do not keep the old invalid-named tag. 6. After import, open **Variables** → **Constant - GA4 Measurement ID** and set the value to your real Measurement ID (replace `G-XXXXXXXX`). 7. **Consent (verify):** each GA4 tag imports with **Additional Consent Checks** requiring **`analytics_storage`** (`consentStatus` = `NEEDED`, list item type `TEMPLATE`). If Preview shows tags firing without analytics consent, open each GA4 tag → **Advanced settings** → **Consent Settings** → require `analytics_storage`. Add `ad_storage` / `ad_user_data` / `ad_personalization` only if you later add ads tags. 8. **Preview** with Tag Assistant against a local/staging site that has analytics consent granted. 9. **Submit** → **Publish**. 10. Set **`PUBLIC_GTM_ID`** in the web env to the container’s public ID (`GTM-…`) so it matches the published container. The placeholder `GTM-XXXXXXX` in the JSON is **not** a real container id — GTM assigns the public ID when you create the container; import merges into *that* container. ## What the container includes ### Variables | Name | Type | Notes | |------|------|--------| | `Constant - GA4 Measurement ID` | Constant | **Replace** `G-XXXXXXXX` | | `DL - value` / `currency` / `items` / `transaction_id` | Data Layer | Ecommerce fields (version 2) | | `DL - page_path` / `page_title` / `page_location` | Data Layer | SPA `page_view` | | `DL - billing_term` / `plan` / `pack_id` | Data Layer | Flat extras on checkout/purchase | Built-ins enabled: Event, Page URL, Page Path, Page Hostname, Referrer. ### Triggers (Custom Event only) | Trigger | Event name | |---------|------------| | `CE - page_view` | `page_view` | | `CE - begin_checkout` | `begin_checkout` | | `CE - purchase` | `purchase` | | `CE - checkout_canceled` | `checkout_canceled` | | `CE - sign_up` | `sign_up` | | `CE - login` | `login` | | `CE - generate_lead` | `generate_lead` | | `CE - processing_job_started` | `processing_job_started` | | `CE - consent_update` | `consent_update` | | `CE - Other Custom Events (catch-all)` | Regex excluding the above + `gtm.*` | **Do not** add History Change or GA4 enhanced-measurement automatic `page_view` — the app already pushes `page_view` from `trackPageview` / `AnalyticsHost`. ### Tags | Tag | Fires on | Behavior | |-----|----------|----------| | `GA4 Configuration` | Initialization (built-in `2147479573`) | `sendPageView=false`; consent: `analytics_storage` | | `GA4 Event - page_view` | `CE - page_view` | Sends `page_path`, `page_title`, `page_location` | | `GA4 Event - begin_checkout` | `CE - begin_checkout` | Maps `currency`, `value`, `items`, plus `billing_term` / `plan` / `pack_id` | | `GA4 Event - purchase` | `CE - purchase` | Same ecommerce fields + `transaction_id` + flat extras | | `GA4 Event - Catch-all Custom Events` | Named core events + catch-all | Event name parameter = `{{Event}}` (dataLayer event name) | ## Ecommerce mapping (`begin_checkout` / `purchase`) Expected dataLayer shape (flat, same object as `event`): ```js { event: "begin_checkout", // or "purchase" currency: "USD", value: 99, items: [ { item_id: "pro", item_name: "Pro", item_category: "subscription", // or "credit_pack" price: 99, quantity: 1 } ], // purchase only: transaction_id: "cs_test_…", // optional flat extras: billing_term: "monthly", plan: "pro", pack_id: "pack_…" } ``` Dedicated tags map those DL variables into GA4 event parameters. **List / marketing prices may be approximates** (e.g. plan card pricing before Stripe tax/discounts); use Stripe / server data for finance-grade reporting. ## Consent The app sets Consent Mode v2 defaults to denied, then updates via the CMP (`updateConsentMode` + `consent_update` dataLayer event). GTM tags are gated with **Additional Consent Required → `analytics_storage`**. Client-side `trackEvent` / `trackPageview` also no-op until analytics is granted. ## After publish — env checklist ```text PUBLIC_GTM_ID=GTM-XXXXXXXX # must match the published container public ID ``` Do not put the GA4 Measurement ID in the web app env for primary loading — measurement ID lives in the GTM variable; the app loads GTM only. ## Re-import after this fix If a previous import failed on `STRING` (or left a partial workspace): 1. Use a **new workspace** (or **Overwrite** on an empty one) so you do not keep a half-imported state. 2. **Admin** → **Import Container** → choose `docs/gtm/descrybe-web-container.json`. 3. Confirm all five GA4 tags show Consent → require `analytics_storage`. 4. Set **Constant - GA4 Measurement ID**, Preview, then Publish. ## Limitations / manual fix-ups - **Placeholder account/container IDs** (`0000000000` / `GTM-XXXXXXX`) are rewritten by GTM on import into *your* container. If import rejects metadata, create an empty Web container first, then **Merge** this file. - **Fingerprints** are synthetic; GTM regenerates them after import. - **Catch-all** does not forward arbitrary event parameters to GA4 (only the event name). Dedicated ecommerce tags carry `value` / `currency` / `items` / `transaction_id`. Add event-parameter tables for other events if needed. - **No ads tags** in this export — no `ad_*` consent requirements yet. - **`items` as a GA4 event parameter** relies on GTM/GA4 accepting the DL array via the event settings table. If Preview shows empty items, switch the ecommerce tags to “Send ecommerce data” and push an `ecommerce: { … }` object from the app, or verify in Tag Assistant that `{{DL - items}}` resolves. - Re-export from GTM after your first successful import if you want a fingerprint-perfect baseline in git.