google analytics new cookies GS2 format

Google quietly changed the GA4 cookie format (GS2)

Published:

Updated:

Categories:

,

Yesterday your tracking was “fine”. Today:

  • Measurement Protocol “works”… but attribution is dead
  • session-based reporting looks off
  • server-side GTM sends requests… yet the data stitching is trash
  • no errors, no warnings, no “we changed something” banner

That’s usually the worst kind of bug: everything fires, but everything lies.

What happened: GA4 started rolling out a new format for its session-state cookie around May 6, 2025. The community calls it GS2.

Screenshot by: Elvinas Karalis

Quick clarification: which GA4 cookies are we talking about?

GA4 web tagging typically sets (at least) these two cookies:

  • _gaused to distinguish users
  • _ga_<container-id>used to persist session state (this is the one that changed format)

So if your logic touches _ga_<MEASUREMENT_ID> (example: _ga_SM37QL5J42) to extract session data — welcome to the party.

What the new GA4 cookie format looks like

A typical GS2 cookie value looks like this (example contains session info):

GS2.1.s1747132561$o1$g0$t1747132655$j0$l0$h0

What changed?

Before, many people relied on fixed positions (split by “.” and pick item #3). With GS2, that approach dies, because the data is now prefix-based and separated by $.

How to extract session_id in GS2

In GS2, session_id is the value after “s”.

So in: GS2.1.s1747132561$o1$g0$t1747132655$j0$l0$h0 -> session_id = 1747132561

Common prefixes you’ll see:

  • s — session id
  • o — session number
  • g — session engaged
  • t — last hit timestamp
  • j — join timer
  • l — logged-in state
  • h — (hash / enhanced user id-related)

Also: order is not guaranteed. That’s kind of the point of GS2.


Who gets hurt (aka “why your pipeline suddenly smells”)

Screenshot by: Matteo Zambon

1) Measurement Protocol setups

If you send MP events and you manually extract identifiers from cookies (especially session_id), your session stitching can break.

GA4 added session_id support specifically so MP events can appear in session-based reporting, and recommends including it when you care about session context.

If your parser now returns garbage (or nothing), you get “valid requests” with broken joins.

2) Server-side GTM (and any custom server collector)

A lot of sGTM or middleware code reads cookie headers and does something like:

  • split('.')
  • take the 3rd/4th chunk
  • call it session_id

With GS2, that becomes random output.

3) CDPs / integrations / identity stitching

Segment-like pipelines, RudderStack-style collectors, custom CDPs, lead matching scripts — anything that used _ga_* values as stable identifiers is now at risk.

4) Bonus damage: WAF / security rules

Some security rules started flagging GS2 values because $ looks “suspicious” to regex-based detectors. There are real reports of ModSecurity/OWASP CRS rules creating false positives on GS2...$... cookies.

Why this is bad

  • No obvious error signal. Your systems keep sending requests. GA4 keeps accepting them. Your dashboards quietly reflect nonsense.
  • Google’s public docs describe cookie names and purposes, not the internal format — so if you built logic on a reverse-engineered string format, you were always one silent update away from pain.

What to do now

Step 1 — Audit everything that touches GA cookies directly

Search your codebase and tag configs for:

  • _ga_
  • document.cookie
  • .split('.') on GA cookies
  • regexes that assume dot-separated GS1 layout
  • server collectors parsing Cookie: header

Make a list. Fix in priority order: MP → sGTM → CRM/CDP → attribution glue.

Step 2 — Use official APIs (stop reverse-engineering cookie strings)

Option A: gtag('get') — official way to read client_id and session_id

Google literally documents pulling both values via the get command:

gtag('get', 'TAG_ID', 'client_id', (client_id) => {
  // store client_id
});

gtag('get', 'TAG_ID', 'session_id', (session_id) => {
  // store session_id
});

This is the clean approach because you’re no longer coupled to cookie formatting.

If you’re sending Measurement Protocol events from your backend, capture these values client-side and pass them with your event payload.

Also keep in mind: GA4’s docs recommend session_id when you want session-specific joins, and they note timing constraints (send close to the session).

Option B (GTM users): readAnalyticsStorage API (official)

After the GS2 mess, GTM introduced an official sandboxed API called readAnalyticsStorage (Aug 1, 2025) explicitly because reverse-engineering cookie formats is fragile. It returns:

  • client_id
  • sessions[] with measurement_id, session_id, session_number

Option C (even easier): GTM built-in variables (Dec 11, 2025)

GTM also shipped built-in variables in Utilities:

  • Client ID
  • Session ID
  • Session Number

If you’re still running custom cookie parsers inside GTM in 2026 — this is your exit.


Step 3 — If you must parse cookies, parse GS2 defensively (not by position)

Sometimes reality forces cookie parsing (custom stacks, blocked tags, partial consent flows, etc.). If you go that route, at least do it in a way that survives GS3.

Here’s a practical JS parser approach (GS1 + GS2):

function parseGa4SessionCookie(value) {
  if (!value) return null;

  // Cookies might arrive URL-encoded in some systems
  const v = decodeURIComponent(value);

  // GS2: "GS2.1.s...$o...$g...$t...$j...$l...$h..."
  if (v.startsWith('GS2.')) {
    const dotParts = v.split('.');
    const body = dotParts.slice(2).join('.'); // everything after "GS2.<x>."
    const chunks = body.split('$');

    const map = {};
    for (const c of chunks) {
      if (!c) continue;
      map[c[0]] = c.slice(1); // 's1747...' -> { s: '1747...' }
    }

    return {
      format: 'GS2',
      session_id: map.s || null,
      session_number: map.o || null,
      session_engaged: map.g || null,
      last_hit_ts: map.t || null,
    };
  }

  // GS1: "GS1.1.<session_id>.<session_number>.<engaged>.<last_hit>...."
  if (v.startsWith('GS1.')) {
    const p = v.split('.');
    return {
      format: 'GS1',
      session_id: p[2] || null,
      session_number: p[3] || null,
      session_engaged: p[4] || null,
      last_hit_ts: p[5] || null,
    };
  }

  return null;
}

If you implement something like this, you’re at least reading by meaning, not by “third dot from the left”. The GS2 format is explicitly designed for that kind of parsing.

Step 4 — Validate the fix (don’t trust “it fires”)

A few checks that catch silent breakage fast:

  • Compare sessions / engaged sessions around the rollout date (May 6, 2025) for weird discontinuities.
  • If you use MP: verify you’re passing a real client_id, and when you need session stitching, pass session_id as well (GA4 specifically supports it for session-based reporting).
  • Check for spikes in (direct) / (none) and “unassigned”-looking behavior (classic symptom when identifiers fail).
  • If you have a WAF: search logs for the literal substring GS2.1. and $o / $g patterns (false positives are a thing).

TL;DR

If you absolutely must parse: parse by prefix (s, o, g, …), not by position.

GA4 rolled out a new GS2 format for session-state cookies around May 6, 2025.

If you manually parse _ga_<MEASUREMENT_ID> and expect the old dot-separated structure — your session_id / session_number extraction can silently break.

Stop treating cookies as a stable API:

use gtag('get', ..., 'client_id' | 'session_id')

in GTM, use readAnalyticsStorage or the newer built-in variables


Leave a Reply

Your email address will not be published. Required fields are marked *