Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(browser-sdk,react-sdk): extend the web SDKs to support the new config option. #285

Merged
merged 32 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4f6ccb6
Add support for feature-specific config payloads
pavkam Jan 10, 2025
7b3a4af
Refactor flag evaluation logic and update SDK version.
pavkam Jan 13, 2025
70f2393
Add feature configuration support and enhance fallback features
pavkam Jan 13, 2025
2e30def
Merge remote-tracking branch 'origin/main' into buc-3198-extend-the-w…
pavkam Jan 13, 2025
d2e9de6
Bump version to 0.4.0
pavkam Jan 13, 2025
5af7fd3
Merge branch 'main' into buc-3198-extend-the-web-sdks-to-expose-the-c…
pavkam Jan 22, 2025
340fc9c
feat(browser-sdk): add format script, improve README formatting, and …
pavkam Jan 22, 2025
a6c6678
feat(browser-sdk): update feature configuration handling and improve …
pavkam Jan 22, 2025
f5f250e
feat(react-sdk): enhance feature handling in useFeature hook
pavkam Jan 22, 2025
66f661a
feat(browser-sdk, react-sdk, openfeature-browser-sdk): enhance featur…
pavkam Jan 22, 2025
38975e1
feat(openfeature-browser-provider): revert changes to openfeature ada…
pavkam Jan 24, 2025
f2ba839
chore(browser-sdk, react-sdk): Respond to comments
pavkam Jan 24, 2025
f1383e6
fix(browser-sdk): update README to reflect changes in feature configu…
pavkam Jan 24, 2025
5f02bb8
Merge branch 'main' into buc-3198-extend-the-web-sdks-to-expose-the-c…
pavkam Jan 27, 2025
295f05d
fix(browser-sdk): update feature remote config type definition
pavkam Jan 27, 2025
0dd5757
chore(react-sdk): remove unused import from index.tsx
pavkam Jan 27, 2025
7173423
feat(browser-sdk): enhance fallback feature configuration handling
pavkam Jan 27, 2025
862df25
test(browser-sdk): remove .only from features test
pavkam Jan 27, 2025
e22475b
test(browser-sdk): update feature test expectations
pavkam Jan 27, 2025
2e62475
refactor(react-sdk): simplify feature config access and reduce payload
pavkam Jan 27, 2025
2f74252
Merge branch 'main' into buc-3198-extend-the-web-sdks-to-expose-the-c…
pavkam Jan 28, 2025
fa735d8
docs(browser-sdk,react-sdk): clarify remote config documentation
pavkam Jan 28, 2025
a6eb451
Merge remote-tracking branch 'origin/main' into buc-3198-extend-the-w…
pavkam Jan 28, 2025
ff3e85a
Merge remote-tracking branch 'origin/buc-3198-extend-the-web-sdks-to-…
pavkam Jan 28, 2025
f986feb
feat(browser-sdk): export FallbackFeatureOverride type
pavkam Jan 28, 2025
7c6c47c
chore(release): bump browser-sdk and react-sdk to 3.0.0-alpha.1
pavkam Jan 28, 2025
352a462
Merge branch 'main' into buc-3198-extend-the-web-sdks-to-expose-the-c…
pavkam Jan 28, 2025
5a8fffe
Merge branch 'main' into buc-3198-extend-the-web-sdks-to-expose-the-c…
pavkam Jan 29, 2025
f4b6643
Merge branch 'main' into buc-3198-extend-the-web-sdks-to-expose-the-c…
pavkam Jan 29, 2025
dde88b5
fix: ensure feature config always has key and payload properties
pavkam Jan 29, 2025
bb506b4
refactor: simplify feature config type definition
pavkam Jan 29, 2025
564332a
Merge remote-tracking branch 'origin/buc-3198-extend-the-web-sdks-to-…
pavkam Jan 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,9 @@
"**/node_modules": true,
"**/*.lock": true
},
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"cSpell.words": [
"bucketco",
"openfeature"
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"build": "lerna run build --stream",
"test:ci": "lerna run test:ci --stream",
"test": "lerna run test --stream",
"format": "lerna run format --stream",
"prettier": "lerna run prettier --stream",
"lint": "lerna run lint --stream",
"lint:ci": "lerna run lint:ci --stream",
Expand Down
43 changes: 39 additions & 4 deletions packages/browser-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,28 @@ const bucketClient = new BucketClient({ publishableKey, user, company });

await bucketClient.initialize();

const { isEnabled, track, requestFeedback } = bucketClient.getFeature("huddle");
const {
isEnabled,
config: { payload: question },
track,
requestFeedback,
} = bucketClient.getFeature("huddle");

if (isEnabled) {
// show feature. When retrieving `isEnabled` the client automatically
// Show feature. When retrieving `isEnabled` the client automatically
// sends a "check" event for the "huddle" feature which is shown in the
// Bucket UI.

// On usage, call `track` to let Bucket know that a user interacted with the feature
track();

// The `payload` is a user-supplied JSON in Bucket that is dynamically picked
// out depending on the user/company.
const question = payload?.question ?? "Tell us what you think of Huddles";

// Use `requestFeedback` to create "Send feedback" buttons easily for specific
// features. This is not related to `track` and you can call them individually.
requestFeedback({ title: "Tell us what you think of Huddles" });
requestFeedback({ title: question });
}

// `track` just calls `bucketClient.track(<featureKey>)` to send an event using the same feature key
Expand Down Expand Up @@ -127,6 +136,7 @@ To retrieve features along with their targeting information, use `getFeature(key
const huddle = bucketClient.getFeature("huddle");
// {
// isEnabled: true,
// config: { key: "zoom", payload: { ... } },
// track: () => Promise<Response>
// requestFeedback: (options: RequestFeedbackData) => void
// }
Expand All @@ -140,6 +150,7 @@ const features = bucketClient.getFeatures();
// huddle: {
// isEnabled: true,
// targetingVersion: 42,
// config: ...
// }
// }
```
Expand All @@ -148,7 +159,31 @@ const features = bucketClient.getFeatures();
by down-stream clients, like the React SDK.

Note that accessing `isEnabled` on the object returned by `getFeatures` does not automatically
generate a `check` event, contrary to the `isEnabled` property on the object return from `getFeature`.
generate a `check` event, contrary to the `isEnabled` property on the object returned by `getFeature`.

### Remote config

Similar to `isEnabled`, each feature has a `config` property. This configuration is managed from within Bucket.
It is managed similar to the way access to features is managed, but instead the binary `isEnabled` you can have multiple configuration values which are given to different user/companies.

```ts
const features = bucketClient.getFeatures();
// {
// huddle: {
// isEnabled: true,
// targetingVersion: 42,
// config?: {
// key: "gpt-3.5",
// payload: { maxTokens: 10000, model: "gpt-3.5-beta1" }
// }
// }
// }
```

The `key` is always present while the `payload` is a optional JSON value for arbitrary configuration needs. `

Just as `isEnabled`, accessing `config` on the object returned by `getFeatures` does not automatically
generate a `check` event, contrary to the `config` property on the object returned by `getFeature`.

### Tracking feature usage

Expand Down
97 changes: 72 additions & 25 deletions packages/browser-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,37 +174,67 @@ const defaultConfig: Config = {
enableTracking: true,
};

/**
* A remotely managed configuration value for a feature.
*/
export type FeatureRemoteConfig =
| {
/**
* The key of the matched configuration value.
*/
key: string;

/**
* The user-supplied data.
*/
payload: any;
}
| { key: undefined; targetingVersion: undefined; value: undefined };
pavkam marked this conversation as resolved.
Show resolved Hide resolved

/**
* A feature.
*/
export interface Feature {
/**
* Result of feature flag evaluation
* Result of feature flag evaluation.
*/
isEnabled: boolean;

/*
* Optional user-defined configuration.
*/
config: FeatureRemoteConfig;

/**
* Function to send analytics events for this feature
*
* Function to send analytics events for this feature.
*/
track: () => Promise<Response | undefined>;

/**
* Function to request feedback for this feature.
*/
requestFeedback: (
options: Omit<RequestFeedbackData, "featureKey" | "featureId">,
) => void;
}

/**
* BucketClient lets you interact with the Bucket API.
*
*/
export class BucketClient {
private publishableKey: string;
private context: BucketContext;
private readonly publishableKey: string;
private readonly context: BucketContext;
private config: Config;
private requestFeedbackOptions: Partial<RequestFeedbackOptions>;
private httpClient: HttpClient;
private readonly httpClient: HttpClient;

private autoFeedback: AutoFeedback | undefined;
private readonly autoFeedback: AutoFeedback | undefined;
private autoFeedbackInit: Promise<void> | undefined;
private featuresClient: FeaturesClient;
private readonly featuresClient: FeaturesClient;

public readonly logger: Logger;

/**
* Create a new BucketClient instance.
*/
Expand Down Expand Up @@ -328,7 +358,7 @@ export class BucketClient {
* Performs a shallow merge with the existing company context.
* Attempting to update the company ID will log a warning and be ignored.
*
* @param company
* @param company The company details.
*/
async updateCompany(company: { [key: string]: string | number | undefined }) {
if (company.id && company.id !== this.context.company?.id) {
Expand All @@ -350,6 +380,8 @@ export class BucketClient {
* Update the company context.
* Performs a shallow merge with the existing company context.
* Updates to the company ID will be ignored.
*
* @param otherContext Additional context.
*/
async updateOtherContext(otherContext: {
[key: string]: string | number | undefined;
Expand All @@ -367,7 +399,7 @@ export class BucketClient {
*
* Calling `client.stop()` will remove all listeners added here.
*
* @param cb this will be called when the features are updated.
* @param cb The callback to call when the update completes.
*/
onFeaturesUpdated(cb: () => void) {
return this.featuresClient.onUpdated(cb);
Expand All @@ -376,8 +408,8 @@ export class BucketClient {
/**
* Track an event in Bucket.
*
* @param eventName The name of the event
* @param attributes Any attributes you want to attach to the event
* @param eventName The name of the event.
* @param attributes Any attributes you want to attach to the event.
*/
async track(eventName: string, attributes?: Record<string, any> | null) {
if (!this.context.user) {
Expand Down Expand Up @@ -405,7 +437,8 @@ export class BucketClient {
/**
* Submit user feedback to Bucket. Must include either `score` or `comment`, or both.
*
* @returns
* @param payload The feedback details to submit.
* @returns The server response.
*/
async feedback(payload: Feedback) {
const userId =
Expand Down Expand Up @@ -501,35 +534,49 @@ export class BucketClient {
* and `isEnabled` does not take any feature overrides
* into account.
*
* @returns Map of features
* @returns Map of features.
*/
getFeatures(): RawFeatures {
return this.featuresClient.getFeatures();
}

/**
* Return a feature. Accessing `isEnabled` will automatically send a `check` event.
* @returns A feature
* Return a feature. Accessing `isEnabled` or `config` will automatically send a `check` event.
* @returns A feature.
*/
getFeature(key: string): Feature {
const f = this.getFeatures()[key];

const fClient = this.featuresClient;
const value = f?.isEnabledOverride ?? f?.isEnabled ?? false;
const config = f?.config
? {
key: f.config.key,
payload: f.config.payload,
}
: ({} as FeatureRemoteConfig);

function sendCheckEvent() {
fClient
.sendCheckEvent({
key: key,
pavkam marked this conversation as resolved.
Show resolved Hide resolved
version: f?.targetingVersion,
value,
})
.catch(() => {
// ignore
});
}
pavkam marked this conversation as resolved.
Show resolved Hide resolved

return {
get isEnabled() {
fClient
.sendCheckEvent({
key,
version: f?.targetingVersion,
value,
})
.catch(() => {
// ignore
});
sendCheckEvent();
return value;
},
get config() {
sendCheckEvent();
return config;
},
track: () => this.track(key),
requestFeedback: (
options: Omit<RequestFeedbackData, "featureKey" | "featureId">,
Expand Down
11 changes: 8 additions & 3 deletions packages/browser-sdk/src/feature/featureCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,24 @@ export function parseAPIFeaturesResponse(
const features: FetchedFeatures = {};
for (const key in featuresInput) {
const feature = featuresInput[key];

if (
typeof feature.isEnabled !== "boolean" ||
feature.key !== key ||
typeof feature.targetingVersion !== "number"
typeof feature.targetingVersion !== "number" ||
(feature.config && typeof feature.config !== "object")
) {
return;
}

features[key] = {
isEnabled: feature.isEnabled,
targetingVersion: feature.targetingVersion,
key,
config: feature.config,
};
}

return features;
}

Expand All @@ -45,8 +50,8 @@ export interface CacheResult {

export class FeatureCache {
private storage: StorageItem;
private staleTimeMs: number;
private expireTimeMs: number;
private readonly staleTimeMs: number;
private readonly expireTimeMs: number;

constructor({
storage,
Expand Down
Loading
Loading