GTM 1st Party UUID - No JavaScript
Generating a Persistent First-Party User UUID in GTM — No Custom JavaScript, No CSP Hashes
Create and maintain a stable cross-session user identifier entirely inside Google Tag Manager's sandboxed template system — using the Cookie Creator community template and a hand-rolled Cookie Rewriter sandboxed template.
GA4's built-in client_id is a first-party cookie — but it is owned by Google and can only be
read through GTM's analytics_storage variable type. If you need a stable, portable, first-party
user identifier that you control entirely — one you can join to your own data warehouse, use in server-side
tagging, or send as a custom dimension — you need to create it yourself.
The standard approach is a Custom HTML tag with a <script> block that reads, generates,
and writes a cookie. That works, but it forces you to maintain a SHA-256 hash in your Content Security
Policy for every version of the script you deploy. This recipe takes a different path: it uses GTM's
sandboxed Custom Template system exclusively, so no new CSP hashes are ever needed.
What's Inside the Container
The exported container (generate-unique-user-uuid-recipe.json) is organised into three
folders, three tags, two triggers, and twenty-seven variables, backed by six Custom Templates.
Folders
| Folder | Contains |
|---|---|
| Analytics | GA4 configuration tag, shared settings variables, environment/stream routing, Browser Client ID utilities, timestamp variables, and the Root Domain helper. |
| UUID Cookie Creator | The Create UUID tag, its trigger, and the three variables that feed it: UUID Cookie Name, UUID Hash Value, and UUID Cookie Value. |
| Extend Cookie Life | The Extend Cookie Life Tag and its trigger, plus the two session-guard cookie variables that prevent it from running more than once per session. |
Tags
| Tag | Type | Purpose |
|---|---|---|
Google Analytics Configuration |
Google Tag (googtag) |
Standard GA4 config tag wired to environment-aware Stream IDs. Fires once per page load on the built-in All Pages trigger. |
Create UUID |
Cookie Creator custom template | Generates a new UUID cookie (_i_uuid) on the first page view where no UUID cookie yet exists. Uses {{UUID Hash Value}} as the cookie value, sets a 24-month expiry, root-domain scope, SameSite=None, and the / path. |
Extend Cookie Life Tag |
Cookie Rewriter custom template (hand-written) | On each new browser session, rewrites the existing _i_uuid cookie (and any others listed in its table) with a fresh 720-day expiry, preventing drift toward the browser's ITP cap. A session-scoped guard cookie (_arrewr) ensures the tag runs at most once per session. |
Triggers
| Trigger | Type | Fires When |
|---|---|---|
Create UUID |
Page View | {{UUID Hash Value}} is a valid non-empty value and {{UUID Cookie Value}} matches the empty/falsy pattern — meaning a hash was computed but no cookie yet exists. |
Extend Cookie Life Trigger |
DOM Ready | {{Extend Cookie Life Already Ran Cookie Value}} matches the empty/falsy pattern — meaning the session guard cookie _arrewr is not yet set for this session. |
The Core Problem: Why Not a Custom HTML Tag?
A Custom HTML tag that reads and writes document.cookie is the first thing most
practitioners reach for when they need a first-party UUID. It is conceptually simple and it works —
but it carries a hidden maintenance cost.
Any site that enforces a Content Security Policy with a script-src directive must either
whitelist the GTM domain (which grants broad script execution permissions) or add a per-script
SHA-256 hash to the CSP header. GTM's sandboxed Custom Templates are categorically exempt from this
requirement: they run inside GTM's own JavaScript sandbox, not as inline scripts, so they never
appear in the page's script execution context at all. This recipe uses that exemption deliberately —
not as a workaround but as the architecturally correct choice.
<script> block before executing it. If the digest does not match an entry in the
script-src CSP header, the script is blocked. GTM's sandboxed templates bypass this
entirely — they are never injected as inline scripts, so they never trigger the digest check.
The Two Custom Templates at the Centre of This Recipe
Cookie Creator (community template, modified)
The Cookie Creator template, authored by
gtm-templates-anto-hed
and imported from the GTM Community Template Gallery, is a sandboxed tag template that calls
GTM's native setCookie() API. It exposes a UI for all standard cookie attributes —
name, value, domain, path, expiry type and duration, Secure flag, SameSite policy — without
requiring any custom JavaScript.
The container uses a modified version of the gallery template (the
isModified: true flag is set in the gallery reference). The modification adds support
for the SameSite=None value, which the upstream version does not expose. Because the
template still uses only GTM's sandboxed setCookie() API, no CSP consideration
changes.
The Create UUID tag uses the Cookie Creator with these settings:
| Cookie Creator Parameter | Value |
|---|---|
| Cookie Name | {{UUID Cookie Name}} → _i_uuid |
| Cookie Value | {{UUID Hash Value}} (cyrb53 hash — see below) |
| Domain | {{Root Domain}} (e.g. .example.com) |
| Path | / |
| Expiry | 24 months |
| Secure | false (set to true on HTTPS-only sites) |
| SameSite | None |
Cookie Rewriter (hand-written sandboxed template)
The Cookie Rewriter template is a custom sandboxed tag template written specifically for this recipe. It does not come from the community gallery — it lives inside the exported container JSON and is imported with it.
Its job is to combat ITP (Intelligent Tracking Prevention) and similar browser policies that
progressively cap the effective lifetime of first-party cookies set by JavaScript. By re-writing
the cookie on each new browser session with a fresh max-age, the UUID cookie's
clock is reset before ITP can shorten it below the intended 24-month window.
The Cookie Rewriter works in three steps per page load:
- Session guard check. It reads the session cookie
_arrewr. If_arrewr === '1', the tag exits immediately — it has already run this session and there is nothing more to do. No cookies are touched. - Rewrite target cookies. For each cookie name listed in the
Persistent Cookies to Rewrite table, it reads the cookie's current value. If the cookie
exists and has a value, it calls GTM's sandboxed
setCookie()with the same value but a newmax-agederived from the Cookie Duration (Days) parameter (720 days in this recipe). Cookies that do not exist are silently skipped. - Set the session guard. It writes
_arrewr=1as a pure session cookie (nomax-age, no domain attribute) so the guard activates for the remainder of the session regardless of how many more pages load.
The Cookie Rewriter uses only GTM's sandboxed getCookieValues() and
setCookie() APIs. It includes a full unit test suite in the template's
___TESTS___ section, covering first-run, already-ran, missing-cookie, and empty-list
scenarios.
client_id) time to
initialise before the rewriter runs. This matters if you ever want to add client_id
to the rewrite list alongside _i_uuid.
How the UUID Is Generated
The UUID value is not a random string generated at tag-fire time — it is a deterministic cyrb53 hash of two values that change each time a new session starts:
dataStr = {{Random Number}}.{{Browser Session ID RegEx Table}}
The {{UUID Hash Value}} variable uses the
cyrb53 Hasher community template (by
mbaersch), configured
with output64 = true (base-64 encoded output) and seed = {{Random Number}}.
The input string concatenates a fresh random number with the current GA4 session ID (after RegEx
extraction to strip the session-start timestamp suffix).
The trigger conditions ensure the tag only fires when the hash resolves to a real value and the UUID cookie does not already exist:
| Condition | Pattern | Logic |
|---|---|---|
{{UUID Hash Value}} matches .* |
Any non-empty string | Ensures the cyrb53 template returned a real hash (not undefined on first tag manager load) |
{{UUID Hash Value}} does NOT match ^(undefined|null|0|false|NaN|)$ |
Falsy exclusion | Prevents the tag from firing with a junk hash value |
{{UUID Cookie Value}} matches ^(undefined|null|0|false|NaN|)$ |
Cookie absent | Fires only if the _i_uuid cookie does not yet exist in the browser |
Math.random() directly? GTM's sandboxed JavaScript
environment restricts access to Math.random() from within Custom Template code.
The {{Random Number}} built-in variable (which GTM evaluates in the
main execution context) is the correct way to introduce entropy into a template-based tag flow.
Timestamp Variables
Three timestamp variables are included in the Analytics folder, produced by the Timestamp community template (by luratic):
| Variable | Source | Use |
|---|---|---|
Timestamp Current |
Timestamp template, current time | The Unix timestamp at the moment of tag evaluation — useful as a hit timestamp in GA4 events. |
Timestamp First User |
RegEx extraction from {{Browser Client ID}} via {{Extract First User Session ID Timestamp from Client ID}} |
The epoch timestamp embedded in the GA4 client ID — represents when the user first visited. |
Timestamp Session Start |
RegEx extraction from {{Browser Session ID}} via {{Browser Session ID RegEx Table}} |
The epoch timestamp embedded in the GA4 session ID — represents when the current session began. |
These are not used by the UUID flow directly, but they are included as companion utilities for analytics implementations that need to surface first-visit and session-start times as GA4 event parameters or BigQuery dimensions.
Root Domain Detection
The {{Root Domain}} variable uses the Get Root Domain
community template (by mbaersch)
to extract the registrable domain from the current {{Page Hostname}} with the
www prefix stripped. This ensures that cookies set by the Create UUID tag are scoped
to the root domain (e.g. .example.com) and are therefore accessible across all
subdomains.
A legacy variable {{Root Domain - Old}} is retained as a fallback but is not wired
into the active tags. If the Get Root Domain template is unavailable in your GTM account's
template gallery region, substitute a Regex Replace variable on {{Page Hostname}}
with the pattern ^(?:www\.)?(.+)$ and replacement .$1.
Environment-Aware Stream ID Routing
The container ships with the same two-tier routing chain as other recipes in this series:
- Measurement Stream RegEx Lookup checks
{{Page URL}}against a pattern matching non-production hostnames (dev|uat|qa|orig|stg|stage|staging|int|local|admin|aemas URL prefixes, plus the GTM Preview appspot domain). If matched, it returns{{Measurement Stream ID Development}}; otherwise it falls through to the next variable. - Environment Stream ID checks the GTM
{{Environment Name}}built-in. Live environments match^liv.*; staging and pre-production match^(pre|de|st|ua|qa|te).*. The default value is{{Measurement Stream ID Production}}.
Update Measurement Stream ID Production and Measurement Stream ID Development
(both simple Constant variables) to your actual GA4 Measurement IDs before publishing.
All Six Custom Templates
| Template | Source | Role in Recipe |
|---|---|---|
| Cookie Creator | GTM gallery — gtm-templates-anto-hed (modified) | Tag template used by Create UUID to write the _i_uuid cookie via GTM's sandboxed setCookie() API. |
| Cookie Rewriter | Hand-written (bundled in this container) | Tag template used by Extend Cookie Life Tag to re-stamp the UUID cookie's expiry each new session without re-generating the value. |
| cyrb53 Hasher | GTM gallery — mbaersch | Variable template used by {{UUID Hash Value}} to produce a base-64 encoded cyrb53 hash of the random-number + session-ID input string. |
| Timestamp | GTM gallery — luratic | Variable template behind Timestamp Current, Timestamp First User, and Timestamp Session Start. |
| Get Root Domain | GTM gallery — mbaersch | Variable template behind {{Root Domain}} — extracts the registrable domain for correct cross-subdomain cookie scoping. |
| If Else If — Advanced Lookup Table | GTM gallery — sublimetrix | Variable template used by Browser Client ID Append Underscore, Timestamp First User, and Timestamp Session Start for conditional value selection. |
Complete Variable Inventory
| Variable | Type | Purpose |
|---|---|---|
Browser Client ID | analytics_storage | GA4 client_id from the _ga cookie — not the same as the UUID this recipe creates. |
Browser Session ID | analytics_storage | GA4 session_id from the _ga_XXXX cookie. |
Browser Session ID RegEx Table | RegEx Table | Extracts the raw session timestamp from the full session ID string; used as input entropy for the UUID hash and for Timestamp Session Start. |
Extract First User Session ID Timestamp from Client ID | RegEx Table | Extracts the first-visit epoch timestamp embedded in the GA4 client ID. |
Random Number | Random Number (built-in) | GTM built-in entropy source. Used both as the cyrb53 seed and as part of the hash input string. |
UUID Hash Value | cyrb53 Hasher template | The computed base-64 cyrb53 hash that becomes the UUID cookie value. |
UUID Cookie Name | Constant | The literal string _i_uuid. |
UUID Cookie Value | 1st Party Cookie | Reads the _i_uuid cookie from the browser. Evaluates to the empty/falsy pattern when no UUID yet exists. |
Extend Cookie Life Already Ran Cookie Name | Constant | The literal string _arrewr — the session guard cookie name. |
Extend Cookie Life Already Ran Cookie Value | 1st Party Cookie | Reads _arrewr from the browser. Used by the Extend Cookie Life Trigger condition. |
Root Domain | Get Root Domain template | Registrable domain with leading dot, e.g. .example.com. |
Root Domain - Old | If Else If template | Legacy fallback root domain calculation. Not wired to active tags. |
Timestamp Current | Timestamp template | Unix epoch at evaluation time. |
Timestamp First User | If Else If template | First-visit epoch from GA4 client ID. |
Timestamp Session Start | If Else If template | Session-start epoch from GA4 session ID. |
Measurement Stream ID Production | Constant | Your production GA4 Measurement ID. Placeholder: G-AAAAAAAA. |
Measurement Stream ID Development | Constant | Your development GA4 Measurement ID. Placeholder: G-ZZZZZZZZ. |
Environment Stream ID | RegEx Table | Routes to prod or dev stream ID based on GTM Environment Name. |
Measurement Stream RegEx Lookup | RegEx Table | Top-level hostname check — sends non-production URLs to the dev stream regardless of environment. |
Measurement Stream ID RegEx Table | RegEx Table | Additional session ID processing utility. |
Browser Client ID Append Underscore | If Else If template | Appends a trailing underscore to the client ID for downstream joins. |
GTM Environment Name | Environment (built-in) | GTM's own environment name string. |
GTM Container ID | Container ID (built-in) | GTM's own container ID. |
Page Title | JavaScript Variable | document.title. |
Undefined Value | Undefined Variable (built-in) | Explicitly undefined — used as a safe default in lookup tables. |
Google Tag Shared Configuration Settings | Google Tag Configuration Settings | Shared config settings object passed to the GA4 tag. |
Google Tag Shared Event Settings | Google Tag Event Settings | Shared event settings object passed to the GA4 tag. |
End-to-End Flow
Here is the full sequence from page load to stable UUID cookie:
- Page view fires. GTM initialises. The GA4 config tag fires on All Pages, populating
client_idandsession_idin theanalytics_storagecontext. - UUID check. The Create UUID trigger evaluates: does
{{UUID Hash Value}}produce a valid hash? Does{{UUID Cookie Value}}come back empty (i.e. no existing UUID cookie)? - First visit — cookie creation. If both conditions are met, the Create UUID tag fires the Cookie Creator, writing
_i_uuid=<cyrb53-hash>with a 24-month expiry on the root domain. - Returning visit — no creation. On subsequent page views where the
_i_uuidcookie already exists,{{UUID Cookie Value}}is non-empty, the trigger condition is not met, and the Cookie Creator tag does not fire. - DOM Ready — cookie life extension. The Extend Cookie Life Trigger evaluates: is
_arrewrabsent? At the start of each new browser session, it is. The Cookie Rewriter tag fires, re-stamps_i_uuidwith a fresh 720-day expiry, and sets_arrewr=1as a session cookie. - Subsequent pages this session — guard activates. On every subsequent page load within the same session,
_arrewr === '1', the Extend Cookie Life Trigger condition is not met, and the rewriter tag is skipped entirely. - New session starts. The browser's session cookie
_arrewris gone. The cycle repeats from step 5.
How to Import
- Download the JSON file from the Google Drive link above.
- In GTM, go to Admin → Import Container.
- Upload
generate-unique-user-uuid-recipe.json. - Choose Merge (not Overwrite) to preserve your existing tags and triggers.
- Update
Measurement Stream ID ProductionandMeasurement Stream ID Developmentto your actual GA4 Measurement IDs. - Update the
Measurement Stream RegEx Lookupvariable with any non-production hostname patterns specific to your environment. - If your site uses HTTPS exclusively (it should), set
checkbox1Secure = truein the Create UUID tag parameters. - Open GTM Preview mode, load your site, and confirm that
_i_uuidappears in Application → Cookies in DevTools after the first page view. - On a second page load in the same session, confirm the Create UUID tag does not fire and the
_arrewrsession cookie is present. - Clear cookies, reload, and confirm the entire flow runs again from the beginning.
Using the UUID in GA4
The UUID stored in _i_uuid is a standalone first-party cookie. To send it to GA4
as a user property or event parameter:
- Create a new First Party Cookie variable in GTM reading
_i_uuid(this is the{{UUID Cookie Value}}variable already in the container). - Add it to the Google Tag Shared Event Settings variable as a user property named
user_uuid, or send it as an event parameter on specific events. - Register
user_uuidas a custom user-scoped dimension in GA4 under Admin → Custom definitions.
Because it is a first-party cookie you control, you can also read it server-side (in your own analytics pipeline or server-side GTM container) without going through the GA4 API.
The full source — container JSON, Cookie Rewriter template with test suite, and variable inventory — is available on GitHub. If you extend the cookie rewrite list, increase the ancestor depth, or add additional entropy sources to the hash input, open a pull request or issue.
⬇ Download GTM Container JSON
Comments
Post a Comment