How to do cookieless tracking with PostHog

Jun 14, 2024

Normally, PostHog stores some information about the user in their browser using a cookie. This approach is typical for analytics tools and enables user tracking across sessions, caching feature flag data, and more.

There are some situations where you don't want to use cookies and do cookieless tracking instead. These include when:

  • You have concerns about user privacy or regulation such as GDPR or HIPAA.

  • You have your own system for identifying users across multiple sessions, rely on server-side tracking, or don’t need to track user identities at all.

  • You hate cookie banners.

This tutorial shows how to configure PostHog's JavaScript Web SDK to do cookieless tracking by using page memory to store user data.

Note: When using cookies, PostHog stores data as a first-party cookie. We don't track users across different sites (like largely blocked third-party cookies do). It also means the same cookie works across subdomains like posthog.com and eu.posthog.com.


Need analytics hosted in the EU?


Step 1: Decide where to store the data

It is helpful first to know what data is being stored and why. Specifically, PostHog stores the following information in the user’s browser:

If you want to use PostHog without cookies, you must store some of this data elsewhere. Although PostHog has multiple persistence options, the most straightforward is to store it in page memory. We show you how to do this in the next step.

Storing in memory avoids cookies, but once the user leaves the page, the data isn't saved. Returning users get new IDs, flags must be re-fetched, and configuration options are reset.

Step 2: Configure persistence

Managing cookies only matters if you use the JavaScript Web SDK. Other SDKs don't use cookies.

If you haven't set up the JavaScript Web SDK yet, you can install the posthog-js library using a package manager or copy the snippet below and paste it into your <head> tag:

HTML
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('<ph_project_api_key>',
{
api_host:'https://us.i.posthog.com',
}
)
</script>

For cookieless tracking, the important part is the initialization. To change from the default localStorage+cookie persistence, to memory, add the persistence config option to your initialization. You can also bootstrap the distinctId and featureFlags from the server to avoid regenerating and re-requesting them.

Here’s how to do that if you want to store data in page memory:

JavaScript
posthog.init('<ph_project_api_key>',
{
api_host:'https://us.i.posthog.com',
persistence: 'memory',
bootstrap: { // optional
distinctID: 'user distinct id',
featureFlags: {
'feature-flag-1': true,
'feature-flag-2': false,
},
},
}
)

Tip: If you want to change your persistence settings after initializing PostHog, you can use posthog.set_config(). This is helpful if you are setting up a cookie banner:

JavaScript
const handleCookieConsent = (consent) => {
posthog.set_config({ persistence: consent === 'yes' ? 'localStorage+cookie' : 'memory' });
localStorage.setItem('cookie_consent', consent);
};

Now that PostHog isn’t using cookies you can, optionally and if you’re not using cookies for any other services, completely remove your cookie banner. Ursula von der Leyen would be proud!

Limitations

Nothing comes for free and limiting what PostHog can store between page loads does affect how the product works. Below are some of the likely consequences of cookieless tracking:

  • Higher anonymous user count - each pageload that is not bootstrapped with a known distinctId counts as a new user and not a returning one.

  • Session replay count - as we can't track a "session" (multiple pageloads over time), session recordings are only as long as the in-memory session and resets (i.e. start a new recording) whenever the browser reloads. In addition, multiple window tracking is not possible.

  • Cache optimizations - PostHog stores some information in browser storage to load faster, for example, the last loaded values for feature flags. Without this, there can be a delay between the page loading and things like feature flags being available to query (unless flags are bootstrapped).

  • Flag consistency - Because setting peristence to memory resets the user distinct_id, if you don't implement bootstrapping or another identification method, the same user might see multiple flag variants across sessions. This can lead to an inconsistent experience if you are doing a percentage rollout or running an A/B test.

Further reading

  • Joe
    a year ago

    Hey, does anyone have an example or idea how to best implement tracking with cookie if the user accepts cookies but if they don't accept cookies then track them cookieless?

    • Konstantin
      a year agoSolution

      Hey!

      I think that right before step 3 there is a small note on how you can achieve that

      image.png

  • Olli
    a year ago

    At least "sessionStorage" does not work correctly. /static/array.js.map and /static/recorder.js.map still add cookie "sessionid".

    Workaround is to remove "Set-Cookie" headers in reverse proxy but it should not be needed.

    • Olli
      Authora year ago

      After reading some more about topic, it looks to be actually ePrivacy Directive, not GDPR which requires user consent and it is needed even if "localStorage" or "sessionStorage" is used without cookies because they store data to user browser.

      In theory "memory" would be allowed option but it would need improvements to reverse proxy to make sure that new session is now created when user navigates from one page to another. I tried by bootstrapping with "distinctID" which would be cached in user browser but still new session get created on every page change.

      Not good, probably need disable PostHog completely from all EU users or switch to some alternative tool which rely purely for server side tracking and does that in way that user cannot be identified.

  • Jannik
    a year ago

    If I set persistence to memory and then call identify what limitation do still apply? I guess I wouldn't have to deal with a higher amount of anonymous users sicne posthog can knows that the events belong to a known user? Will posthog still be able to identify sessions or will every page reload still create an unconnected new session?

  • Adnan
    a year ago

    Does this mean for A/B tests it's possible for anonymous users to refresh the page and see different variants?

    • Neil
      a year agoSolution

      Hey Adnan, assuming you're using memory persistence / the ID changes on every refresh, then yes, they can refresh the page and see different variants.

  • Kristie
    a year ago

    If we use this strategy, disable GeoIP, and blacklist $ip and $device_id properties, and don't track anything except generic events like "button_clicked," can we be GDPR compliant without any type of consent banner?

  • Jakub
    2 years ago

    Hi there,

    What do you think about such setup where, depending on user consent I initiate posthog with memory or cookie persistence? I assume that some sessions will be inflated, but not the ones that accepted cookies.

    useEffect(() => {
    if (consents.analytics_storage === undefined) {
    return;
    }
    if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) {
    return;
    }
    let persistence: PostHogConfig['persistence'] = 'localStorage+cookie';
    if (consents.analytics_storage === false) {
    persistence = 'memory';
    }
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
    persistence: persistence,
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
    capture_pageview: false, // Disable automatic pageview capture, as we capture manually
    });
    setPosthogInitiated(true);
    }, [consents]);

    I'm asking myself if it is possible to "upgrade" posthog persistence from "memory" to "cookie" once "Agree to cookies" consent is given. Ideal flow would be: user visits the page -> posthog with 'memory' option is set. a) user clicks reject: nothing changes b) user clicks accept: posthog is configured with persistence of choice (localStorage+cookie in my case).

  • Faizan
    2 years ago

    I am trying to implement Posthog using Borlabs cookies plugin in Wordpress. In the opt-in section, I have added the code snippet without "persistence: 'memory'". But in the opt-out section, I have added the code snippet with persistence: 'memory'. Now, when the user opts in, the Posthog works perfectly but if the user opts out, the persistence: 'memory' snippets does not run. Kindly help.

    • Marcus
      2 years agoSolution

      You could check if that user is going to be associated with another PostHog user each time you close and re-open the page. (since closing the tab will clear the memory)

  • Ralph
    2 years ago

    How does it work with Elementor / Wordpress?

    Is there an easy way to implement this in Elementor / Wordpress? Like a cookieless version of the web snippet?

    • Ian
      2 years agoSolution

      In the posthog.init(), you can set the config to persistence memory to go cookieless.

      posthog.init(<ph_project_api_key>, {api_host: 'https://app.posthog.com', persistence: 'memory'
      })
  • Jesse
    2 years ago

    Jesse

    So does this mean we don't need a cookie banner / any consent for tracking with this setup? No banners about even informing them?

    Not even a simple statement in the privacy decleration?

    • E
      2 years ago

      I'm pretty sure that if you collect any identifiable user data you still need consent according to GDPR.

      You might not need consent to place a cookie, but you do need consent to collect their PII data.

      Only if you collect truly anonymous data and don't use a cookie (as a cookie tracker is an identifier) do you not need consent.

      I would say even the "distinctID" they are doing is questionable.

      I am a bit confused by these articles, I wonder if they got any legal advice on this.

  • Tobit
    3 years ago

    User ID persistence

    We're looking into moving to using the js snippet, and away from the current set up which is based on Segment.

    Currently our user IDs are those from our internal DB, if we moved to the js approach would this still be the case? If so, would returning users still appear as new, despite having a persistent User ID?

    If we could implement it without cookies it would be ideal as it saves some headaches with additional banners as listed in the article.

    • Alex
      3 years agoSolution

      Hey Tobit, thanks for considering using PH! If you already have a way of accessing those persistent user ids for returning customers using Segment, you'll be able to use posthog.alias() to tie the new anonymous id to an existing user from your internal DB. This should work regardless of where your persistence layer is.

  • Lukas
    3 years ago

    So where is the data stored?

    I didn’t try this out yet, but where is the data then stored in case of “memory”?

    • Li
      3 years agoSolution

      In "memory" storage it's just stored in a JavaScript object that only lasts the length of the pageview.