PostHog Fast Feature Flags

PostHog Fast Feature Flags is a small library that assigns feature flags to visitors before the PostHog web snippet (posthog.js) loads. It works in limited cases where zero latency is more important than some of the advanced PostHog feature flags features (e.g. release conditions).

If you need the PFFF code snippet and know what you're doing, here it is:

// PostHog Fast Feature Flags
(()=>{var p=(e,a)=>{let n=A(a.variants),s=w(a.key,e,"variant");for(let i of n)if(s>=i.value_min&&s<i.value_max)return i.key;return null},A=e=>{let a=[],n=0;for(let[s,i]of Object.entries(e)){let u=n+i;a.push({value_min:n,value_max:u,key:s}),n=u}return a},w=(e,a,n="")=>{let s=`${e}.${a}${n}`,i=C(s),u=BigInt(`0x${i}`),l=BigInt("0xFFFFFFFFFFFFFFF");return Number(u)/Number(l)},C=e=>{function a(o,c){return(o<<c|o>>>32-c)>>>0}let n=1732584193,s=4023233417,i=2562383102,u=271733878,l=3285377520,r=[];for(let o=0;o<e.length;o++){let c=e.charCodeAt(o);r.push(c&255)}for(r.push(128);r.length*8%512!==448;)r.push(0);let m=e.length*8;r.push(0,0,0,0),r.push(m>>>24&255),r.push(m>>>16&255),r.push(m>>>8&255),r.push(m&255);for(let o=0;o<r.length;o+=64){let c=new Array(80);for(let t=0;t<16;t++)c[t]=r[o+t*4]<<24|r[o+t*4+1]<<16|r[o+t*4+2]<<8|r[o+t*4+3];for(let t=16;t<80;t++)c[t]=a(c[t-3]^c[t-8]^c[t-14]^c[t-16],1);let[h,f,g,F,d]=[n,s,i,u,l];for(let t=0;t<80;t++){let v=t<20?f&g|~f&F:t<40?f^g^F:t<60?f&g|f&F|g&F:f^g^F,k=t<20?1518500249:t<40?1859775393:t<60?2400959708:3395469782,y=a(h,5)+v+d+k+c[t]>>>0;d=F,F=g,g=a(f,30),f=h,h=y}n=n+h>>>0,s=s+f>>>0,i=i+g>>>0,u=u+F>>>0,l=l+d>>>0}return[n,s,i,u,l].map(o=>o.toString(16).padStart(8,"0")).join("").slice(0,15)};var x=()=>{let e;if(e)return e;let n=document.cookie.split(";").find(s=>s.trim().startsWith("pfff="));return n?(e=n.split("=")[1].trim(),e):(e=Math.random().toString(36).substring(2)+Date.now().toString(36),document.cookie=`pfff=${e};path=/;max-age=31536000`,e)};function _(){let e=x(),a=s=>{let i={};return s.forEach(u=>{let l=Object.values(u.variants).reduce((r,m)=>r+m,0);if(Math.abs(l-1)>1e-4)throw new Error(`Variants for flag ${u.key} must sum to 1, got ${l}`);i[u.key]=p(e,u)}),i},n=function(s){return a(s)};return n.evaluate=a,n.identity=x,n}var b=_();typeof window<"u"&&(window.PFFF=b);var j=b;})();

Otherwise, read on for more details…


Out of the box, PostHog web snippet (posthog.js) assigns feature flags by:

What Where
Generating a unique identifier for the visitor, if one doesn't already exist. [posthog.js]
Sending the unique identifier to PostHog's /decide endpoint to determine feature flag assignments. [posthog.js -> server]
Receiving the assignments from the server, parsing the response, and applying the feature flags. [server -> posthog.js]

Notice the little round-trip there? This approach works for many use cases but can include some latency. If you want to remove the latency, you can:

Here's how you can use PostHog Fast Feature Flags:

<script>
  /**
   * 1. Load the PFFF library before
   * you call posthog.init()
   */
  (()=>{var p=(e,a)=>{let n=A(a.variants),s=w(a.key,e,"variant");for(let i of n)if(s>=i.value_min&&s<i.value_max)return i.key;return null},A=e=>{let a=[],n=0;for(let[s,i]of Object.entries(e)){let u=n+i;a.push({value_min:n,value_max:u,key:s}),n=u}return a},w=(e,a,n="")=>{let s=`${e}.${a}${n}`,i=C(s),u=BigInt(`0x${i}`),l=BigInt("0xFFFFFFFFFFFFFFF");return Number(u)/Number(l)},C=e=>{function a(o,c){return(o<<c|o>>>32-c)>>>0}let n=1732584193,s=4023233417,i=2562383102,u=271733878,l=3285377520,r=[];for(let o=0;o<e.length;o++){let c=e.charCodeAt(o);r.push(c&255)}for(r.push(128);r.length*8%512!==448;)r.push(0);let m=e.length*8;r.push(0,0,0,0),r.push(m>>>24&255),r.push(m>>>16&255),r.push(m>>>8&255),r.push(m&255);for(let o=0;o<r.length;o+=64){let c=new Array(80);for(let t=0;t<16;t++)c[t]=r[o+t*4]<<24|r[o+t*4+1]<<16|r[o+t*4+2]<<8|r[o+t*4+3];for(let t=16;t<80;t++)c[t]=a(c[t-3]^c[t-8]^c[t-14]^c[t-16],1);let[h,f,g,F,d]=[n,s,i,u,l];for(let t=0;t<80;t++){let v=t<20?f&g|~f&F:t<40?f^g^F:t<60?f&g|f&F|g&F:f^g^F,k=t<20?1518500249:t<40?1859775393:t<60?2400959708:3395469782,y=a(h,5)+v+d+k+c[t]>>>0;d=F,F=g,g=a(f,30),f=h,h=y}n=n+h>>>0,s=s+f>>>0,i=i+g>>>0,u=u+F>>>0,l=l+d>>>0}return[n,s,i,u,l].map(o=>o.toString(16).padStart(8,"0")).join("").slice(0,15)};var x=()=>{let e;if(e)return e;let n=document.cookie.split(";").find(s=>s.trim().startsWith("pfff="));return n?(e=n.split("=")[1].trim(),e):(e=Math.random().toString(36).substring(2)+Date.now().toString(36),document.cookie=`pfff=${e};path=/;max-age=31536000`,e)};function _(){let e=x(),a=s=>{let i={};return s.forEach(u=>{let l=Object.values(u.variants).reduce((r,m)=>r+m,0);if(Math.abs(l-1)>1e-4)throw new Error(`Variants for flag ${u.key} must sum to 1, got ${l}`);i[u.key]=p(e,u)}),i},n=function(s){return a(s)};return n.evaluate=a,n.identity=x,n}var b=_();typeof window<"u"&&(window.PFFF=b);var j=b;})();

  /**
   * 2. Use PFFF to allocate feature flags and
   * generate an identifier for the visitor.
   */
  const assignedFeatureFlags = PFFF([
    {
      key: 'my-awesome-experiment',
      variants: {
        control: 0.5,
        test: 0.5,
      },
    },
  ]);
  const identity = PFFF.identity();

  /**
   * 3. Pass the assigned feature flags
   * and identity to PostHog's 'bootstrap' option
   * when loading the PostHog web snippet.
   */
  !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.crossOrigin="anonymous",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="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".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(INSERT_YOUR_POSTHOG_TOKEN_HERE, {
    api_host: 'https://us.i.posthog.com',
    person_profiles: 'identified_only',
    bootstrap: {
      featureFlags: assignedFeatureFlags,
      distinctID: identity,
    },
  });

  /**
   * 4. Apply whatever logic you need when
   * feature flags are loaded.
   */
  posthog.onFeatureFlags((flags, variants) => {
    const waitUntilReady = () => {
      if (variants['my-awesome-experiments'] === 'test') {
        document.getElementById('pfff-status').innerHTML =
          'Your identity is <code>' +
          identity +
          '</code> and you are assigned to the <code>test</code> variant';
      } else {
        document.getElementById('pfff-status').innerHTML =
          'Your identity is <code>' +
          identity +
          '</code> and you are assigned to the <code>control</code> variant';
      }
    };
    if (document.readyState === 'complete') {
      waitUntilReady();
    } else {
      window.addEventListener('DOMContentLoaded', waitUntilReady);
    }
  });
</script>

If you want to redirect the visitor to a different landing page based on their feature flag assignment, replace the last bit with something like this:

<script>
  posthog.onFeatureFlags((flags, variants) => {
    var maybeRedirect = function (path) {
      if (path && window.location.pathname.indexOf(path) === -1) {
        window.location.href = path;
      }
    };
    if (variants['my-awesome-experiments'] === 'test') {
      maybeRedirect('/test');
    } else {
      maybeRedirect('/control');
    }
  });
</script>

Keep in mind: PostHog Fast Feature Flags is pretty limited. Because the feature flags are assigned in the browser, advanced features like release conditions, etc. aren't available.

Feel free to open a GitHub issue if you have any questions or feedback!