html icon indicating copy to clipboard operation
html copied to clipboard

Client Side Feature Flags

Open keithamus opened this issue 1 year ago • 3 comments

What problem are you trying to solve?

Complex websites often use feature flags to gradually roll-out changes to users. These features can be easily toggled on-or-off per-request, or tweaked by engineers in realtime without changing code and going through a whole deploy/upgrade cycle. On the server it's quite straightforward to run with a solution that makes database queries but on the client there are more considerations and complexities. The biggest problem - in my opinion - is that without a standard third party libraries have no way to coordinate feature flags and so many libraries will expose a config instead, this means more JS required to "initialize" the library with a set of features. But also the solutions that exist today usually have to trade-off between which parts of the platform they get exposed to (JS, CSS, ServiceWorkers) and/or efficiency.

What solutions exist today?

Myriad ways exist, each with their own trade-offs:

  • Expose an API endpoint on the server and make API requests every time you want to check a feature (or make a request for a list of all the client side features). The problem with this is it involves an additional HTTP round trip for data the server likely had (or had easy access to) in the initial response. This can also be tricky because it's asynchronous - introducing accidental complexity of now requiring some code to lazy-load in order to accommodate the asynchronous timing. This also prevents use of these features in CSS as CSS cannot request and read JSON APIs. My rating: D tier solution.
  • Use the cookie header and make the values exposed to script. This solves the synchronicity problem but raises new issues; one of which is that the flags (cookie) are then sent back with every request which is useless for the server and a waste of bandwidth in general. The other is that you've still got to write a bunch of code to parse document.cookie and extract the relevant feature flags. Finally, Set-Cookie is a forbidden Response header name meaning if you have a ServiceWorker, you're unable intercept responses and read feature flags within the service worker context. My rating: E tier solution.
  • Abuse the server-timing header! server-timing can have arbitrary data (server-timing: my_flag;desc="on"), it can store multiple values, it is not a forbidden header which means ServiceWorkers can read it, and also it has a JS API via performance.getEntriesByType('navigation')?.[0]?.serverTiming. However this is not exposed to CSS, and is obviously a gnarly hack and I frankly feel bad for mentioning it. My rating: F tier solution.
  • Set a bunch of classnames/attributes on the html or body element, or stuff them in a <meta> tag etc. This can solve the synchronicity problem, and these would be exposed to CSS, but it also increases the difficulty of reading these from the service worker, as you'll need to pull out and parse the response body (related; https://github.com/whatwg/dom/issues/1217). My rating: C tier solution.
  • Dump a blob of JSON in a <script> tag or similar: This seems to be quite common with React apps, where JSON will be embedded somewhere in the HTML, that can then be read from JS and fed into the framework. This isn't CSS exposed and it's difficult to get at via ServiceWorker without parsing the response body as DOM. My rating: E tier solution.

How would you solve it?

I think what I'd like to see is a new HTTP response header (let's call it features) which looks a little like server-timing, allowing arbitrary read-only keys (with optional values) to be extracted by CSS/JS. This flag would be parsed and become part of the page context, with complementary DOM & CSS APIs. Importantly, while this looks a bit like Set-Cookie, the values are never sent back to the server, and there would be no concept of "third party features".

features: use-new-widgets, llm-temp=0.4, cookie-compliance=gdpr, color-contrast=aaa
<!-- features can also be defined with an http-equiv meta tag -->
<meta http-equiv="features" content="use-new-widgets, llm-temp=0.4, cookie-compliance=gdpr, color-contrast=aaa">
interface Features {
  bool has(DOMString feature);
  DOMString get(DOMString feature);
}
partial interface Document {
  readonly attribute Features features;
}
// .has can be used to check for presence of a feature
if (document.features.has('use-new-widgets')) {
  await import('widgets-v4');
}

// While get retrieves the string
respondToPromopt({ temperature: Number(document.features.get('llm-temp')) || 0.7 })

switch (document.features.get('cookie-compliance')) {
  case 'gdpr': return showGDPRCookieBanner();
  case 'ccpa': return showCCPACookieBanner();
}
/* the @feature rule can check for presence of a flag also */
@feature (use-new-widgets) {
  @import "./widgets-v4.css";
}

:root {
  --global-contrast: wcag2(aa);
}
/* but can also check equality against the string value */
@feature (color-contrast: "aaa") {
  --global-contrast: wcag2(aaa);
}

This header would be a safe header for ServiceWorkers to read, and given the simplicity of the format would be straightforward to parse with a couple of lines of JS in the ServiceWorker.

Anything else?

I realise this might not be the ideal venue to discuss this as it crosses many working groups, but as HTML defines Document this seemed as good a place as any.

keithamus avatar Oct 16 '24 13:10 keithamus