diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts
index eb7488f6..91c1b458 100644
--- a/packages/browser-sdk/src/client.ts
+++ b/packages/browser-sdk/src/client.ts
@@ -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";
@@ -177,15 +182,10 @@ export type ToolbarOptions =
position?: ToolbarPosition;
};
-/**
- * Feature definitions.
- */
-export type FeatureDefinitions = Readonly>;
-
/**
* BucketClient initialization options.
*/
-export type InitOptions = {
+export type InitOptions = {
/**
* Publishable key for authentication
*/
@@ -284,7 +284,7 @@ export type InitOptions = {
/**
* Local-first definition of features
*/
- features?: FeatureDefinitions;
+ features?: TFeatures;
};
const defaultConfig: Config = {
@@ -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 {
/**
* Result of feature flag evaluation.
*/
@@ -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.
@@ -338,7 +333,7 @@ export interface Feature {
) => void;
}
-function shouldShowToolbar(opts: InitOptions) {
+function shouldShowToolbar(opts: InitOptions) {
const toolbarOpts = opts.toolbar;
if (typeof toolbarOpts === "boolean") return toolbarOpts;
if (typeof toolbarOpts?.show === "boolean") return toolbarOpts.show;
@@ -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,
+> {
private readonly publishableKey: string;
private readonly context: BucketContext;
private config: Config;
@@ -367,7 +365,7 @@ export class BucketClient {
/**
* Create a new BucketClient instance.
*/
- constructor(opts: InitOptions) {
+ constructor(opts: InitOptions) {
this.publishableKey = opts.publishableKey;
this.logger =
opts?.logger ?? loggerWithPrefix(quietConsoleLogger, "[Bucket]");
@@ -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`.
@@ -404,7 +405,7 @@ export class BucketClient {
company: this.context.company,
other: this.context.otherContext,
},
- opts?.features || [],
+ keys,
this.logger,
{
expireTimeMs: opts.expireTimeMs,
@@ -614,7 +615,7 @@ export class BucketClient {
*
* @param options
*/
- requestFeedback(options: RequestFeedbackData) {
+ requestFeedback(options: RequestFeedbackData>) {
if (!this.context.user?.id) {
this.logger.error(
"`requestFeedback` call ignored. No `user` context provided at initialization",
@@ -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(
+ key: TKey,
+ ): Feature> {
const f = this.getFeatures()[key];
const fClient = this.featuresClient;
diff --git a/packages/browser-sdk/src/config.ts b/packages/browser-sdk/src/constants.ts
similarity index 100%
rename from packages/browser-sdk/src/config.ts
rename to packages/browser-sdk/src/constants.ts
diff --git a/packages/browser-sdk/src/feature/features.ts b/packages/browser-sdk/src/feature/features.ts
index cd536e5d..07da92e4 100644
--- a/packages/browser-sdk/src/feature/features.ts
+++ b/packages/browser-sdk/src/feature/features.ts
@@ -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";
diff --git a/packages/browser-sdk/src/featureDefinitions.ts b/packages/browser-sdk/src/featureDefinitions.ts
new file mode 100644
index 00000000..2d2d97de
--- /dev/null
+++ b/packages/browser-sdk/src/featureDefinitions.ts
@@ -0,0 +1,41 @@
+export type ConfigDataType =
+ | "string"
+ | "number"
+ | "boolean"
+ | { [key: string]: ConfigDataType };
+
+export type FeatureDefinitions = Readonly<
+ Array
+>;
+
+export type StringTypeToTSType = C extends "string"
+ ? string
+ : C extends "number"
+ ? number
+ : C extends "boolean"
+ ? boolean
+ : C extends { [key: string]: ConfigDataType }
+ ? { [K in keyof C]: StringTypeToTSType }
+ : never;
+
+export type ConfigType =
+ Extract extends never
+ ? undefined
+ : StringTypeToTSType<
+ Extract["config"]
+ >;
+
+export type FeatureKey =
+ | Extract
+ | Extract["key"];
+
+/**
+ * Define features for the SDK
+ * @param features Feature definitions
+ * @returns Feature definitions, ready to plug into the SDK
+ */
+export function defineFeatures(
+ features: TFeatures,
+): TFeatures {
+ return features;
+}
diff --git a/packages/browser-sdk/src/feedback/feedback.ts b/packages/browser-sdk/src/feedback/feedback.ts
index 3e70bd74..dec3c2df 100644
--- a/packages/browser-sdk/src/feedback/feedback.ts
+++ b/packages/browser-sdk/src/feedback/feedback.ts
@@ -69,7 +69,7 @@ export function handleDeprecatedFeedbackOptions(
};
}
-export type RequestFeedbackData = Omit<
+export type RequestFeedbackData = Omit<
OpenFeedbackFormOptions,
"key" | "onSubmit"
> & {
@@ -93,7 +93,7 @@ export type RequestFeedbackData = Omit<
/**
* Bucket feature key.
*/
- featureKey: string;
+ featureKey: TFeatureKey;
};
export type RequestFeedbackOptions = RequestFeedbackData & {
diff --git a/packages/browser-sdk/src/httpClient.ts b/packages/browser-sdk/src/httpClient.ts
index 43c925f8..ce91b596 100644
--- a/packages/browser-sdk/src/httpClient.ts
+++ b/packages/browser-sdk/src/httpClient.ts
@@ -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;
diff --git a/packages/browser-sdk/src/index.ts b/packages/browser-sdk/src/index.ts
index a1044228..499bf1b4 100644
--- a/packages/browser-sdk/src/index.ts
+++ b/packages/browser-sdk/src/index.ts
@@ -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,
diff --git a/packages/browser-sdk/test/e2e/acceptance.browser.spec.ts b/packages/browser-sdk/test/e2e/acceptance.browser.spec.ts
index 1fc5e8c6..41cedcbf 100644
--- a/packages/browser-sdk/test/e2e/acceptance.browser.spec.ts
+++ b/packages/browser-sdk/test/e2e/acceptance.browser.spec.ts
@@ -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();
diff --git a/packages/browser-sdk/test/features.test.ts b/packages/browser-sdk/test/features.test.ts
index b0282fe0..d8360e64 100644
--- a/packages/browser-sdk/test/features.test.ts
+++ b/packages/browser-sdk/test/features.test.ts
@@ -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";
@@ -48,7 +48,7 @@ function featuresClientFactory() {
other: { eventId: "big-conference1" },
...context,
},
- features || [],
+ features?.map((f) => (f instanceof Object ? f.key : f)) || [],
testLogger,
{
cache,
diff --git a/packages/browser-sdk/test/usage.test.ts b/packages/browser-sdk/test/usage.test.ts
index b792fdb2..3ffbc293 100644
--- a/packages/browser-sdk/test/usage.test.ts
+++ b/packages/browser-sdk/test/usage.test.ts
@@ -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 {
diff --git a/packages/react-sdk/dev/plain/app.tsx b/packages/react-sdk/dev/plain/app.tsx
index 3890fa63..7ce5a81b 100644
--- a/packages/react-sdk/dev/plain/app.tsx
+++ b/packages/react-sdk/dev/plain/app.tsx
@@ -10,20 +10,14 @@ import {
useUpdateOtherContext,
useUpdateUser,
} from "../../src";
-
-// Extending the Features interface to define the available features
-declare module "../../src" {
- interface Features {
- huddles: boolean;
- }
-}
+import bucketFeatures from "./bucket.features";
const publishableKey = import.meta.env.VITE_PUBLISHABLE_KEY || "";
const apiBaseUrl = import.meta.env.VITE_BUCKET_API_BASE_URL;
function HuddleFeature() {
- // Type safe feature
const feature = useFeature("huddles");
+
return (
Huddle feature
@@ -148,7 +142,7 @@ function Feedback() {
onClick={(e) =>
requestFeedback({
title: "How do you like Huddles?",
- featureKey: "huddle",
+ featureKey: "huddles",
position: {
type: "POPOVER",
anchor: e.currentTarget as HTMLElement,
@@ -164,20 +158,26 @@ function Feedback() {
// App.tsx
function Demos() {
+ const { isEnabled, config } = useFeature("optIn");
+
return (
React SDK
-
Feature opt-in
-
- Create a huddle feature and set a rule:{" "}
- optin-huddles IS TRUE. Hit the checkbox below to opt-in/out
- of the feature.
-
-
-
+ {isEnabled && (
+ <>
+
Feature opt-in
+
+ Create a huddle feature and set a rule:{" "}
+ optin-huddles IS TRUE. Hit the checkbox below to
+ opt-in/out of the feature.
+ {config.payload?.additionalCopy}
+