FlowFuse / website

The FlowFuse Website
https://flowfuse.com
MIT License
22 stars 12 forks source link

Investigate Website A/B Testing #475

Closed joepavitt closed 1 year ago

joepavitt commented 1 year ago

Story

No response

Description

A/B Testing in PostHog is known as "Feature Flags" - https://posthog.com/manual/feature-flags

Given we generate static content through Eleventy, we need to investigate what we can get away with and our our site would render the Feature Flags accordingly.

This task will be scoped to exploring the options, and testing out results in PostHog, to understand what the workflow looks like.

joepavitt commented 1 year ago

Currently knee deep in learning about Netlify's Edge Functions as Eleventy Edge (https://www.11ty.dev/docs/plugins/edge/) seems an option, although still trying to understand constraints.

Our biggest challenge is that we're generating static content via Eleventy, but need a dynamic variable for each user depending on the response from the PostHog Feature Flag API call https://posthog.com/manual/feature-flags

joepavitt commented 1 year ago

turns out we want "Experiments" in PostHog (https://posthog.com/manual/experimentation) not Feature Flags

joepavitt commented 1 year ago

Having played with this more, these are my findings:

PostHog Experiments

Eleventy Limitations

Eleventy Edge Functions (docs)

Eleventy Shortcodes:

{% abtesting "flag", "control"%}
    <h1 class="text-gray-50 max-w-lg m-auto">
        DevOps for Node-RED
    </h1>
{% endabtesting %}
{% abtesting "flag", "test"%}
    <h1 class="text-gray-50 max-w-lg m-auto">
        Run Node-RED in Production
    </h1>
{% endabtesting %}

and then uses the PostHog Experiments call in the shortcode handler:

eleventyConfig.addPairedShortcode("abtesting", async function (content, flag, value) {
        const fFlag = await posthog.getFeatureFlag('test-flag', 'joepavitt@flowforge.com')
        console.log(fFlag)
        if (fFlag === value) {
            return `${content}`
        } else {
            return ''
        }
})

Our Options

From my understanding, we are now left with 3 options:

  1. No A/B Testing, stay with Eleventy
  2. Have "edge" A/B Testing to randomly show different content to different users, then workout posthog's cookie structure and set the relevant cookie client-side, and call to PH to let it know what we have chosen, so that we can store the relevant data. With this option, we can no longer host on GitHub pages, and would need to switch to Netlify.
  3. Switch to something like Astro which is also a static site generator, but have JS built in natively, so can run (where we want to) server-side rendered code. It also comes with much better Headless CMS Integration which would mean we could move the editing/maintenance of the Handbook, Docs & Blog away from the website repo. However, this would be at least a few days of work in order to make that transition as it requires a re-write of the website.
joepavitt commented 1 year ago

Just discovered that, whilst the Eleventy Edge Function documentation is terrible, the Netlify Edge Function documentation is much better and details all of the available Web APIs - including fetch, so we could call an API directly, additionally, it details here about how we can actually use remotely served js libraries too

new investigation beginning.

joepavitt commented 1 year ago

Eleventy is so fiddly and tricky to work with. Following examples and best-guessing this to the best of my ability, but I just don't think this is possible for what we want to do.

joepavitt commented 1 year ago

Edge functions are very broken in Eleventy unfortunately. Work okay first time round, as long as you're just rendering raw content, then on refresh, they break. Really am swaying more and more towards option 3 here.

joepavitt commented 1 year ago

Seems as though I'm not the first to be having these issues with Eleventy: https://github.com/11ty/eleventy/issues/2585

joepavitt commented 1 year ago

okay, breakthrough... I have a working solution that just uses cookies and hardcoded option, but it toggles between content using an Edge Function to parse cookies.

To avoid the bug above, all of our Edge Functions must be written in liquid instead of njk.

I also have to list all of the cookies we plan on using with the Eleventy config, so we can't just add a new cookie for each feature, as such, I nest the feature flags inside a JSON object, and then encodeURIComponent() and JSON.stringify() that in order to store it. We reverse this process in the edge function with a toObj filter.

End Result in the template .njk files is something like this:

{% edge "liquid" %}
    {% assign feats = eleventy.edge.cookies.feats | toObj %}
    {% if feats.flagA == 'optionA' %}
        Hello Option A
    {% endif %}
    {% if feats.flagA == 'optionB' %}
        Hello Option B
    {% endif %}
{% endedge %}
joepavitt commented 1 year ago

Now added

eleventyConfig.addGlobalData("feats", () => {
        const encoded = context.cookies.get('feats')
        const decoded = decodeURIComponent(encoded)
        const feats = JSON.parse(decoded)
        return feats
      });

into our Eleventy Edge function which makes feat a global property (sensible given we' can use it store all feature flags), and now reads as this in templates:

{% edge "liquid" %}
    {% if feats.flagA == 'optionA' %}
        Hello Option A
    {% endif %}
    {% if feats.flagA == 'optionB' %}
        Hello Option B
    {% endif %}
{% endedge %}

Our main constraint is the cookie size limit of 4kB. A very rough, back of paper calculation, I've estimated we can handle about 150-200 feature flags, which is never going to be an issue.

joepavitt commented 1 year ago

Thought I would try reverting back to using Eleventy Shortcodes within Edge Functions now that I wasn't using njk edge functions, and it works:

{% edge "liquid" %}
    {% abtesting "flagA", "optionA" %}
        Hello Option A
    {% endabtesting %}
    {% abtesting "flagA", "optionB" %}
        Hello Option B
    {% endabtesting %}
{% endedge %}

I prefer this formatting as it makes it explicitly clear that it's A/B Testing taking place, and not easily confused with other if conditionals we have within the website.

joepavitt commented 1 year ago

Latest updates - fully working end-to-end PoC is in place. Still to-do:

joepavitt commented 1 year ago

https://answers.netlify.com/t/edge-functions-negate-endpoints-in-netlify-toml/88455

above link for me investigating the challenges around getting edge function to just run on html endpoints, and not for all images, css files, etc.