Skip to content

Commit 04c59eb

Browse files
committed
fix(browser-sdk): flush identity updates immediately
1 parent 2b56992 commit 04c59eb

5 files changed

Lines changed: 143 additions & 7 deletions

File tree

‎.changeset/tough-flies-push.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@reflag/browser-sdk": patch
3+
---
4+
5+
Start flushing queued user and company updates immediately when `updateUser` or `updateCompany` changes identity context.

‎packages/browser-sdk/src/bulkQueue.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,13 @@ export class BulkQueue {
9292
this.schedule(this.flushDelayMs);
9393
}
9494

95-
async flush() {
95+
async flush(): Promise<void> {
9696
if (this.inFlightPromise) {
9797
await this.inFlightPromise;
98-
return;
98+
if (this.queue.length === 0) {
99+
return;
100+
}
101+
return this.flush();
99102
}
100103

101104
if (this.queue.length === 0) {

‎packages/browser-sdk/src/client.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,18 +908,26 @@ export class ReflagClient {
908908

909909
this.context = newContext;
910910

911+
let shouldFlushIdentityUpdate = false;
912+
911913
if (companyChanged) {
912914
void this.company();
915+
shouldFlushIdentityUpdate = true;
913916
}
914917

915918
if (userChanged) {
916919
void this.user();
920+
shouldFlushIdentityUpdate = true;
917921
// Update the automatic feedback user if the user ID has changed
918922
if (userIdChanged) {
919923
void this.updateAutoFeedbackUser(String(newContext.user!.id));
920924
}
921925
}
922926

927+
if (shouldFlushIdentityUpdate) {
928+
void this.bulkQueue?.flush();
929+
}
930+
923931
const shouldTrackLoading = this.state === "initialized";
924932
if (shouldTrackLoading) {
925933
this.setState("initializing");

‎packages/browser-sdk/test/bulkQueue.test.ts‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ describe("BulkQueue", () => {
271271
resolveSend?.(new Response("", { status: 200 }));
272272
});
273273

274-
it("requires a second flush to send pending events after an in-flight batch", async () => {
274+
it("flush waits for an in-flight batch and sends pending events", async () => {
275275
let resolveFirstSend: ((res: Response) => void) | undefined;
276276
const firstSend = new Promise<Response>((resolve) => {
277277
resolveFirstSend = resolve;
@@ -306,10 +306,6 @@ describe("BulkQueue", () => {
306306
await flushWhileInFlight;
307307

308308
expect(waitedForInFlight).toBe(true);
309-
expect(sendBulk).toHaveBeenCalledTimes(1);
310-
expect(await queue.size()).toBe(2);
311-
312-
await queue.flush();
313309
expect(sendBulk).toHaveBeenCalledTimes(2);
314310
expect(sendBulk).toHaveBeenNthCalledWith(2, [trackEvent, lateTrackEvent]);
315311
expect(await queue.size()).toBe(0);

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

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { http, HttpResponse } from "msw";
12
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23

34
import { ReflagClient } from "../src/client";
45
import { FlagsClient } from "../src/flag/flags";
56
import { HttpClient } from "../src/httpClient";
67
import { flagsResult } from "./mocks/handlers";
8+
import { server } from "./mocks/server";
79

810
describe("ReflagClient", () => {
911
let client: ReflagClient;
@@ -71,6 +73,84 @@ describe("ReflagClient", () => {
7173
"No company Id provided in context, company will be ignored",
7274
);
7375
});
76+
77+
it("starts flushing the user update before refetching flags without waiting for it", async () => {
78+
const requests: string[] = [];
79+
let bulkResolved = false;
80+
let resolveBulk: ((response: HttpResponse) => void) | undefined;
81+
const bulkResponse = new Promise<HttpResponse>((resolve) => {
82+
resolveBulk = (response) => {
83+
bulkResolved = true;
84+
resolve(response);
85+
};
86+
});
87+
88+
server.use(
89+
http.post("https://front.reflag.com/bulk", async ({ request }) => {
90+
requests.push("bulk");
91+
const data = await request.json();
92+
const userEvent = Array.isArray(data)
93+
? data.find((event) => event?.type === "user")
94+
: undefined;
95+
expect(userEvent?.attributes?.siteCentricOptIn).toBe("true");
96+
97+
return bulkResponse;
98+
}),
99+
http.get(
100+
"https://front.reflag.com/features/evaluated",
101+
({ request }) => {
102+
requests.push("flags");
103+
const url = new URL(request.url);
104+
expect(url.searchParams.get("context.user.siteCentricOptIn")).toBe(
105+
"true",
106+
);
107+
108+
return HttpResponse.json({
109+
success: true,
110+
features: {
111+
SITE_CENTRIC: {
112+
key: "SITE_CENTRIC",
113+
isEnabled: true,
114+
targetingVersion: 1,
115+
},
116+
},
117+
});
118+
},
119+
),
120+
);
121+
122+
client = new ReflagClient({
123+
publishableKey: "test-key-user-update-before-flags",
124+
user: { id: "user1" },
125+
bootstrappedFlags: {
126+
SITE_CENTRIC: {
127+
key: "SITE_CENTRIC",
128+
isEnabled: false,
129+
targetingVersion: 1,
130+
},
131+
},
132+
trackingQueue: {
133+
flushDelayMs: 2_000,
134+
},
135+
});
136+
await client.initialize();
137+
138+
const updatePromise = client.updateUser({ siteCentricOptIn: "true" });
139+
140+
try {
141+
await vi.waitFor(() => expect(requests).toEqual(["bulk", "flags"]));
142+
143+
let updateResolved = false;
144+
void updatePromise.then(() => {
145+
updateResolved = true;
146+
});
147+
await vi.waitFor(() => expect(updateResolved).toBe(true));
148+
expect(bulkResolved).toBe(false);
149+
} finally {
150+
resolveBulk?.(HttpResponse.json({ success: true }));
151+
await updatePromise;
152+
}
153+
});
74154
});
75155

76156
describe("updateCompany", () => {
@@ -100,6 +180,50 @@ describe("ReflagClient", () => {
100180
);
101181
expect(flagClientSetContext).toHaveBeenCalledWith(client["context"]);
102182
});
183+
184+
it("starts flushing the company update before refetching flags", async () => {
185+
const requests: string[] = [];
186+
let resolveCompanyPlan: ((plan: unknown) => void) | undefined;
187+
const companyPlan = new Promise<unknown>((resolve) => {
188+
resolveCompanyPlan = resolve;
189+
});
190+
191+
server.use(
192+
http.post("https://front.reflag.com/bulk", async ({ request }) => {
193+
requests.push("bulk");
194+
const data = await request.json();
195+
const companyEvent = Array.isArray(data)
196+
? data.find((event) => event?.type === "company")
197+
: undefined;
198+
resolveCompanyPlan?.(companyEvent?.attributes?.plan);
199+
200+
return HttpResponse.json({ success: true });
201+
}),
202+
http.get("https://front.reflag.com/features/evaluated", () => {
203+
requests.push("flags");
204+
return HttpResponse.json({
205+
success: true,
206+
features: {},
207+
});
208+
}),
209+
);
210+
211+
client = new ReflagClient({
212+
publishableKey: "test-key-company-update-before-flags",
213+
user: { id: "user1" },
214+
company: { id: "company1" },
215+
bootstrappedFlags: {},
216+
trackingQueue: {
217+
flushDelayMs: 2_000,
218+
},
219+
});
220+
await client.initialize();
221+
222+
await client.updateCompany({ plan: "enterprise" });
223+
224+
expect(requests).toEqual(["bulk", "flags"]);
225+
await expect(companyPlan).resolves.toBe("enterprise");
226+
});
103227
});
104228

105229
describe("getFlag", () => {

0 commit comments

Comments
 (0)