Skip to content

Commit cd2627a

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/packages/openfeature-browser-provider/example/next-14.2.25
2 parents 979cf73 + 3abb3a1 commit cd2627a

51 files changed

Lines changed: 14064 additions & 928 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Browser SDK for use in non-React web applications
2323
## Node.js SDK
2424

2525
Node.js SDK for use on the server side.
26+
Use this for Cloudflare Workers as well.
2627

2728
[Read the docs](packages/node-sdk/README.md)
2829

packages/browser-sdk/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ type Configuration = {
109109
staleWhileRevalidate?: boolean; // Revalidate in the background when cached features turn stale to avoid latency in the UI (default: false)
110110
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
111111
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
112+
offline?: boolean; // Use the SDK in offline mode. Offline mode is useful during testing and local development
112113
};
113114
```
114115

@@ -282,7 +283,7 @@ Feedback can be submitted to Bucket using the SDK:
282283

283284
```ts
284285
bucketClient.feedback({
285-
featureId: "my_feature_id", // String (required), copy from Feature feedback tab
286+
featureKey: "my-feature-key", // String (required), copy from Feature feedback tab
286287
score: 5, // Number: 1-5 (optional)
287288
comment: "Absolutely stellar work!", // String (optional)
288289
});
@@ -292,7 +293,7 @@ bucketClient.feedback({
292293

293294
If you are not using the Bucket Browser SDK, you can still submit feedback using the HTTP API.
294295

295-
See details in [Feedback HTTP API](https://docs.bucket.co/reference/http-tracking-api#feedback)
296+
See details in [Feedback HTTP API](https://docs.bucket.co/api/http-api#post-feedback)
296297

297298
## Tracking feature usage
298299

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.2.0",
3+
"version": "3.3.0",
44
"packageManager": "yarn@4.1.1",
55
"license": "MIT",
66
"repository": {

packages/browser-sdk/src/client.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ export interface Config {
165165
* Whether to enable tracking.
166166
*/
167167
enableTracking: boolean;
168+
169+
/**
170+
* Whether to enable offline mode.
171+
*/
172+
offline: boolean;
168173
}
169174

170175
/**
@@ -228,6 +233,11 @@ export type InitOptions = {
228233
*/
229234
appBaseUrl?: string;
230235

236+
/**
237+
* Whether to enable offline mode. Defaults to `false`.
238+
*/
239+
offline?: boolean;
240+
231241
/**
232242
* Feature keys for which `isEnabled` should fallback to true
233243
* if SDK fails to fetch features from Bucket servers. If a record
@@ -294,6 +304,7 @@ const defaultConfig: Config = {
294304
appBaseUrl: APP_BASE_URL,
295305
sseBaseUrl: SSE_REALTIME_BASE_URL,
296306
enableTracking: true,
307+
offline: false,
297308
};
298309

299310
/**
@@ -396,6 +407,7 @@ export class BucketClient {
396407
appBaseUrl: opts?.appBaseUrl ?? defaultConfig.appBaseUrl,
397408
sseBaseUrl: opts?.sseBaseUrl ?? defaultConfig.sseBaseUrl,
398409
enableTracking: opts?.enableTracking ?? defaultConfig.enableTracking,
410+
offline: opts?.offline ?? defaultConfig.offline,
399411
};
400412

401413
this.requestFeedbackOptions = {
@@ -423,10 +435,12 @@ export class BucketClient {
423435
staleTimeMs: opts.staleTimeMs,
424436
fallbackFeatures: opts.fallbackFeatures,
425437
timeoutMs: opts.timeoutMs,
438+
offline: this.config.offline,
426439
},
427440
);
428441

429442
if (
443+
!this.config.offline &&
430444
this.context?.user &&
431445
!isNode && // do not prompt on server-side
432446
opts?.feedback?.enableAutoFeedback !== false // default to on
@@ -470,6 +484,7 @@ export class BucketClient {
470484
* Must be called before calling other SDK methods.
471485
*/
472486
async initialize() {
487+
const start = Date.now();
473488
if (this.autoFeedback) {
474489
// do not block on automated feedback surveys initialization
475490
this.autoFeedbackInit = this.autoFeedback.initialize().catch((e) => {
@@ -489,6 +504,13 @@ export class BucketClient {
489504
this.logger.error("error sending company", e);
490505
});
491506
}
507+
508+
this.logger.info(
509+
"Bucket initialized in " +
510+
Math.round(Date.now() - start) +
511+
"ms" +
512+
(this.config.offline ? " (offline mode)" : ""),
513+
);
492514
}
493515

494516
/**
@@ -607,6 +629,10 @@ export class BucketClient {
607629
return;
608630
}
609631

632+
if (this.config.offline) {
633+
return;
634+
}
635+
610636
const payload: TrackedEvent = {
611637
userId: String(this.context.user.id),
612638
event: eventName,
@@ -634,9 +660,14 @@ export class BucketClient {
634660
* @returns The server response.
635661
*/
636662
async feedback(payload: Feedback) {
663+
if (this.config.offline) {
664+
return;
665+
}
666+
637667
const userId =
638668
payload.userId ||
639669
(this.context.user?.id ? String(this.context.user?.id) : undefined);
670+
640671
const companyId =
641672
payload.companyId ||
642673
(this.context.company?.id ? String(this.context.company?.id) : undefined);
@@ -833,6 +864,10 @@ export class BucketClient {
833864
return;
834865
}
835866

867+
if (this.config.offline) {
868+
return;
869+
}
870+
836871
const { id, ...attributes } = this.context.user;
837872
const payload: User = {
838873
userId: String(id),
@@ -863,6 +898,10 @@ export class BucketClient {
863898
return;
864899
}
865900

901+
if (this.config.offline) {
902+
return;
903+
}
904+
866905
const { id, ...attributes } = this.context.company;
867906
const payload: Company = {
868907
userId: String(this.context.user.id),

packages/browser-sdk/src/feature/features.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,14 @@ type Config = {
9797
fallbackFeatures: Record<string, FallbackFeatureOverride>;
9898
timeoutMs: number;
9999
staleWhileRevalidate: boolean;
100+
offline: boolean;
100101
};
101102

102103
export const DEFAULT_FEATURES_CONFIG: Config = {
103104
fallbackFeatures: {},
104105
timeoutMs: 5000,
105106
staleWhileRevalidate: false,
107+
offline: false,
106108
};
107109

108110
export function validateFeaturesResponse(response: any) {
@@ -235,6 +237,7 @@ export class FeaturesClient {
235237
expireTimeMs?: number;
236238
cache?: FeatureCache;
237239
rateLimiter?: RateLimiter;
240+
offline?: boolean;
238241
},
239242
) {
240243
this.fetchedFeatures = {};
@@ -265,7 +268,11 @@ export class FeaturesClient {
265268
fallbackFeatures = options?.fallbackFeatures ?? {};
266269
}
267270

268-
this.config = { ...DEFAULT_FEATURES_CONFIG, ...options, fallbackFeatures };
271+
this.config = {
272+
...DEFAULT_FEATURES_CONFIG,
273+
...options,
274+
fallbackFeatures,
275+
};
269276

270277
this.rateLimiter =
271278
options?.rateLimiter ??
@@ -456,6 +463,10 @@ export class FeaturesClient {
456463
}
457464

458465
private async maybeFetchFeatures(): Promise<FetchedFeatures | undefined> {
466+
if (this.config.offline) {
467+
return;
468+
}
469+
459470
const cacheKey = this.fetchParams().toString();
460471
const cachedItem = this.cache.get(cacheKey);
461472

packages/browser-sdk/test/client.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { featuresResult } from "./mocks/handlers";
99
describe("BucketClient", () => {
1010
let client: BucketClient;
1111
const httpClientPost = vi.spyOn(HttpClient.prototype as any, "post");
12+
const httpClientGet = vi.spyOn(HttpClient.prototype as any, "get");
1213

1314
const featureClientSetContext = vi.spyOn(
1415
FeaturesClient.prototype,
@@ -21,6 +22,8 @@ describe("BucketClient", () => {
2122
user: { id: "user1" },
2223
company: { id: "company1" },
2324
});
25+
26+
vi.clearAllMocks();
2427
});
2528

2629
describe("updateUser", () => {
@@ -161,4 +164,26 @@ describe("BucketClient", () => {
161164
expect(featuresUpdated).not.toHaveBeenCalled();
162165
});
163166
});
167+
168+
describe("offline mode", () => {
169+
it("should not make HTTP calls when offline", async () => {
170+
client = new BucketClient({
171+
publishableKey: "test-key",
172+
user: { id: "user1" },
173+
company: { id: "company1" },
174+
offline: true,
175+
feedback: { enableAutoFeedback: true },
176+
});
177+
178+
await client.initialize();
179+
await client.track("offline-event");
180+
await client.feedback({ featureKey: "featureA", score: 5 });
181+
await client.updateUser({ name: "New User" });
182+
await client.updateCompany({ name: "New Company" });
183+
await client.stop();
184+
185+
expect(httpClientPost).not.toHaveBeenCalled();
186+
expect(httpClientGet).not.toHaveBeenCalled();
187+
});
188+
});
164189
});

packages/flag-evaluation/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bucketco/flag-evaluation",
3-
"version": "0.1.4",
3+
"version": "0.1.5",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

packages/flag-evaluation/src/index.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -203,25 +203,43 @@ export interface Rule<T extends RuleValue> {
203203
* @return {Record<string, string>} A flattened JSON object with "stringified" keys and values.
204204
*/
205205
export function flattenJSON(data: object): Record<string, string> {
206-
if (Object.keys(data).length === 0) return {};
207-
const result: Record<string, any> = {};
208-
function recurse(cur: any, prop: string) {
209-
if (Object(cur) !== cur) {
210-
result[prop] = cur;
211-
} else if (Array.isArray(cur)) {
212-
const l = cur.length;
213-
for (let i = 0; i < l; i++)
214-
recurse(cur[i], prop ? prop + "." + i : "" + i);
215-
if (l == 0) result[prop] = [];
206+
const result: Record<string, string> = {};
207+
208+
if (Object.keys(data).length === 0) {
209+
return result;
210+
}
211+
212+
function recurse(value: any, prop: string) {
213+
if (value === undefined) {
214+
return;
215+
}
216+
217+
if (value === null) {
218+
result[prop] = "";
219+
} else if (typeof value !== "object") {
220+
result[prop] = String(value);
221+
} else if (Array.isArray(value)) {
222+
if (value.length === 0) {
223+
result[prop] = "";
224+
}
225+
226+
for (let i = 0; i < value.length; i++) {
227+
recurse(value[i], prop ? prop + "." + i : "" + i);
228+
}
216229
} else {
217230
let isEmpty = true;
218-
for (const p in cur) {
231+
232+
for (const p in value) {
219233
isEmpty = false;
220-
recurse(cur[p], prop ? prop + "." + p : p);
234+
recurse(value[p], prop ? prop + "." + p : p);
235+
}
236+
237+
if (isEmpty) {
238+
result[prop] = "";
221239
}
222-
if (isEmpty) result[prop] = {};
223240
}
224241
}
242+
225243
recurse(data, "");
226244
return result;
227245
}
@@ -325,9 +343,9 @@ export function evaluate(
325343
: fieldValueDate < daysAgo.getTime();
326344
}
327345
case "SET":
328-
return fieldValue != "";
346+
return fieldValue !== "";
329347
case "NOT_SET":
330-
return fieldValue == "";
348+
return fieldValue === "";
331349
case "IS":
332350
return fieldValue === value;
333351
case "IS_NOT":
@@ -357,13 +375,17 @@ function evaluateRecursively(
357375
case "constant":
358376
return filter.value;
359377
case "context":
360-
if (!(filter.field in context)) {
378+
if (
379+
!(filter.field in context) &&
380+
filter.operator !== "SET" &&
381+
filter.operator !== "NOT_SET"
382+
) {
361383
missingContextFieldsSet.add(filter.field);
362384
return false;
363385
}
364386

365387
return evaluate(
366-
context[filter.field],
388+
context[filter.field] ?? "",
367389
filter.operator,
368390
filter.values || [],
369391
filter.valueSet,

0 commit comments

Comments
 (0)