diff --git a/.github/workflows/package-ci.yml b/.github/workflows/package-ci.yml
index f753fcac..e8a49da4 100644
--- a/.github/workflows/package-ci.yml
+++ b/.github/workflows/package-ci.yml
@@ -25,9 +25,6 @@ jobs:
- name: Restore package.json
# This step is necessary because the previous step may have updated package.json
run: git checkout -- package.json packages/*/package.json
- - name: Install Playwright Browsers
- run: yarn playwright install --with-deps
- working-directory: ./packages/tracking-sdk
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
working-directory: ./packages/browser-sdk
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 43d9ceba..54de58ef 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -39,12 +39,12 @@ jobs:
path: bucket-docs
- name: Copy generated docs to docs repo
run: |
- rm -rf bucket-docs/sdk-docs
- cp -R dist/docs bucket-docs/sdk-docs
+ rm -rf bucket-docs/sdk
+ cp -R dist/docs bucket-docs/sdk
- name: Commit and push changes
run: |
cd bucket-docs
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@bucket.co"
- git add sdk-docs
+ git add sdk
git commit -m "Update documentation" && git push || echo "No docs changes to commit"
diff --git a/.gitignore b/.gitignore
index abe7f314..c25132d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,4 @@ junit.xml
.next
eslint-report.json
+bucket.config.json
diff --git a/package.json b/package.json
index 050c213f..a6a9d970 100644
--- a/package.json
+++ b/package.json
@@ -24,10 +24,10 @@
"packageManager": "yarn@4.1.1",
"devDependencies": {
"lerna": "^8.1.3",
- "prettier": "^3.3.3",
+ "prettier": "^3.5.2",
"typedoc": "0.27.6",
"typedoc-plugin-frontmatter": "^1.1.2",
- "typedoc-plugin-markdown": "^4.4.1",
+ "typedoc-plugin-markdown": "^4.4.2",
"typedoc-plugin-mdn-links": "^4.0.7",
"typescript": "^5.7.3"
}
diff --git a/packages/browser-sdk/FEEDBACK.md b/packages/browser-sdk/FEEDBACK.md
index f93d2626..6dcdc80f 100644
--- a/packages/browser-sdk/FEEDBACK.md
+++ b/packages/browser-sdk/FEEDBACK.md
@@ -351,13 +351,7 @@ properties to your page in your CSS `:root`-scope.
For example, a dark mode theme might look like this:
-```html
-
-```
+
```css
:root {
diff --git a/packages/browser-sdk/README.md b/packages/browser-sdk/README.md
index 588e9ae4..82090d11 100644
--- a/packages/browser-sdk/README.md
+++ b/packages/browser-sdk/README.md
@@ -2,6 +2,8 @@
Basic client for Bucket.co. If you're using React, you'll be better off with the Bucket React SDK.
+Bucket supports feature toggling, tracking feature usage, [collecting feedback](#qualitative-feedback) on features, and [remotely configuring features](#remote-config-beta).
+
## Install
First find your `publishableKey` under [environment settings](https://app.bucket.co/envs/current/settings/app-environments) in Bucket.
@@ -100,15 +102,13 @@ type Configuration = {
sseBaseUrl?: "https://livemessaging.bucket.co";
feedback?: undefined; // See FEEDBACK.md
enableTracking?: true; // set to `false` to stop sending track events and user/company updates to Bucket servers. Useful when you're impersonating a user
- featureOptions?: {
- fallbackFeatures?:
- | string[]
- | Record; // Enable these features if unable to contact bucket.co. Can be a list of feature keys or a record with configuration values
- timeoutMs?: number; // Timeout for fetching features (default: 5000ms)
- staleWhileRevalidate?: boolean; // Revalidate in the background when cached features turn stale to avoid latency in the UI (default: false)
- staleTimeMs?: number; // at initialization time features are loaded from the cache unless they have gone stale. Defaults to 0 which means the cache is disabled. Increase in the case of a non-SPA
- expireTimeMs?: number; // In case we're unable to fetch features from Bucket, cached/stale features will be used instead until they expire after `expireTimeMs`. Default is 30 days
- };
+ fallbackFeatures?:
+ | string[]
+ | Record; // Enable these features if unable to contact bucket.co. Can be a list of feature keys or a record with configuration values
+ timeoutMs?: number; // Timeout for fetching features (default: 5000ms)
+ staleWhileRevalidate?: boolean; // Revalidate in the background when cached features turn stale to avoid latency in the UI (default: false)
+ staleTimeMs?: number; // at initialization time features are loaded from the cache unless they have gone stale. Defaults to 0 which means the cache is disabled. Increase this in the case of a non-SPA
+ expireTimeMs?: number; // In case we're unable to fetch features from Bucket, cached/stale features will be used instead until they expire after `expireTimeMs`. Default is 30 days
};
```
@@ -174,44 +174,7 @@ 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 returned by `getFeature`.
-### Feature Overrides
-
-You can override feature flags locally for testing purposes using `setFeatureOverride`:
-
-```ts
-// Override a feature to be enabled
-bucketClient.setFeatureOverride("huddle", true);
-
-// Override a feature to be disabled
-bucketClient.setFeatureOverride("huddle", false);
-
-// Remove the override
-bucketClient.setFeatureOverride("huddle", null);
-
-// Get current override value
-const override = bucketClient.getFeatureOverride("huddle"); // returns boolean | null
-```
-
-Feature overrides are persisted in `localStorage` and will be restored when the page is reloaded.
-
-### Feature Updates
-
-You can listen for feature updates using `onFeaturesUpdated`:
-
-```ts
-// Register a callback for feature updates
-const unsubscribe = bucketClient.onFeaturesUpdated(() => {
- console.log("Features were updated");
-});
-
-// Later, stop listening for updates
-unsubscribe();
-```
-
-> [!NOTE]
-> Note that the callback may be called even if features haven't actually changed.
-
-### Remote config
+### Remote config (beta)
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 of the binary `isEnabled` you can have
@@ -312,11 +275,12 @@ See details in [Feedback HTTP API](https://docs.bucket.co/reference/http-trackin
Event listeners allow for capturing various events occurring in the `BucketClient`. This is useful to build integrations with other system or for various debugging purposes. There are 5 kinds of events:
-- FeaturesUpdated
-- User
-- Company
-- Check
-- Track
+- `configCheck`: Your code used a feature config
+- `enabledCheck`: Your code checked whether a specific feature should be enabled
+- `featuresUpdated`: Features were updated. Either because they were loaded as part of initialization or because the user/company updated
+- `user`: User information updated (similar to the `identify` call used in tracking terminology)
+- `company`: Company information updated (sometimes to the `group` call used in tracking terminology)
+- `track`: Track event occurred.
Use the `on()` method to add an event listener to respond to certain events. See the API reference for details on each hook.
diff --git a/packages/browser-sdk/eslint.config.js b/packages/browser-sdk/eslint.config.js
index 75295e2d..53da33e1 100644
--- a/packages/browser-sdk/eslint.config.js
+++ b/packages/browser-sdk/eslint.config.js
@@ -1,3 +1,23 @@
-const base = require("@bucketco/eslint-config/base");
+const base = require("@bucketco/eslint-config");
-module.exports = [...base, { ignores: ["dist/", "example/"] }];
+module.exports = [
+ ...base,
+ {
+ // Preact projects
+ files: ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"],
+ settings: {
+ react: {
+ // We only care about marking h() as being a used variable.
+ pragma: "h",
+ // We use "react 16.0" to avoid pushing folks to UNSAFE_ methods.
+ version: "16.0",
+ },
+ },
+ rules: {
+ // Ignore React attributes that are not valid in Preact.
+ // Alternatively, we could use the preact/compat alias or turn off the rule.
+ "react/no-unknown-property": ["off"],
+ },
+ },
+ { ignores: ["dist/", "example/"] },
+];
diff --git a/packages/browser-sdk/index.html b/packages/browser-sdk/index.html
index 80353b29..6768647d 100644
--- a/packages/browser-sdk/index.html
+++ b/packages/browser-sdk/index.html
@@ -7,7 +7,7 @@
Bucket Browser SDK
-
+
Loading...
diff --git a/packages/browser-sdk/package.json b/packages/browser-sdk/package.json
index b9590217..9323ab7b 100644
--- a/packages/browser-sdk/package.json
+++ b/packages/browser-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@bucketco/browser-sdk",
- "version": "3.0.0-alpha.4",
+ "version": "3.0.0-alpha.6",
"packageManager": "yarn@4.1.1",
"license": "MIT",
"repository": {
@@ -45,23 +45,22 @@
"@bucketco/eslint-config": "0.0.2",
"@bucketco/tsconfig": "0.0.2",
"@playwright/test": "^1.49.1",
+ "@types/js-cookie": "^3.0.6",
"@types/node": "^22.12.0",
- "@types/webpack": "^5.28.5",
- "css-loader": "^6.9.0",
- "eslint": "^8.57.0",
+ "@vitest/coverage-v8": "^2.0.4",
+ "c8": "~10.1.3",
+ "eslint": "^9.21.0",
+ "http-server": "^14.1.1",
"jsdom": "^24.1.0",
"msw": "^2.3.4",
+ "nock": "^14.0.1",
"postcss": "^8.4.33",
- "postcss-loader": "^7.3.4",
"postcss-nesting": "^12.0.2",
"postcss-preset-env": "^9.3.0",
- "prettier": "^3.2.5",
- "style-loader": "^3.3.4",
+ "prettier": "^3.5.2",
"typescript": "^5.7.3",
"vite": "^5.3.5",
"vite-plugin-dts": "^4.0.0-beta.1",
- "vitest": "^2.0.4",
- "webpack": "^5.89.0",
- "webpack-cli": "^5.1.4"
+ "vitest": "^2.0.4"
}
}
diff --git a/packages/browser-sdk/playwright.config.ts b/packages/browser-sdk/playwright.config.ts
index 1e335ac2..b9d9d09c 100644
--- a/packages/browser-sdk/playwright.config.ts
+++ b/packages/browser-sdk/playwright.config.ts
@@ -34,5 +34,6 @@ export default defineConfig({
// separate port to let the app run alongside the tracking sdk tests
command: "npx http-server . -p 8001",
timeout: 120 * 1000,
+ port: 8001,
},
});
diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts
index 77e4fa9c..789b80d4 100644
--- a/packages/browser-sdk/src/client.ts
+++ b/packages/browser-sdk/src/client.ts
@@ -307,7 +307,7 @@ export type FeatureRemoteConfig =
| { key: undefined; payload: undefined };
/**
- * A feature.
+ * Represents a feature.
*/
export interface Feature {
/**
@@ -770,15 +770,21 @@ export class BucketClient {
};
}
+ /**
+ * @internal
+ */
setFeatureOverride(key: string, isEnabled: boolean | null) {
this.featuresClient.setFeatureOverride(key, isEnabled);
}
+ /**
+ * @internal
+ */
getFeatureOverride(key: string): boolean | null {
return this.featuresClient.getFeatureOverride(key);
}
- sendCheckEvent(checkEvent: CheckEvent) {
+ private sendCheckEvent(checkEvent: CheckEvent) {
return this.featuresClient.sendCheckEvent(checkEvent, () => {
this.hooks.trigger(
checkEvent.action == "check-config" ? "configCheck" : "enabledCheck",
diff --git a/packages/browser-sdk/src/feature/featureCache.ts b/packages/browser-sdk/src/feature/featureCache.ts
index 70f26159..7e851f22 100644
--- a/packages/browser-sdk/src/feature/featureCache.ts
+++ b/packages/browser-sdk/src/feature/featureCache.ts
@@ -88,7 +88,7 @@ export class FeatureCache {
if (cachedResponseRaw) {
cacheData = validateCacheData(JSON.parse(cachedResponseRaw)) ?? {};
}
- } catch (e) {
+ } catch {
// ignore errors
}
@@ -123,7 +123,7 @@ export class FeatureCache {
};
}
}
- } catch (e) {
+ } catch {
// ignore errors
}
return;
diff --git a/packages/browser-sdk/src/feature/features.ts b/packages/browser-sdk/src/feature/features.ts
index 8ed47fe9..75bc696a 100644
--- a/packages/browser-sdk/src/feature/features.ts
+++ b/packages/browser-sdk/src/feature/features.ts
@@ -20,6 +20,7 @@ export type FetchedFeature = {
/**
* Result of feature flag evaluation.
+ * Note: does not take local overrides into account.
*/
isEnabled: boolean;
@@ -71,9 +72,11 @@ export type FetchedFeature = {
const FEATURES_UPDATED_EVENT = "featuresUpdated";
+/**
+ * @internal
+ */
export type FetchedFeatures = Record;
-// todo: on next major, come up with a better name for this type. Maybe `LocalFeature`.
export type RawFeature = FetchedFeature & {
/**
* If not null, the result is being overridden locally
@@ -328,7 +331,7 @@ export class FeaturesClient {
let errorBody = null;
try {
errorBody = await res.json();
- } catch (e) {
+ } catch {
// ignore
}
diff --git a/packages/browser-sdk/src/feedback/promptStorage.ts b/packages/browser-sdk/src/feedback/promptStorage.ts
index bea0ed9d..905e9da0 100644
--- a/packages/browser-sdk/src/feedback/promptStorage.ts
+++ b/packages/browser-sdk/src/feedback/promptStorage.ts
@@ -51,7 +51,7 @@ export const getAuthToken = (userId: string) => {
channel,
token,
};
- } catch (e) {
+ } catch {
return undefined;
}
};
diff --git a/packages/browser-sdk/src/feedback/ui/Button.css b/packages/browser-sdk/src/feedback/ui/Button.css
index acf690f3..859a662e 100644
--- a/packages/browser-sdk/src/feedback/ui/Button.css
+++ b/packages/browser-sdk/src/feedback/ui/Button.css
@@ -21,8 +21,8 @@
0 1px 1px 0 rgba(0, 0, 0, 0.01);
border-radius: var(--bucket-feedback-dialog-border-radius, 6px);
transition-duration: 200ms;
- transition-property: background-color, border-color, color, opacity,
- box-shadow, transform;
+ transition-property:
+ background-color, border-color, color, opacity, box-shadow, transform;
&.primary {
background-color: var(
@@ -40,8 +40,8 @@
border-color: var(--bucket-feedback-dialog-primary-border-color, #d8d9df);
transition-duration: 200ms;
- transition-property: background-color, border-color, color, opacity,
- box-shadow, transform;
+ transition-property:
+ background-color, border-color, color, opacity, box-shadow, transform;
}
&:focus {
diff --git a/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx b/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx
index 2893ebbe..f551045f 100644
--- a/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx
+++ b/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx
@@ -41,12 +41,20 @@ export const FeedbackDialog: FunctionComponent = ({
"idle" | "submitting" | "submitted"
>("idle");
+ const { isOpen, close } = useDialog({ onClose, initialValue: true });
+
+ const autoClose = useTimer({
+ enabled: position.type === "DIALOG",
+ initialDuration: INACTIVE_DURATION_MS,
+ onEnd: close,
+ });
+
const submit = useCallback(
async (data: Omit) => {
await onSubmit({ ...data, feedbackId });
autoClose.startWithDuration(SUCCESS_DURATION_MS);
},
- [feedbackId, onSubmit],
+ [autoClose, feedbackId, onSubmit],
);
const submitScore = useCallback(
@@ -59,17 +67,8 @@ export const FeedbackDialog: FunctionComponent = ({
setScoreState("submitted");
}
},
- [feedbackId, onSubmit],
+ [feedbackId, onScoreSubmit],
);
-
- const { isOpen, close } = useDialog({ onClose, initialValue: true });
-
- const autoClose = useTimer({
- enabled: position.type === "DIALOG",
- initialDuration: INACTIVE_DURATION_MS,
- onEnd: close,
- });
-
const dismiss = useCallback(() => {
autoClose.stop();
close();
@@ -78,25 +77,25 @@ export const FeedbackDialog: FunctionComponent = ({
return (
<>
-
+