How to add Session ID to GA4 Measurement Protocol events?

Table of Contents

May 2025 Update
Google has changed the way these cookies are structured without prior notice, causing many implementations to break. The post has been updated to reflect this change.

I was recently helping a client migrate to GA4 (it’s right around the corner, July 1st, 2023). This client, a SaaS B2B product, has a complex funnel, in which deals are closed offline via sales representatives. Most leads are generated on the marketing site and sent directly to their CRM (Hubspot), and from there the client wanted to report that conversion back to both GA4 and the various marketing platforms. Unsurprisingly, the answer was using GA’s Measurement Protocol API to report to GA. 

But the reports connecting the conversion to user visits were broken and nobody knew why.

image-9727972

When I started investigating it, I found out that they didn’t pass the session_id param in the Measurement Protocol hit to GA4. HOLD ON? Sessions? In GA4? YES!

Universal analytics was based on sessions, and once you passed the ga cookie id (i.e. client id) in the measurement protocol hit, then GA would match that hit to its corresponding “place”, but GA4 is built on events and not sessions, and this is why we pass to “tell” GA the session_id.

Google hasn’t done the best job in terms of documentation on this one, to say the least. I went over the GA 4 documentation for measurement protocol and CTR+F’d for “session_id”, and came up short. I was however able to find this release note from May ‘22 introducing the session_id param, and further exploration led me to this thread in which someone named Kevin from the Google Analytics API team acknowledges that Google needs to update their documentation on this matter (but can’t commit on a timeline).

capture-5420803

So let me provide you guys with an answer on how to add session_id to GA4 measurement protocol (and where to take it from). According to Google: “In order for user activity to display in standard reports like Realtime, engagement_time_msec and session_id must be supplied as part of the params for an event.” 

This means that session_id should be part of the params array of the payload as such:

{
"client_id": "123123123",
"events": [
  {
    "name": "offline_purchase",
    "params": {
      "engagement_time_msec": "100",
      "session_id": "123456"
    }
  }
]}

So now that we know where to put it, we need to find how to pull the session_id and from where. When a session_start event is fired a new session_id is created by GA, but it is not part of the _ga cookie like it was in Universal, it’s part of a new cookie that contains the container_id

And looks something like this: _ga_XXXXXXXXXX, where the X’s stand for the Stream’s Measurement ID without the G- prefix.

Here you can see an example, in which the highlighted part is the session_id:

The session_id cookie

Google uses a structured format in its cookies where each section is separated by a $ symbol. Here’s a quick overview of the components you’ll encounter in this string:

  1. GS2.1 – This part defines the version of the cookie and the domain level.
  2. s<timestamp> – Represents the session’s start time, effectively serving as the session ID.
  3. o<number> – Indicates the session count (e.g., o1 means it’s the user’s first session).
  4. g<0|1> – Flags whether the session was engaged: 1 for engaged, 0 for not.
  5. t<timestamp> – Timestamp of the most recent event within the session.
  6. j<number> – Likely a countdown in seconds, potentially used for session timeout or idle tracking.

To extract the session ID from this cookie, you’ll want to target the value that follows the s prefix (e.g., s1746773758) and strip the s to get the raw timestamp.

Now all you have to do is direct your developer to this number to be pulled from the client and then pass it to the measurement protocol hit, as a session_id param under the params array.

Grabbing the Session ID

To grab the Session ID you can use this script:

function getGA4SessionId(cookieSuffix) {
  const targetCookieName = `_ga_${cookieSuffix}`;
  const allCookies = document.cookie.split('; ');
  const ga4Cookie = allCookies.find(cookie => cookie.startsWith(`${targetCookieName}=`));

  if (!ga4Cookie) {
    console.warn(`Cookie ${targetCookieName} not found`);
    return null;
  }

  const cookieValue = ga4Cookie.split('=')[1];
  console.log('GA4 cookie value:', cookieValue);

  // Remove the initial "GS2.1." part
  const strippedValue = cookieValue.replace(/^GS2\.1\./, '');
  const parts = strippedValue.split('$');

  const sessionPart = parts.find(part => part.startsWith('s'));
  if (!sessionPart) {
    console.warn('Session part not found in cookie');
    return null;
  }

  return sessionPart.slice(1); // remove the 's'
}

// Example usage:
const sessionId = getGA4SessionId('XXXX'); // replace with your real suffix
console.log('GA4 Session ID:', sessionId);

Leave a Reply

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

Related Posts