LW IT Solutions
« Blog Overview /Data Privacy/Tutorials / Cookieless Tracking Fallbacks: Synthetic Server-Side Session Stitching

Cookieless Tracking Fallbacks: Synthetic Server-Side Session Stitching

Cookieless Tracking Fallbacks: Synthetic Server-Side Session Stitching
Contents
  1. Architectural Overview: The Dilemma of Cookie Rejection in Analytics
  2. Step-by-Step Implementation Guide
  3. Summary and Measurable Added Value
  4. Questions and answers
  5. Sources

Architectural Overview: The Dilemma of Cookie Rejection in Analytics

In modern web analytics, explicit opt-in consent banners (CMP) result in substantial measurement gaps. When users reject tracking cookies or navigate via browsers enforcing aggressive Intelligent Tracking Prevention (ITP), traditional client-side identifiers such as _ga, _gid, or Matomo visitor IDs are blocked or stripped. Consequently, every pageview from an unconsented user is recorded as an isolated, single-event session, rendering funnel conversion rates, attribution models, and user journey analytics statistically meaningless.

To restore analytical continuity without infringing upon GDPR, ePrivacy, or TTDSG/TDDDG regulations, Server-Side Synthetic Session Stitching can be implemented. Unlike persistent cross-site tracking or fingerprinting, synthetic session stitching operates entirely within a server-side proxy or edge worker (such as Server-Side GTM, Cloudflare Workers, or custom PHP/Node.js endpoints). By calculating an ephemeral, daily-rotating cryptographic hash from non-persistent network attributes—such as a truncated IP subnet, User-Agent, and a server-generated daily salt—sessions can be accurately reconstructed for statistical aggregation while ensuring zero persistent client-side storage and preventing cross-day user identification.

Generation of a synthetic session ID from truncated IP, user agent, accept-language and a daily rotating salt via SHA-256
Path of a synthetic ID: the truncated IP, two stable headers and the daily salt are hashed into a session ID. Nothing is written to the browser — and once the salt rotates, the ID of the previous day can no longer be reproduced.

Step-by-Step Implementation Guide

Step 1: Understanding Ephemeral Privacy-Preserving Hash Generation

Generating a GDPR-compliant synthetic session identifier requires three fundamental safeguards:

  1. IP Subnet Truncation: Full IP addresses must never be hashed directly, as they remain personal data. IPv4 addresses must be masked to the /24 subnet (e.g., 192.0.2.0), and IPv6 addresses to the /48 subnet.
  2. Dynamic Daily Salt: A cryptographic random salt must be generated on the server and rotated automatically at midnight (UTC) at the latest. This guarantees that hashes expire completely within 24 hours at most, making multi-day tracking mathematically impossible.
  3. One-Way Hashing: Attributes must be combined and processed via an irreversible hashing algorithm such as SHA-256.

Step 2: Designing an Edge Worker / Server-Side GTM Hash Generator

The following production-ready JavaScript implementation demonstrates how to generate a privacy-compliant synthetic session ID within a Server-Side GTM custom variable or Cloudflare Edge Worker without writing any HTTP Set-Cookie headers:

/**
 * Privacy-Preserving Synthetic Session Hash Generator
 * Target: Server-Side GTM / Edge Workers
 */
const crypto = require('crypto');

function getSyntheticSessionId(requestHeaders, clientIp, dailySalt) {
    // 1. Anonymize IP address (truncate IPv4 /24 or IPv6 /48)
    let maskedIp = '0.0.0.0';
    if (clientIp.includes('.')) {
        maskedIp = clientIp.split('.').slice(0, 3).join('.') + '.0';
    } else if (clientIp.includes(':')) {
        maskedIp = clientIp.split(':').slice(0, 3).join(':') + '::';
    }

    // 2. Extract stable browser environment headers
    const userAgent = requestHeaders['user-agent'] || 'unknown-ua';
    const acceptLanguage = requestHeaders['accept-language'] || 'unknown-lang';

    // 3. Construct input payload with daily rotating salt
    const rawPayload = `${maskedIp}|${userAgent}|${acceptLanguage}|${dailySalt}`;

    // 4. Generate SHA-256 digest
    const syntheticHash = crypto
        .createHash('sha256')
        .update(rawPayload)
        .digest('hex');

    // Return truncated 16-character ephemeral ID
    return 'syn_' + syntheticHash.substring(0, 16);
}

Step 3: WordPress PHP Collector Integration (functions.php)

For WordPress v7.0.2 infrastructures utilizing custom REST API ingestion or server-side measurement endpoints, the synthetic session calculation can be integrated into a helper utility within functions.php:

/**
 * Server-Side Synthetic Session Hash Utility
 * Target: WordPress v7.0.2
 */
function lw_generate_synthetic_session_id() {
    $client_ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    
    // Mask IPv4 to /24
    if ( strpos( $client_ip, '.' ) !== false ) {
        $ip_parts  = explode( '.', $client_ip );
        $ip_parts[3] = '0';
        $masked_ip = implode( '.', $ip_parts );
    } else {
        // Mask IPv6 to /48
        $ip_parts  = explode( ':', $client_ip );
        $masked_ip = implode( ':', array_slice( $ip_parts, 0, 3 ) ) . '::';
    }

    $user_agent      = ( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ?: 'unknown-ua';
    $accept_language = ( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '' ) ?: 'unknown-lang';
    
    // Retrieve or initialize daily rotating salt from options table
    $daily_salt = get_transient( 'lw_daily_privacy_salt' );
    if ( ! $daily_salt ) {
        $daily_salt = wp_generate_password( 32, true, true );
        // Expire salt at midnight UTC (seconds left in the UTC day)
        set_transient( 'lw_daily_privacy_salt', $daily_salt, DAY_IN_SECONDS - ( time() % DAY_IN_SECONDS ) );
    }

    $raw_payload = $masked_ip . '|' . $user_agent . '|' . $accept_language . '|' . $daily_salt;
    $sha256_hash = hash( 'sha256', $raw_payload );

    return 'syn_' . substr( $sha256_hash, 0, 16 );
}

Step 4: Mapping Synthetic IDs to Analytics Platforms

Once generated, the ephemeral session ID must be passed to the downstream analytics endpoint in place of the missing cookie ID:

  • Google Analytics 4 (GA4): Map the generated value to the client_id parameter in Server-Side GTM while explicitly setting the parameter non_personalized_ads=1 and stripping user IP headers.
  • Matomo / Piwik PRO: Assign the hash to the server-side visitor ID property (_id or cid) and force Cookieless Mode in the measurement configuration.
  • BigQuery Raw Exports: Store the identifier inside a dedicated column (e.g., synthetic_session_id) to separate cookie-based sessions from server-stitched sessions during SQL data modeling.

Step 5: Quality Assurance and Compliance Auditing

To verify that synthetic session stitching operates correctly without violating privacy mandates, validation must be conducted across three vectors:

  1. Network Header Verification: Inspect server responses using browser developer tools (F12) to confirm that no Set-Cookie HTTP headers are transmitted when consent is declined.
  2. Collision and Stability Testing: Generate test requests from identical subnet ranges and verify that different User-Agent strings yield distinct hashes, while sequential pageviews from the same browser resolve to an identical session ID.
  3. Midnight Salt Expiration Audit: Simulate a daily salt reset and verify that all generated session IDs immediately change, confirming that cross-day user profiling is impossible.

Summary and Measurable Added Value

What is achieved: Replacement of broken, single-event pageviews for unconsented traffic with an ephemeral, server-side session reconstruction model that functions without storing cookies or processing unmasked personal IP addresses.

Resulting added value:

  • Restoration of Funnel and Journey Analytics: Statistical conversion rates, bounce rates, and multi-step navigation paths remain approximately measurable for visits without cookie consent as well.
  • Less intrusive than cookie tracking, but not exempt: Nothing is stored on the user device. The hash of subnet and browser headers nevertheless remains pseudonymous and therefore personal data despite the daily salt (GDPR Recital 26); processing it requires a legal basis, and whether it is permissible without consent has to be assessed case by case.
  • Zero Advertising Contamination: By expiring identifiers daily and stripping persistent cross-site profiles, analytical reporting accuracy is preserved without enabling invasive retargeting or profiling.

Questions and answers

How reliably does the hash separate visitors who share a network?

Only as reliably as the inputs differ. The hash merges all requests that come from the same /24 subnet and carry the same User-Agent, and also the same Accept-Language. In a corporate network, on hotel Wi-Fi or behind a mobile operator’s carrier-grade NAT, many people share one address, and common devices with an up-to-date browser often send identical User-Agent strings. Those visits merge into a single synthetic session. Chromium has also reduced the User-Agent string, so it distinguishes less than it used to.

Conversely, a single visit falls apart when the inputs change along the way: a phone moving from Wi-Fi to the mobile network gets a different subnet, and a browser update changes the User-Agent. Both create a new identifier in the middle of the visit.

The reconstructed sessions are therefore an estimate with errors in both directions. The dedicated BigQuery column the article proposes makes it possible to analyze this share separately, for example by setting conversion rates of sessions with and without a cookie side by side instead of mixing them.

Why must the daily salt never be stored or backed up?

Because it is the only thing protecting the hashes from being reversed. The space of possible inputs is small: there are only about 16.8 million IPv4 subnets of size /24, and common User-Agents can be listed, so with the salt known, every identifier of that day can be traced back to a subnet and a browser by trying the combinations. In the PHP example the salt sits in the database as a transient, in the options table when no persistent object cache is in use, and therefore in every database backup, where it outlives its day.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Write a comment

Observations from other implementations, objections and questions are welcome here.

The email address is not published. Required fields are marked with an asterisk.

ALL ARTICLES & CATEGORIES

CCTV

Follow this category by RSS

Cloud & AI

Follow this category by RSS

Data Privacy

All 18 articles in this category Follow this category by RSS

Digital Analytics

All 55 articles in this category Follow this category by RSS

Digital Marketing

All 36 articles in this category Follow this category by RSS

IT & Networks

All 17 articles in this category Follow this category by RSS

Music Production

All 14 articles in this category Follow this category by RSS

Raspberry PI

Follow this category by RSS

Smart Home

All 18 articles in this category Follow this category by RSS

Web Development

All 11 articles in this category Follow this category by RSS

WordPress Plugins & Tricks

Follow this category by RSS