Skip to content

Commit be46e7c

Browse files
authored
feat: added offline mode to aid in testing (#444)
This PR adds an optional `offline` mode to the browser SDK to aid with testing where contacting the servers is not needed.
1 parent 3db6943 commit be46e7c

File tree

3 files changed

+76
-1
lines changed

3 files changed

+76
-1
lines changed

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
});

0 commit comments

Comments
 (0)