Skip to content

Commit 553ae6b

Browse files
authored
Merge branch 'main' into feat/new-bucket-cli
2 parents ff38810 + 1736c14 commit 553ae6b

24 files changed

Lines changed: 277 additions & 82 deletions

File tree

packages/browser-sdk/README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ generate a `check` event, contrary to the `isEnabled` property on the object ret
176176

177177
### Remote config (beta)
178178

179+
Remote config is a dynamic and flexible approach to configuring feature behavior outside of your app – without needing to re-deploy it.
180+
179181
Similar to `isEnabled`, each feature has a `config` property. This configuration is managed from within Bucket.
180182
It is managed similar to the way access to features is managed, but instead of the binary `isEnabled` you can have
181183
multiple configuration values which are given to different user/companies.
@@ -194,10 +196,7 @@ const features = bucketClient.getFeatures();
194196
// }
195197
```
196198

197-
The `key` is always present while the `payload` is a optional JSON value for arbitrary configuration needs.
198-
If feature has no configuration or, no configuration value was matched against the context, the `config` object
199-
will be empty, thus, `key` will be `undefined`. Make sure to check against this case when trying to use the
200-
configuration in your application.
199+
`key` is mandatory for a config, but if a feature has no config or no config value was matched against the context, the `key` will be `undefined`. Make sure to check against this case when trying to use the configuration in your application. `payload` is an optional JSON value for arbitrary configuration needs.
201200

202201
Just as `isEnabled`, accessing `config` on the object returned by `getFeatures` does not automatically
203202
generate a `check` event, contrary to the `config` property on the object returned by `getFeature`.
@@ -321,6 +320,13 @@ The two cookies are:
321320
- `bucket-prompt-${userId}`: store the last automated feedback prompt message ID received to avoid repeating surveys
322321
- `bucket-token-${userId}`: caching a token used to connect to Bucket's live messaging infrastructure that is used to deliver automated feedback surveys in real time.
323322

323+
### Upgrading to 3.0 from 2.x
324+
325+
Breaking changes:
326+
327+
- `client.onFeaturesUpdated()` is now replaced by [event listeners](#event-listeners)
328+
- Arguments to the `BucketClient` constructor which were previously under `featureOptions` are now supplied directly in the root.
329+
324330
### TypeScript
325331

326332
Types are bundled together with the library and exposed automatically when importing through a package manager.

packages/browser-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bucketco/browser-sdk",
3-
"version": "3.0.0-alpha.6",
3+
"version": "3.1.0",
44
"packageManager": "yarn@4.1.1",
55
"license": "MIT",
66
"repository": {

packages/browser-sdk/src/client.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,13 @@ export type InitOptions = {
256256
*/
257257
staleTimeMs?: number;
258258

259+
/**
260+
* When proxying requests, you may want to include credentials like cookies
261+
* so you can authorize the request in the proxy.
262+
* This option controls the `credentials` option of the fetch API.
263+
*/
264+
credentials?: "include" | "same-origin" | "omit";
265+
259266
/**
260267
* Base URL of Bucket servers for SSE connections used by AutoFeedback.
261268
*/
@@ -387,6 +394,7 @@ export class BucketClient {
387394
this.httpClient = new HttpClient(this.publishableKey, {
388395
baseUrl: this.config.apiBaseUrl,
389396
sdkVersion: opts?.sdkVersion,
397+
credentials: opts?.credentials,
390398
});
391399

392400
this.featuresClient = new FeaturesClient(
@@ -458,7 +466,6 @@ export class BucketClient {
458466
}
459467

460468
await this.featuresClient.initialize();
461-
462469
if (this.context.user && this.config.enableTracking) {
463470
this.user().catch((e) => {
464471
this.logger.error("error sending user", e);
@@ -476,12 +483,14 @@ export class BucketClient {
476483
* Add a hook to the client.
477484
*
478485
* @param hook Hook to add.
486+
* @returns A function to remove the hook.
479487
*/
480488
on<THookType extends keyof HookArgs>(
481489
type: THookType,
482490
handler: (args0: HookArgs[THookType]) => void,
483491
) {
484492
this.hooks.addHook(type, handler);
493+
return () => this.hooks.removeHook(type, handler);
485494
}
486495

487496
/**
@@ -796,8 +805,6 @@ export class BucketClient {
796805
/**
797806
* Stop the SDK.
798807
* This will stop any automated feedback surveys.
799-
* It will also stop the features client, including removing
800-
* any onFeaturesUpdated listeners.
801808
*
802809
**/
803810
async stop() {

packages/browser-sdk/src/hooksManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface HookArgs {
1313
track: TrackEvent;
1414
}
1515

16-
type TrackEvent = {
16+
export type TrackEvent = {
1717
user: UserContext;
1818
company?: CompanyContext;
1919
eventName: string;

packages/browser-sdk/src/httpClient.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import { API_BASE_URL, SDK_VERSION, SDK_VERSION_HEADER_NAME } from "./config";
33
export interface HttpClientOptions {
44
baseUrl?: string;
55
sdkVersion?: string;
6+
credentials?: RequestCredentials;
67
}
78

89
export class HttpClient {
910
private readonly baseUrl: string;
1011
private readonly sdkVersion: string;
1112

13+
private readonly fetchOptions: RequestInit;
14+
1215
constructor(
1316
public publishableKey: string,
1417
opts: HttpClientOptions = {},
@@ -21,6 +24,7 @@ export class HttpClient {
2124
this.baseUrl += "/";
2225
}
2326
this.sdkVersion = opts.sdkVersion ?? SDK_VERSION;
27+
this.fetchOptions = { credentials: opts.credentials };
2428
}
2529

2630
getUrl(path: string): URL {
@@ -50,13 +54,14 @@ export class HttpClient {
5054
url.search = params.toString();
5155

5256
if (timeoutMs === undefined) {
53-
return fetch(url);
57+
return fetch(url, this.fetchOptions);
5458
}
5559

5660
const controller = new AbortController();
5761
const id = setTimeout(() => controller.abort(), timeoutMs);
5862

5963
const res = await fetch(url, {
64+
...this.fetchOptions,
6065
signal: controller.signal,
6166
});
6267
clearTimeout(id);
@@ -73,6 +78,7 @@ export class HttpClient {
7378
body: any;
7479
}): ReturnType<typeof fetch> {
7580
return fetch(this.getUrl(path), {
81+
...this.fetchOptions,
7682
method: "POST",
7783
headers: {
7884
"Content-Type": "application/json",

packages/browser-sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ export type {
2828
OnScoreSubmitResult,
2929
OpenFeedbackFormOptions,
3030
} from "./feedback/ui/types";
31+
export type { TrackEvent } from "./hooksManager";
3132
export type { Logger } from "./logger";
3233
export { feedbackContainerId, propagatedEvents } from "./ui/constants";

packages/browser-sdk/src/toolbar/Features.css

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,19 @@
4747
opacity: 0;
4848
transform: translateY(-7px);
4949
transition-property: opacity, transform;
50-
transition-duration: 0.1s;
50+
transition-duration: 0.075s;
5151
transition-timing-function: cubic-bezier(0.75, -0.015, 0.565, 1.055);
5252

53-
&.show {
53+
&.show-on-open {
5454
opacity: 1;
5555
transform: translateY(0);
56-
transition-delay: calc(0.01s * var(--i));
56+
/* stagger effect where first item (i=0) has no delay,
57+
and delay is based on item count (n) so total animation time always is 509ms */
58+
transition-delay: calc(0.05s * var(--i) / max(var(--n) - 1, 1));
59+
}
60+
61+
&.not-visible {
62+
visibility: hidden;
5763
}
5864
}
5965

@@ -63,12 +69,23 @@
6369
text-overflow: ellipsis;
6470
width: auto;
6571
padding: 6px 6px 6px 0;
72+
display: flex;
73+
align-items: center;
74+
gap: 8px;
75+
76+
.feature-icon {
77+
height: 15px;
78+
width: 15px;
79+
color: var(--dimmed-color);
80+
}
6681
}
6782

6883
.feature-link {
6984
color: var(--text-color);
7085
text-decoration: none;
71-
&:hover {
86+
87+
&:hover,
88+
&:focus-visible {
7289
text-decoration: underline;
7390
}
7491
}
@@ -81,6 +98,13 @@
8198

8299
.reset {
83100
color: var(--brand300);
101+
102+
text-decoration: none;
103+
104+
&:hover,
105+
&:focus-visible {
106+
text-decoration: underline;
107+
}
84108
}
85109

86110
.feature-switch-cell {

packages/browser-sdk/src/toolbar/Features.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,47 @@
11
import { h } from "preact";
22
import { useEffect, useState } from "preact/hooks";
33

4+
import { Feature } from "../ui/icons/Feature";
5+
46
import { Switch } from "./Switch";
57
import { FeatureItem } from "./Toolbar";
68

79
export function FeaturesTable({
810
features,
11+
searchQuery,
912
setEnabledOverride,
1013
appBaseUrl,
1114
isOpen,
1215
}: {
1316
features: FeatureItem[];
17+
searchQuery: string | null;
1418
setEnabledOverride: (key: string, value: boolean | null) => void;
1519
appBaseUrl: string;
1620
isOpen: boolean;
1721
}) {
18-
if (features.length === 0) {
22+
const searchedFeatures =
23+
searchQuery === null
24+
? features
25+
: [...features].sort((a, _b) => (a.key.includes(searchQuery) ? -1 : 1));
26+
27+
if (searchedFeatures.length === 0) {
1928
return <div style={{ color: "var(--gray500)" }}>No features found</div>;
2029
}
2130
return (
22-
<table class="features-table">
31+
<table class="features-table" style={{ "--n": searchedFeatures.length }}>
2332
<tbody>
24-
{features.map((feature, index) => (
33+
{searchedFeatures.map((feature, index) => (
2534
<FeatureRow
2635
key={feature.key}
2736
appBaseUrl={appBaseUrl}
2837
feature={feature}
2938
index={index}
39+
isNotVisible={
40+
searchQuery !== null &&
41+
!feature.key
42+
.toLocaleLowerCase()
43+
.includes(searchQuery.toLocaleLowerCase())
44+
}
3045
isOpen={isOpen}
3146
setEnabledOverride={setEnabledOverride}
3247
/>
@@ -42,28 +57,36 @@ function FeatureRow({
4257
feature,
4358
isOpen,
4459
index,
60+
isNotVisible,
4561
}: {
4662
feature: FeatureItem;
4763
appBaseUrl: string;
4864
setEnabledOverride: (key: string, value: boolean | null) => void;
4965
isOpen: boolean;
5066
index: number;
67+
isNotVisible: boolean;
5168
}) {
52-
const [show, setShow] = useState(true);
69+
const [showOnOpen, setShowOnOpen] = useState(isOpen);
5370
useEffect(() => {
54-
setShow(isOpen);
71+
setShowOnOpen(isOpen);
5572
}, [isOpen]);
5673
return (
5774
<tr
5875
key={feature.key}
59-
class={["feature-row", show ? "show" : undefined].join(" ")}
76+
class={[
77+
"feature-row",
78+
showOnOpen ? "show-on-open" : undefined,
79+
isNotVisible ? "not-visible" : undefined,
80+
].join(" ")}
6081
style={{ "--i": index }}
6182
>
6283
<td class="feature-name-cell">
84+
<Feature class="feature-icon" />
6385
<a
6486
class="feature-link"
6587
href={`${appBaseUrl}/envs/current/features/by-key/${feature.key}`}
6688
rel="noreferrer"
89+
tabIndex={index + 1}
6790
target="_blank"
6891
>
6992
{feature.key}
@@ -74,6 +97,7 @@ function FeatureRow({
7497
<Reset
7598
featureKey={feature.key}
7699
setEnabledOverride={setEnabledOverride}
100+
tabIndex={index + 1}
77101
/>
78102
) : null}
79103
</td>
@@ -83,6 +107,7 @@ function FeatureRow({
83107
(feature.localOverride === null && feature.isEnabled) ||
84108
feature.localOverride === true
85109
}
110+
tabIndex={index + 1}
86111
onChange={(e) => {
87112
const isChecked = e.currentTarget.checked;
88113
const isOverridden = isChecked !== feature.isEnabled;
@@ -103,6 +128,7 @@ export function FeatureSearch({
103128
<input
104129
class="search-input"
105130
placeholder="Search features"
131+
tabIndex={0}
106132
type="search"
107133
autoFocus
108134
onInput={(s) => onSearch(s.currentTarget.value)}
@@ -113,10 +139,11 @@ export function FeatureSearch({
113139
function Reset({
114140
setEnabledOverride,
115141
featureKey,
142+
...props
116143
}: {
117144
setEnabledOverride: (key: string, value: boolean | null) => void;
118145
featureKey: string;
119-
}) {
146+
} & h.JSX.HTMLAttributes<HTMLAnchorElement>) {
120147
return (
121148
<a
122149
class="reset"
@@ -125,6 +152,7 @@ function Reset({
125152
e.preventDefault();
126153
setEnabledOverride(featureKey, null);
127154
}}
155+
{...props}
128156
>
129157
reset
130158
</a>

packages/browser-sdk/src/toolbar/Switch.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
.switch {
22
cursor: pointer;
3+
position: relative;
4+
}
5+
6+
.switch-input {
7+
border: 0px;
8+
clip: rect(0px, 0px, 0px, 0px);
9+
height: 1px;
10+
width: 1px;
11+
margin: -1px;
12+
padding: 0px;
13+
overflow: hidden;
14+
white-space: nowrap;
15+
position: absolute;
16+
}
17+
18+
.switch-input:focus-visible + .switch-track {
19+
outline: none;
20+
box-shadow: 0 0 0 1px #fff;
321
}
422

523
.switch-track {
624
position: relative;
725
transition: background 0.1s ease;
826
background: #606476;
27+
border-radius: 999px;
928
}
1029

1130
.switch[data-enabled="true"] .switch-track {

packages/browser-sdk/src/toolbar/Switch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export function Switch({
1919
<label class="switch" data-enabled={checked}>
2020
<input
2121
checked={checked}
22+
class="switch-input"
2223
name="enabled"
23-
style={{ display: "none" }}
2424
type="checkbox"
2525
{...props}
2626
/>

0 commit comments

Comments
 (0)