From b328b2a02201cf8ae57db8bded06a01cf77ab3e8 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Wed, 4 Jun 2025 13:30:08 +0200 Subject: [PATCH 1/3] refactor(browser-sdk): refactor overrides API includes new example of how to make your own toolbar --- packages/browser-sdk/src/client.ts | 30 ++++++------ packages/browser-sdk/src/toolbar/Features.tsx | 22 ++++----- packages/browser-sdk/src/toolbar/Toolbar.tsx | 8 ++-- packages/browser-sdk/test/client.test.ts | 2 +- packages/browser-sdk/test/usage.test.ts | 12 +++++ packages/react-sdk/dev/plain/app.tsx | 46 ++++++++++++++++++- 6 files changed, 86 insertions(+), 34 deletions(-) diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts index fb3a68c4..dcb3eac7 100644 --- a/packages/browser-sdk/src/client.ts +++ b/packages/browser-sdk/src/client.ts @@ -319,6 +319,7 @@ export type FeatureRemoteConfig = export interface Feature { /** * Result of feature flag evaluation. + * Note: Does not take local overrides into account. */ isEnabled: boolean; @@ -338,6 +339,17 @@ export interface Feature { requestFeedback: ( options: Omit, ) => void; + + /** + * The current override status of isEnabled for the feature. + */ + isEnabledOverride: boolean | null; + + /** + * Set the override status for isEnabled for the feature. + * Set to `null` to remove the override. + */ + setIsEnabledOverride(isEnabled: boolean | null): void; } function shouldShowToolbar(opts: InitOptions) { @@ -778,23 +790,13 @@ export class BucketClient { ...options, }); }, + isEnabledOverride: this.featuresClient.getFeatureOverride(key), + setIsEnabledOverride(isEnabled: boolean | null) { + self.featuresClient.setFeatureOverride(key, isEnabled); + }, }; } - /** - * @internal - */ - setFeatureOverride(key: string, isEnabled: boolean | null) { - this.featuresClient.setFeatureOverride(key, isEnabled); - } - - /** - * @internal - */ - getFeatureOverride(key: string): boolean | null { - return this.featuresClient.getFeatureOverride(key); - } - private sendCheckEvent(checkEvent: CheckEvent) { return this.featuresClient.sendCheckEvent(checkEvent, () => { this.hooks.trigger( diff --git a/packages/browser-sdk/src/toolbar/Features.tsx b/packages/browser-sdk/src/toolbar/Features.tsx index 2a8ea856..d8028fa9 100644 --- a/packages/browser-sdk/src/toolbar/Features.tsx +++ b/packages/browser-sdk/src/toolbar/Features.tsx @@ -9,15 +9,15 @@ import { FeatureItem } from "./Toolbar"; export function FeaturesTable({ features, searchQuery, - setEnabledOverride, appBaseUrl, isOpen, + setIsEnabledOverride, }: { features: FeatureItem[]; searchQuery: string | null; - setEnabledOverride: (key: string, value: boolean | null) => void; appBaseUrl: string; isOpen: boolean; + setIsEnabledOverride: (key: string, isEnabled: boolean | null) => void; }) { const searchedFeatures = searchQuery === null @@ -43,7 +43,7 @@ export function FeaturesTable({ .includes(searchQuery.toLocaleLowerCase()) } isOpen={isOpen} - setEnabledOverride={setEnabledOverride} + setEnabledOverride={() => setIsEnabledOverride(feature.key, null)} /> ))} @@ -61,7 +61,7 @@ function FeatureRow({ }: { feature: FeatureItem; appBaseUrl: string; - setEnabledOverride: (key: string, value: boolean | null) => void; + setEnabledOverride: (isEnabled: boolean | null) => void; isOpen: boolean; index: number; isNotVisible: boolean; @@ -94,11 +94,7 @@ function FeatureRow({ {feature.localOverride !== null ? ( - + ) : null} @@ -111,7 +107,7 @@ function FeatureRow({ onChange={(e) => { const isChecked = e.currentTarget.checked; const isOverridden = isChecked !== feature.isEnabled; - setEnabledOverride(feature.key, isOverridden ? isChecked : null); + setEnabledOverride(isOverridden ? isChecked : null); }} /> @@ -138,11 +134,9 @@ export function FeatureSearch({ function Reset({ setEnabledOverride, - featureKey, ...props }: { - setEnabledOverride: (key: string, value: boolean | null) => void; - featureKey: string; + setEnabledOverride: (isEnabled: boolean | null) => void; } & h.JSX.HTMLAttributes) { return ( { e.preventDefault(); - setEnabledOverride(featureKey, null); + setEnabledOverride(null); }} {...props} > diff --git a/packages/browser-sdk/src/toolbar/Toolbar.tsx b/packages/browser-sdk/src/toolbar/Toolbar.tsx index e0450ad2..a1f0d7f6 100644 --- a/packages/browser-sdk/src/toolbar/Toolbar.tsx +++ b/packages/browser-sdk/src/toolbar/Toolbar.tsx @@ -48,7 +48,7 @@ export default function Toolbar({ (feature) => ({ key: feature.key, - localOverride: bucketClient.getFeatureOverride(feature?.key), + localOverride: feature.isEnabledOverride, isEnabled: feature.isEnabled, }) satisfies FeatureItem, ), @@ -108,9 +108,9 @@ export default function Toolbar({ features={sortedFeatures} isOpen={isOpen} searchQuery={search} - setEnabledOverride={bucketClient.setFeatureOverride.bind( - bucketClient, - )} + setIsEnabledOverride={(key, isEnabled) => + bucketClient.getFeature(key).setIsEnabledOverride(isEnabled) + } /> diff --git a/packages/browser-sdk/test/client.test.ts b/packages/browser-sdk/test/client.test.ts index cf306cd9..b4c2e464 100644 --- a/packages/browser-sdk/test/client.test.ts +++ b/packages/browser-sdk/test/client.test.ts @@ -70,7 +70,7 @@ describe("BucketClient", () => { await client.initialize(); expect(featuresResult["featureA"].isEnabled).toBe(true); expect(client.getFeature("featureA").isEnabled).toBe(true); - client.setFeatureOverride("featureA", false); + client.getFeature("featureA").setIsEnabledOverride(false); expect(client.getFeature("featureA").isEnabled).toBe(false); }); }); diff --git a/packages/browser-sdk/test/usage.test.ts b/packages/browser-sdk/test/usage.test.ts index a51fbdf4..5cbd1f21 100644 --- a/packages/browser-sdk/test/usage.test.ts +++ b/packages/browser-sdk/test/usage.test.ts @@ -80,6 +80,8 @@ describe("usage", () => { track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); }); @@ -413,6 +415,8 @@ describe(`sends "check" events `, () => { config: { key: undefined, payload: undefined }, track: expect.any(Function), requestFeedback: expect.any(Function), + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); expect(client.getFeature("featureB")).toStrictEqual({ @@ -426,6 +430,8 @@ describe(`sends "check" events `, () => { }, track: expect.any(Function), requestFeedback: expect.any(Function), + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); expect(client.getFeature("featureC")).toStrictEqual({ @@ -433,6 +439,8 @@ describe(`sends "check" events `, () => { config: { key: undefined, payload: undefined }, track: expect.any(Function), requestFeedback: expect.any(Function), + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); }); @@ -567,6 +575,8 @@ describe(`sends "check" events `, () => { track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); vi.spyOn(client, "track"); @@ -586,6 +596,8 @@ describe(`sends "check" events `, () => { track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); vi.spyOn(client, "requestFeedback"); diff --git a/packages/react-sdk/dev/plain/app.tsx b/packages/react-sdk/dev/plain/app.tsx index dd22267b..67247160 100644 --- a/packages/react-sdk/dev/plain/app.tsx +++ b/packages/react-sdk/dev/plain/app.tsx @@ -9,6 +9,7 @@ import { useUpdateCompany, useUpdateOtherContext, useUpdateUser, + useClient, } from "../../src"; // Extending the Features interface to define the available features @@ -187,6 +188,7 @@ function Demos() { + ); } @@ -223,6 +225,49 @@ function FeatureOptIn({ ); } +function CustomToolbar() { + const client = useClient(); + + if (!client) { + return null; + } + + return ( +
+

Custom toolbar

+
    + {Object.entries(client.getFeatures()).map(([featureKey, feature]) => ( +
  • + {featureKey} - + {(feature.isEnabledOverride ?? feature.isEnabled) + ? "Enabled" + : "Disabled"}{" "} + {feature.isEnabledOverride !== null && ( + + )} + { + console.log("onChange", e.target.checked); + client + .getFeature(featureKey) + .setIsEnabledOverride(e.target.checked ?? false); + }} + /> +
  • + ))} +
+
+ ); +} + export function App() { return ( - {} ); } From 0cf76831e22655750957790f84e9685af5ac1246 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Wed, 4 Jun 2025 13:35:18 +0200 Subject: [PATCH 2/3] pass override value in the Toolbar --- packages/browser-sdk/src/toolbar/Features.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/browser-sdk/src/toolbar/Features.tsx b/packages/browser-sdk/src/toolbar/Features.tsx index d8028fa9..fc736c1e 100644 --- a/packages/browser-sdk/src/toolbar/Features.tsx +++ b/packages/browser-sdk/src/toolbar/Features.tsx @@ -43,7 +43,9 @@ export function FeaturesTable({ .includes(searchQuery.toLocaleLowerCase()) } isOpen={isOpen} - setEnabledOverride={() => setIsEnabledOverride(feature.key, null)} + setEnabledOverride={(override) => + setIsEnabledOverride(feature.key, override) + } /> ))} From 7f61a592d96dab67c005b6b47ad1a0803ca3b8fd Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Wed, 4 Jun 2025 13:55:25 +0200 Subject: [PATCH 3/3] fixed toolbar overrides --- packages/browser-sdk/src/toolbar/Features.tsx | 5 +---- packages/react-sdk/dev/plain/app.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/browser-sdk/src/toolbar/Features.tsx b/packages/browser-sdk/src/toolbar/Features.tsx index fc736c1e..a5d6eb09 100644 --- a/packages/browser-sdk/src/toolbar/Features.tsx +++ b/packages/browser-sdk/src/toolbar/Features.tsx @@ -101,10 +101,7 @@ function FeatureRow({ { const isChecked = e.currentTarget.checked; diff --git a/packages/react-sdk/dev/plain/app.tsx b/packages/react-sdk/dev/plain/app.tsx index 67247160..a393b89e 100644 --- a/packages/react-sdk/dev/plain/app.tsx +++ b/packages/react-sdk/dev/plain/app.tsx @@ -252,10 +252,10 @@ function CustomToolbar() { )} { - console.log("onChange", e.target.checked); + // this uses slightly simplified logic compared to the Bucket Toolbar client .getFeature(featureKey) .setIsEnabledOverride(e.target.checked ?? false); @@ -277,6 +277,12 @@ export function App() { otherContext={initialOtherContext} apiBaseUrl={apiBaseUrl} > + {!publishableKey && ( +
+ No publishable key set. Please set the VITE_PUBLISHABLE_KEY + environment variable. +
+ )} );