whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.16k stars 2.69k forks source link

Client Side Feature Flags #10706

Open keithamus opened 1 month ago

keithamus commented 1 month ago

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:

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.

ydaniv commented 1 month ago

That sounds great for simple stateless toggles. Would there be a way to make this sticky somehow to also enable the A/B use-case?

keithamus commented 1 month ago

I think how the flags are set depends on how your server applies them, so stickiness would be part of the server, no? For multivariant you could set a feature like cart-button-color="green",cart-button-color="blue" and so on. Does that answer the A/B use-case you're thinking of?

ydaniv commented 1 month ago

I think this currently achieved with cookies. You want the server to be stateless, but the client needs to remember it's variant and stick to it. I guess that's exactly the problem you're trying to avoid. So IIUC, unless another solution is found, this is strictly out of scope, which is fine.