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.

Published · Tags: GTM, Google Analytics 4, UUID, First-Party Cookie, CSP, cyrb53, Cookie Creator, Sandboxed JavaScript, dataLayer

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.

⬇ Download the GTM Container JSON from Google Drive

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

FolderContains
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

TagTypePurpose
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

TriggerTypeFires 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.

CSP in brief: The browser computes a SHA-256 digest of every inline <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 ParameterValue
Cookie Name{{UUID Cookie Name}}_i_uuid
Cookie Value{{UUID Hash Value}} (cyrb53 hash — see below)
Domain{{Root Domain}} (e.g. .example.com)
Path/
Expiry24 months
Securefalse (set to true on HTTPS-only sites)
SameSiteNone

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:

  1. 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.
  2. 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 new max-age derived from the Cookie Duration (Days) parameter (720 days in this recipe). Cookies that do not exist are silently skipped.
  3. Set the session guard. It writes _arrewr=1 as a pure session cookie (no max-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.

Why DOM Ready? The Extend Cookie Life Trigger fires at DOM Ready rather than Page View to give the GA4 config tag (and therefore the 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:

ConditionPatternLogic
{{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
Why not 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):

VariableSourceUse
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:

  1. Measurement Stream RegEx Lookup checks {{Page URL}} against a pattern matching non-production hostnames (dev|uat|qa|orig|stg|stage|staging|int|local|admin|aem as URL prefixes, plus the GTM Preview appspot domain). If matched, it returns {{Measurement Stream ID Development}}; otherwise it falls through to the next variable.
  2. 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

TemplateSourceRole 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

VariableTypePurpose
Browser Client IDanalytics_storageGA4 client_id from the _ga cookie — not the same as the UUID this recipe creates.
Browser Session IDanalytics_storageGA4 session_id from the _ga_XXXX cookie.
Browser Session ID RegEx TableRegEx TableExtracts 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 IDRegEx TableExtracts the first-visit epoch timestamp embedded in the GA4 client ID.
Random NumberRandom Number (built-in)GTM built-in entropy source. Used both as the cyrb53 seed and as part of the hash input string.
UUID Hash Valuecyrb53 Hasher templateThe computed base-64 cyrb53 hash that becomes the UUID cookie value.
UUID Cookie NameConstantThe literal string _i_uuid.
UUID Cookie Value1st Party CookieReads the _i_uuid cookie from the browser. Evaluates to the empty/falsy pattern when no UUID yet exists.
Extend Cookie Life Already Ran Cookie NameConstantThe literal string _arrewr — the session guard cookie name.
Extend Cookie Life Already Ran Cookie Value1st Party CookieReads _arrewr from the browser. Used by the Extend Cookie Life Trigger condition.
Root DomainGet Root Domain templateRegistrable domain with leading dot, e.g. .example.com.
Root Domain - OldIf Else If templateLegacy fallback root domain calculation. Not wired to active tags.
Timestamp CurrentTimestamp templateUnix epoch at evaluation time.
Timestamp First UserIf Else If templateFirst-visit epoch from GA4 client ID.
Timestamp Session StartIf Else If templateSession-start epoch from GA4 session ID.
Measurement Stream ID ProductionConstantYour production GA4 Measurement ID. Placeholder: G-AAAAAAAA.
Measurement Stream ID DevelopmentConstantYour development GA4 Measurement ID. Placeholder: G-ZZZZZZZZ.
Environment Stream IDRegEx TableRoutes to prod or dev stream ID based on GTM Environment Name.
Measurement Stream RegEx LookupRegEx TableTop-level hostname check — sends non-production URLs to the dev stream regardless of environment.
Measurement Stream ID RegEx TableRegEx TableAdditional session ID processing utility.
Browser Client ID Append UnderscoreIf Else If templateAppends a trailing underscore to the client ID for downstream joins.
GTM Environment NameEnvironment (built-in)GTM's own environment name string.
GTM Container IDContainer ID (built-in)GTM's own container ID.
Page TitleJavaScript Variabledocument.title.
Undefined ValueUndefined Variable (built-in)Explicitly undefined — used as a safe default in lookup tables.
Google Tag Shared Configuration SettingsGoogle Tag Configuration SettingsShared config settings object passed to the GA4 tag.
Google Tag Shared Event SettingsGoogle Tag Event SettingsShared event settings object passed to the GA4 tag.

End-to-End Flow

Here is the full sequence from page load to stable UUID cookie:

  1. Page view fires. GTM initialises. The GA4 config tag fires on All Pages, populating client_id and session_id in the analytics_storage context.
  2. 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)?
  3. 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.
  4. Returning visit — no creation. On subsequent page views where the _i_uuid cookie already exists, {{UUID Cookie Value}} is non-empty, the trigger condition is not met, and the Cookie Creator tag does not fire.
  5. DOM Ready — cookie life extension. The Extend Cookie Life Trigger evaluates: is _arrewr absent? At the start of each new browser session, it is. The Cookie Rewriter tag fires, re-stamps _i_uuid with a fresh 720-day expiry, and sets _arrewr=1 as a session cookie.
  6. 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.
  7. New session starts. The browser's session cookie _arrewr is gone. The cycle repeats from step 5.

How to Import

  1. Download the JSON file from the Google Drive link above.
  2. In GTM, go to Admin → Import Container.
  3. Upload generate-unique-user-uuid-recipe.json.
  4. Choose Merge (not Overwrite) to preserve your existing tags and triggers.
  5. Update Measurement Stream ID Production and Measurement Stream ID Development to your actual GA4 Measurement IDs.
  6. Update the Measurement Stream RegEx Lookup variable with any non-production hostname patterns specific to your environment.
  7. If your site uses HTTPS exclusively (it should), set checkbox1Secure = true in the Create UUID tag parameters.
  8. Open GTM Preview mode, load your site, and confirm that _i_uuid appears in Application → Cookies in DevTools after the first page view.
  9. On a second page load in the same session, confirm the Create UUID tag does not fire and the _arrewr session cookie is present.
  10. Clear cookies, reload, and confirm the entire flow runs again from the beginning.
Custom Template gallery availability: The cyrb53 Hasher, Timestamp, Get Root Domain, and If Else If — Advanced Lookup Table templates are sourced from the GTM Community Template Gallery. If any of them are not available in your GTM account region's gallery, import them manually from their respective GitHub repositories using GTM's Templates → New → Import flow. The Cookie Creator and Cookie Rewriter templates are bundled directly inside the container JSON and will be imported automatically.

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_uuid as 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

Popular posts from this blog

Site Landing & Site Referrer Preservation

GTM Browser Viewport Measurement

UTM & URL Query String 2 Cookies