Skip to content

Commit

Permalink
Runtime frontend feature flags. (#2730)
Browse files Browse the repository at this point in the history
* Implement frontend feature flags.

* Implement frontend feature flags.

* Implement frontend feature flags.
  • Loading branch information
sea-snake authored Dec 6, 2024
1 parent dbc9a9a commit e186505
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 0 deletions.
63 changes: 63 additions & 0 deletions src/frontend/src/featureFlags/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FeatureFlag } from "$src/featureFlags/index";

class MockStorage {
#data: Record<string, string> = {};

getItem(key: string): string | null {
return this.#data[key] ?? null;
}

setItem(key: string, value: string): void {
this.#data[key] = value;
}

removeItem(key: string): void {
delete this.#data[key];
}
}

test("feature flag to be initialized", () => {
const storage = new MockStorage();
storage.setItem("c", "true");
storage.setItem("d", "false");

const enabledFlag = new FeatureFlag(storage, "a", true);
const disabledFlag = new FeatureFlag(storage, "b", false);
const storedOverrideFlag = new FeatureFlag(storage, "c", false);
const storedDisabledFlag = new FeatureFlag(storage, "d", true);

expect(enabledFlag.isEnabled()).toEqual(true);
expect(disabledFlag.isEnabled()).toEqual(false);
expect(storedOverrideFlag.isEnabled()).toEqual(true);
expect(storedDisabledFlag.isEnabled()).toEqual(false);
});

test("feature flag to be set", () => {
const storage = new MockStorage();
const enabledFlag = new FeatureFlag(storage, "a", true);
const disabledFlag = new FeatureFlag(storage, "b", false);

enabledFlag.set(false);
disabledFlag.set(true);

expect(enabledFlag.isEnabled()).toEqual(false);
expect(disabledFlag.isEnabled()).toEqual(true);
expect(storage.getItem("a")).toEqual("false");
expect(storage.getItem("b")).toEqual("true");
});

test("feature flag to be reset", () => {
const storage = new MockStorage();
const enabledFlag = new FeatureFlag(storage, "a", true);
const disabledFlag = new FeatureFlag(storage, "b", false);

enabledFlag.set(false);
disabledFlag.set(true);
enabledFlag.reset();
disabledFlag.reset();

expect(enabledFlag.isEnabled()).toEqual(true);
expect(disabledFlag.isEnabled()).toEqual(false);
expect(storage.getItem("a")).toEqual(null);
expect(storage.getItem("b")).toEqual(null);
});
66 changes: 66 additions & 0 deletions src/frontend/src/featureFlags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Feature flags with default values
const FEATURE_FLAGS_WITH_DEFAULTS = {
DOMAIN_COMPATIBILITY: false,
} as const satisfies Record<string, boolean>;

const LOCALSTORAGE_FEATURE_FLAGS_PREFIX = "ii-localstorage-feature-flags__";

export class FeatureFlag {
readonly #storage: Pick<Storage, "getItem" | "setItem" | "removeItem">;
readonly #key: string;
readonly #defaultValue: boolean;
#value: boolean;

constructor(
storage: Pick<Storage, "getItem" | "setItem" | "removeItem">,
key: string,
defaultValue: boolean
) {
this.#storage = storage;
this.#key = key;
this.#defaultValue = defaultValue;
const storedValue = this.#storage.getItem(this.#key);
try {
this.#value =
storedValue === null
? this.#defaultValue
: Boolean(JSON.parse(storedValue));
} catch {
this.#value = this.#defaultValue;
}
}

isEnabled(): boolean {
return this.#value;
}

set(value: boolean) {
this.#value = Boolean(value);
this.#storage.setItem(this.#key, JSON.stringify(this.#value));
}

reset(): void {
this.#value = this.#defaultValue;
this.#storage.removeItem(this.#key);
}
}

// Initialize feature flags with values from localstorage
const initializedFeatureFlags = Object.fromEntries(
Object.entries(FEATURE_FLAGS_WITH_DEFAULTS).map(([key, defaultValue]) => [
key,
new FeatureFlag(
window.localStorage,
LOCALSTORAGE_FEATURE_FLAGS_PREFIX + key,
defaultValue
),
])
);

// Make feature flags configurable from browser console
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.__featureFlags = initializedFeatureFlags;

// Export initialized feature flags as named exports
export const { DOMAIN_COMPATIBILITY } = initializedFeatureFlags;

0 comments on commit e186505

Please sign in to comment.