Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 12 additions & 6 deletions packages/browser-sdk/example/typescript/app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BucketClient } from "../../src";
import { BucketClient, FeatureKey } from "../../src";
import features from "./bucket.config";

const urlParams = new URLSearchParams(window.location.search);
const publishableKey = urlParams.get("publishableKey");
const featureKey = urlParams.get("featureKey") ?? "huddles";

const featureList = ["huddles"];
const featureKey = (urlParams.get("featureKey") ?? "huddles") as FeatureKey<
typeof features
>;

if (!publishableKey) {
throw Error("publishableKey is missing");
Expand All @@ -18,7 +19,7 @@ const bucket = new BucketClient({
show: true,
position: { placement: "bottom-right" },
},
featureList,
features,
});

document
Expand All @@ -38,9 +39,14 @@ bucket.initialize().then(() => {
});

bucket.onFeaturesUpdated(() => {
const { isEnabled } = bucket.getFeature("huddles");
const { isEnabled, config } = bucket.getFeature("huddles");

const startHuddleElem = document.getElementById("start-huddle");
if (startHuddleElem) {
startHuddleElem.innerText =
config?.payload?.startHuddleCopy ?? "Start Huddle";
}

if (isEnabled) {
// show the start-huddle button
if (startHuddleElem) startHuddleElem.style.display = "block";
Expand Down
7 changes: 7 additions & 0 deletions packages/browser-sdk/example/typescript/bucket.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineFeatures } from "../../src";

export default defineFeatures([
"feature1",
"feature2",
{ key: "huddles", config: { startHuddleCopy: "string" } },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing config key?

]);
2 changes: 1 addition & 1 deletion packages/browser-sdk/example/typescript/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}
</style>
<div id="start-huddle" style="display: none">
Start huddle!<br />
<span id="start-huddle-copy">Start huddle!</span><br />
<button id="startHuddle">Click me</button>
<button id="giveFeedback">Give feedback!</button>
</div>
Expand Down
69 changes: 36 additions & 33 deletions packages/browser-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ import {
} from "./feedback/feedback";
import * as feedbackLib from "./feedback/ui";
import { ToolbarPosition } from "./toolbar/Toolbar";
import { API_BASE_URL, APP_BASE_URL, SSE_REALTIME_BASE_URL } from "./config";
import { API_BASE_URL, APP_BASE_URL, SSE_REALTIME_BASE_URL } from "./constants";
import { BucketContext, CompanyContext, UserContext } from "./context";
import {
ConfigType,
FeatureDefinitions,
FeatureKey,
} from "./featureDefinitions";
import { HttpClient } from "./httpClient";
import { Logger, loggerWithPrefix, quietConsoleLogger } from "./logger";
import { showToolbarToggle } from "./toolbar";
Expand Down Expand Up @@ -177,15 +182,10 @@ export type ToolbarOptions =
position?: ToolbarPosition;
};

/**
* Feature definitions.
*/
export type FeatureDefinitions = Readonly<Array<string>>;

/**
* BucketClient initialization options.
*/
export type InitOptions = {
export type InitOptions<TFeatures extends FeatureDefinitions = any> = {
/**
* Publishable key for authentication
*/
Expand Down Expand Up @@ -284,7 +284,7 @@ export type InitOptions = {
/**
* Local-first definition of features
*/
features?: FeatureDefinitions;
features?: TFeatures;
};

const defaultConfig: Config = {
Expand All @@ -294,27 +294,10 @@ 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 optional user-supplied payload data.
*/
payload: any;
}
| { key: undefined; payload: undefined };

/**
* A feature.
*/
export interface Feature {
export interface Feature<TRemoteConfig = any> {
/**
* Result of feature flag evaluation.
*/
Expand All @@ -323,7 +306,19 @@ export interface Feature {
/*
* Optional user-defined configuration.
*/
config: FeatureRemoteConfig;
config:
| {
/**
* The key of the matched configuration value.
*/
key: string;

/**
* The optional user-supplied payload data.
*/
payload: TRemoteConfig;
}
| { key: undefined; payload: undefined };

/**
* Function to send analytics events for this feature.
Expand All @@ -338,7 +333,7 @@ export interface Feature {
) => void;
}

function shouldShowToolbar(opts: InitOptions) {
function shouldShowToolbar(opts: InitOptions<any>) {
const toolbarOpts = opts.toolbar;
if (typeof toolbarOpts === "boolean") return toolbarOpts;
if (typeof toolbarOpts?.show === "boolean") return toolbarOpts.show;
Expand All @@ -351,7 +346,10 @@ function shouldShowToolbar(opts: InitOptions) {
/**
* BucketClient lets you interact with the Bucket API.
*/
export class BucketClient {
export class BucketClient<
TFeatures extends FeatureDefinitions = any,
TFeatureKey extends string = FeatureKey<TFeatures>,
> {
private readonly publishableKey: string;
private readonly context: BucketContext;
private config: Config;
Expand All @@ -367,7 +365,7 @@ export class BucketClient {
/**
* Create a new BucketClient instance.
*/
constructor(opts: InitOptions) {
constructor(opts: InitOptions<TFeatures>) {
this.publishableKey = opts.publishableKey;
this.logger =
opts?.logger ?? loggerWithPrefix(quietConsoleLogger, "[Bucket]");
Expand Down Expand Up @@ -396,6 +394,9 @@ export class BucketClient {
sdkVersion: opts?.sdkVersion,
});

const keys =
opts.features?.map((f) => (f instanceof Object ? f.key : f)) ?? [];

this.featuresClient = new FeaturesClient(
this.httpClient,
// API expects `other` and we have `otherContext`.
Expand All @@ -404,7 +405,7 @@ export class BucketClient {
company: this.context.company,
other: this.context.otherContext,
},
opts?.features || [],
keys,
this.logger,
{
expireTimeMs: opts.expireTimeMs,
Expand Down Expand Up @@ -614,7 +615,7 @@ export class BucketClient {
*
* @param options
*/
requestFeedback(options: RequestFeedbackData) {
requestFeedback(options: RequestFeedbackData<FeatureKey<TFeatures>>) {
if (!this.context.user?.id) {
this.logger.error(
"`requestFeedback` call ignored. No `user` context provided at initialization",
Expand Down Expand Up @@ -692,7 +693,9 @@ export class BucketClient {
* Return a feature. Accessing `isEnabled` or `config` will automatically send a `check` event.
* @returns A feature.
*/
getFeature(key: string): Feature {
getFeature<TKey extends TFeatureKey>(
key: TKey,
): Feature<ConfigType<TFeatures, TKey>> {
const f = this.getFeatures()[key];

const fClient = this.featuresClient;
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-sdk/src/feature/features.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FEATURE_EVENTS_PER_MIN } from "../config";
import { FEATURE_EVENTS_PER_MIN } from "../constants";
import { HttpClient } from "../httpClient";
import { Logger, loggerWithPrefix } from "../logger";
import RateLimiter from "../rateLimiter";
Expand Down
41 changes: 41 additions & 0 deletions packages/browser-sdk/src/featureDefinitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export type ConfigDataType =
| "string"
| "number"
| "boolean"
| { [key: string]: ConfigDataType };

export type FeatureDefinitions = Readonly<
Array<string | { key: string; config?: ConfigDataType }>
>;

export type StringTypeToTSType<C extends ConfigDataType> = C extends "string"
? string
: C extends "number"
? number
: C extends "boolean"
? boolean
: C extends { [key: string]: ConfigDataType }
? { [K in keyof C]: StringTypeToTSType<C[K]> }
: never;

export type ConfigType<Defs extends FeatureDefinitions, Key extends string> =
Extract<Defs[number], { key: Key; config: any }> extends never
? undefined
: StringTypeToTSType<
Extract<Defs[number], { key: Key; config: any }>["config"]
>;

export type FeatureKey<Defs extends FeatureDefinitions> =
| Extract<Defs[number], string>
| Extract<Defs[number], { key: string }>["key"];

/**
* Define features for the SDK
* @param features Feature definitions
* @returns Feature definitions, ready to plug into the SDK
*/
export function defineFeatures<const TFeatures extends FeatureDefinitions>(
features: TFeatures,
): TFeatures {
return features;
}
4 changes: 2 additions & 2 deletions packages/browser-sdk/src/feedback/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function handleDeprecatedFeedbackOptions(
};
}

export type RequestFeedbackData = Omit<
export type RequestFeedbackData<TFeatureKey extends string = string> = Omit<
OpenFeedbackFormOptions,
"key" | "onSubmit"
> & {
Expand All @@ -93,7 +93,7 @@ export type RequestFeedbackData = Omit<
/**
* Bucket feature key.
*/
featureKey: string;
featureKey: TFeatureKey;
};

export type RequestFeedbackOptions = RequestFeedbackData & {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-sdk/src/httpClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { API_BASE_URL, SDK_VERSION, SDK_VERSION_HEADER_NAME } from "./config";
import { API_BASE_URL, SDK_VERSION, SDK_VERSION_HEADER_NAME } from "./constants";

export interface HttpClientOptions {
baseUrl?: string;
Expand Down
6 changes: 6 additions & 0 deletions packages/browser-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export type {
RawFeature,
RawFeatures,
} from "./feature/features";
export type {
ConfigType,
FeatureDefinitions,
FeatureKey,
} from "./featureDefinitions";
export { defineFeatures } from "./featureDefinitions";
export type {
Feedback,
FeedbackOptions,
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-sdk/test/e2e/acceptance.browser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { randomUUID } from "crypto";
import { expect, test } from "@playwright/test";

import { API_BASE_URL } from "../../src/config";
import { API_BASE_URL } from "../../src/constants";

const KEY = randomUUID();

Expand Down
4 changes: 2 additions & 2 deletions packages/browser-sdk/test/features.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { afterAll, beforeEach, describe, expect, test, vi } from "vitest";

import { version } from "../package.json";
import { FeatureDefinitions } from "../src/client";
import {
FEATURES_EXPIRE_MS,
FeaturesClient,
FetchedFeature,
RawFeature,
} from "../src/feature/features";
import { FeatureDefinitions } from "../src/featureDefinitions";
import { HttpClient } from "../src/httpClient";

import { featuresResult } from "./mocks/handlers";
Expand Down Expand Up @@ -48,7 +48,7 @@ function featuresClientFactory() {
other: { eventId: "big-conference1" },
...context,
},
features || [],
features?.map((f) => (f instanceof Object ? f.key : f)) || [],
testLogger,
{
cache,
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-sdk/test/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "vitest";

import { BucketClient } from "../src";
import { API_BASE_URL } from "../src/config";
import { API_BASE_URL } from "../src/constants";
import { FeaturesClient } from "../src/feature/features";
import { FeedbackPromptHandler } from "../src/feedback/feedback";
import {
Expand Down
Loading
Loading