Skip to content

Commit 445631d

Browse files
authored
feat: add ability to bootstrap flags on server and populate clients (#475)
This pull request introduces support for bootstrapping the Reflag browser SDK with pre-fetched flag data, enabling faster initialization for server-side rendered applications. It also refactors context management for improved efficiency, adds SDK lifecycle methods, and updates documentation and tooling. The most important changes are grouped below. **Bootstrapping and SSR support:** * Added support for initializing the SDK with `bootstrappedFlags`, allowing applications to avoid the initial network request for flags and improve performance in server-side rendering scenarios. This includes updates to `InitOptions`, `Config`, and initialization logic in `ReflagClient`. [[1]](diffhunk://#diff-f52188a93c3d2bacd6ba3cbc89a0c690673861c8848fae253abcb2ea55aa7acdL193-L215) [[2]](diffhunk://#diff-f52188a93c3d2bacd6ba3cbc89a0c690673861c8848fae253abcb2ea55aa7acdR290-R294) [[3]](diffhunk://#diff-f52188a93c3d2bacd6ba3cbc89a0c690673861c8848fae253abcb2ea55aa7acdR408-R409) [[4]](diffhunk://#diff-f52188a93c3d2bacd6ba3cbc89a0c690673861c8848fae253abcb2ea55aa7acdR498-R499) [[5]](diffhunk://#diff-b1fb2a339ad0237ec791b1b15cdeeb37155b030813262be5e9f32487f1c6f988R51-R73) * Updated the documentation (`README.md`) with clear instructions and examples for using bootstrapped flags in SSR setups. **Context and state management improvements:** * Refactored context update methods (`updateUser`, `updateCompany`, `updateOtherContext`, `updateContext`) to use deep equality checks and avoid unnecessary updates, improving efficiency and reducing redundant network calls. * Added new methods to get and update context (`getContext`, `updateContext`) and to update flags directly (`updateFlags`). [[1]](diffhunk://#diff-f52188a93c3d2bacd6ba3cbc89a0c690673861c8848fae253abcb2ea55aa7acdR571-R577) [[2]](diffhunk://#diff-f52188a93c3d2bacd6ba3cbc89a0c690673861c8848fae253abcb2ea55aa7acdL555-R701) **SDK lifecycle management:** * Introduced SDK state management with internal state tracking (`idle`, `initializing`, `initialized`, `stopped`) and corresponding methods (`initialize`, `stop`, `getState`). [[1]](diffhunk://#diff-f52188a93c3d2bacd6ba3cbc89a0c690673861c8848fae253abcb2ea55aa7acdR483-R488) [[2]](diffhunk://#diff-f52188a93c3d2bacd6ba3cbc89a0c690673861c8848fae253abcb2ea55aa7acdR511-R539) **Tooling and dependencies:** * Added `fast-equals` dependency for deep equality checks and updated scripts in `package.json` for improved test and build workflows. [[1]](diffhunk://#diff-475fbf96532457ba4b9457941110adacea0f854ef45e19448218936a2086e9dbR42) [[2]](diffhunk://#diff-475fbf96532457ba4b9457941110adacea0f854ef45e19448218936a2086e9dbL16-R25) * Updated `.vscode/settings.json` for consistent formatting and improved search excludes. [[1]](diffhunk://#diff-a5de3e5871ffcc383a2294845bd3df25d3eeff6c29ad46e3a396577c413bf357R23-R25) [[2]](diffhunk://#diff-a5de3e5871ffcc383a2294845bd3df25d3eeff6c29ad46e3a396577c413bf357R38) **Examples and documentation:** * Updated example code and HTML to demonstrate bootstrapped flag usage and improved flag visibility logic. [[1]](diffhunk://#diff-b1fb2a339ad0237ec791b1b15cdeeb37155b030813262be5e9f32487f1c6f988R18) [[2]](diffhunk://#diff-b1fb2a339ad0237ec791b1b15cdeeb37155b030813262be5e9f32487f1c6f988R51-R73) [[3]](diffhunk://#diff-b1fb2a339ad0237ec791b1b15cdeeb37155b030813262be5e9f32487f1c6f988L64-R83) [[4]](diffhunk://#diff-33e97e534680a9ce8684a75ac72a7e4a3242bc3909d119a4ea7c64b81ee38901L1-R1) These changes collectively enhance SDK performance, developer experience, and maintainability.
1 parent c4030e2 commit 445631d

Some content is hidden

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

83 files changed

+4629
-1976
lines changed

.github/workflows/package-ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ jobs:
1313
runs-on: ubuntu-22.04
1414
steps:
1515
- name: Checkout source code
16-
uses: actions/checkout@v3
16+
uses: actions/checkout@v4
17+
- name: Enable corepack
18+
run: corepack enable
1719
- name: Use Node.js
18-
uses: actions/setup-node@v3
20+
uses: actions/setup-node@v4
1921
with:
2022
node-version-file: ".nvmrc"
2123
cache: "yarn"

.github/workflows/publish.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ jobs:
99
release:
1010
runs-on: ubuntu-latest
1111
steps:
12-
- uses: actions/checkout@v3
12+
- uses: actions/checkout@v4
1313
# Setup .npmrc file to publish to npm
14-
- uses: actions/setup-node@v3
14+
- name: Enable corepack
15+
run: corepack enable
16+
- uses: actions/setup-node@v4
1517
with:
1618
node-version-file: ".nvmrc"
1719
cache: "yarn"

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"[typescriptreact]": {
2121
"editor.defaultFormatter": "esbenp.prettier-vscode"
2222
},
23+
"[vue]": {
24+
"editor.defaultFormatter": "esbenp.prettier-vscode"
25+
},
2326
"[yaml]": {
2427
"editor.defaultFormatter": "esbenp.prettier-vscode"
2528
},
@@ -32,6 +35,7 @@
3235
"**/node_modules": true
3336
},
3437
"search.exclude": {
38+
"**/.next": true,
3539
"**/build": true,
3640
"**/dist": true,
3741
"**/coverage": true,

.yarn/releases/yarn-4.1.1.cjs

Lines changed: 0 additions & 893 deletions
This file was deleted.

.yarnrc.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
nodeLinker: node-modules
2-
3-
yarnPath: .yarn/releases/yarn-4.1.1.cjs

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"version": "lerna version --exact --no-push",
2222
"docs": "./docs.sh"
2323
},
24-
"packageManager": "yarn@4.1.1",
24+
"packageManager": "yarn@4.10.3",
2525
"devDependencies": {
2626
"lerna": "^8.1.3",
2727
"prettier": "^3.5.2",

packages/browser-sdk/README.md

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,71 @@ const flags = reflagClient.getFlags();
227227
Just as `isEnabled`, accessing `config` on the object returned by `getFlags` does not automatically
228228
generate a `check` event, contrary to the `config` property on the object returned by `getFlag`.
229229

230-
## Updating user/company/other context
230+
## Server-side rendering and bootstrapping
231+
232+
For server-side rendered applications, you can eliminate the initial network request by bootstrapping the client with pre-fetched flag data.
233+
234+
### Init options bootstrapped
235+
236+
```typescript
237+
type Configuration = {
238+
logger: console; // by default only logs warn/error, by passing `console` you'll log everything
239+
apiBaseUrl?: "https://front.reflag.com";
240+
sseBaseUrl?: "https://livemessaging.bucket.co";
241+
feedback?: undefined; // See FEEDBACK.md
242+
enableTracking?: true; // set to `false` to stop sending track events and user/company updates to Reflag servers. Useful when you're impersonating a user
243+
offline?: boolean; // Use the SDK in offline mode. Offline mode is useful during testing and local development
244+
bootstrappedFlags?: FetchedFlags; // Pre-fetched flags from server-side (see Server-side rendering section)
245+
};
246+
```
247+
248+
### Using bootstrappedFlags
249+
250+
Use the Node SDK's `getFlagsForBootstrap()` method to pre-fetch flags server-side, then pass them to the browser client:
251+
252+
```typescript
253+
// Server-side: Get flags using Node SDK
254+
import { ReflagClient as ReflagNodeClient } from "@reflag/node-sdk";
255+
256+
const serverClient = new ReflagNodeClient({ secretKey: "your-secret-key" });
257+
await serverClient.initialize();
258+
259+
const { flags } = serverClient.getFlagsForBootstrap({
260+
user: { id: "user123", name: "John Doe", email: "[email protected]" },
261+
company: { id: "company456", name: "Acme Inc", plan: "enterprise" },
262+
});
263+
264+
// Pass flags data to client using your framework's preferred method
265+
// or for example in a script tag
266+
app.get("/", (req, res) => {
267+
res.set("Content-Type", "text/html");
268+
res.send(
269+
Buffer.from(
270+
`<script>var flags = ${JSON.stringify(flags)};</script>
271+
<main id="app"></main>`,
272+
),
273+
);
274+
});
275+
276+
// Client-side: Initialize with pre-fetched flags
277+
import { ReflagClient } from "@reflag/browser-sdk";
278+
279+
const reflagClient = new ReflagClient({
280+
publishableKey: "your-publishable-key",
281+
user: { id: "user123", name: "John Doe", email: "[email protected]" },
282+
company: { id: "company456", name: "Acme Inc", plan: "enterprise" },
283+
bootstrappedFlags: flags, // No network request needed
284+
});
285+
286+
await reflagClient.initialize(); // Initializes all but flags
287+
const { isEnabled } = reflagClient.getFlag("huddle");
288+
```
289+
290+
This eliminates loading states and improves performance by avoiding the initial flags API call.
291+
292+
## Context management
293+
294+
### Updating user/company/other context
231295

232296
Attributes given for the user/company/other context in the ReflagClient constructor can be updated for use in flag targeting evaluation with the `updateUser()`, `updateCompany()` and `updateOtherContext()` methods.
233297
They return a promise which resolves once the flags have been re-evaluated follow the update of the attributes.
@@ -244,6 +308,57 @@ await reflagClient.updateUser({ voiceHuddleOptIn: (!isEnabled).toString() });
244308

245309
> [!NOTE] > `user`/`company` attributes are also stored remotely on the Reflag servers and will automatically be used to evaluate flag targeting if the page is refreshed.
246310
311+
### setContext()
312+
313+
The `setContext()` method allows you to replace the entire context (user, company, and other attributes) at once. This method is useful when you need to completely change the context, such as when a user logs in or switches between different accounts.
314+
315+
```ts
316+
await reflagClient.setContext({
317+
user: {
318+
id: "new-user-123",
319+
name: "Jane Doe",
320+
321+
role: "admin",
322+
},
323+
company: {
324+
id: "company-456",
325+
name: "New Company Inc",
326+
plan: "enterprise",
327+
},
328+
other: {
329+
feature: "beta",
330+
locale: "en-US",
331+
},
332+
});
333+
```
334+
335+
The method will:
336+
337+
- Replace the entire context with the new values
338+
- Re-evaluate all flags based on the new context
339+
- Update the user and company information on Reflag servers
340+
- Return a promise that resolves once the flags have been re-evaluated
341+
342+
### getContext()
343+
344+
The `getContext()` method returns the current context being used for flag evaluation. This is useful for debugging or when you need to inspect the current user, company, and other attributes.
345+
346+
```ts
347+
const currentContext = reflagClient.getContext();
348+
console.log(currentContext);
349+
// {
350+
// user: { id: "user-123", name: "John Doe", email: "[email protected]" },
351+
// company: { id: "company-456", name: "Acme Inc", plan: "enterprise" },
352+
// other: { locale: "en-US", feature: "beta" }
353+
// }
354+
```
355+
356+
The returned context object contains:
357+
358+
- `user`: Current user attributes (if any)
359+
- `company`: Current company attributes (if any)
360+
- `other`: Additional context attributes not related to user or company
361+
247362
## Toolbar
248363

249364
The Reflag Toolbar is great for toggling flags on/off for yourself to ensure that everything works both when a flag is on and when it's off.

packages/browser-sdk/example/typescript/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReflagClient, CheckEvent, RawFlags } from "../../src";
1+
import { ReflagClient, RawFlags } from "../../src";
22

33
const urlParams = new URLSearchParams(window?.location?.search);
44
const publishableKey = urlParams.get("publishableKey");

packages/browser-sdk/index.html

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
const urlParams = new URLSearchParams(window.location.search);
1616
const publishableKey = urlParams.get("publishableKey");
1717
const flagKey = urlParams.get("flagKey") ?? "huddles";
18+
const isBootstrapped = urlParams.get("bootstrapped") === "true";
1819
</script>
1920
<style>
2021
body {
@@ -47,11 +48,29 @@
4748
placement: "bottom-right",
4849
},
4950
},
51+
bootstrappedFlags: isBootstrapped
52+
? {
53+
[flagKey]: {
54+
key: flagKey,
55+
isEnabled: true,
56+
},
57+
}
58+
: undefined,
5059
});
5160

61+
function setVisibility(isVisible) {
62+
const startHuddleElem = document.getElementById("start-huddle");
63+
if (startHuddleElem)
64+
startHuddleElem.style.display = isVisible ? "block" : "none";
65+
}
66+
5267
reflag.initialize().then(() => {
5368
console.log("Reflag initialized");
5469
document.getElementById("loading").style.display = "none";
70+
if (isBootstrapped) {
71+
const flag = reflag.getFlag(flagKey);
72+
setVisibility(flag.isEnabled);
73+
}
5574
});
5675

5776
reflag.on("check", (check) =>
@@ -61,15 +80,7 @@
6180
reflag.on("flagsUpdated", (flags) => {
6281
console.log("Flags updated");
6382
const flag = reflag.getFlag(flagKey);
64-
65-
const startHuddleElem = document.getElementById("start-huddle");
66-
if (flag.isEnabled) {
67-
// show the start-huddle button
68-
if (startHuddleElem) startHuddleElem.style.display = "block";
69-
} else {
70-
// hide the start-huddle button
71-
if (startHuddleElem) startHuddleElem.style.display = "none";
72-
}
83+
setVisibility(flag.isEnabled);
7384
});
7485
</script>
7586
</body>

packages/browser-sdk/package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@reflag/browser-sdk",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"packageManager": "[email protected]",
55
"license": "MIT",
66
"repository": {
@@ -13,15 +13,16 @@
1313
"scripts": {
1414
"dev": "vite",
1515
"build": "tsc --project tsconfig.build.json && vite build",
16-
"test": "tsc --project tsconfig.json && vitest -c vitest.config.ts",
16+
"test": "vitest run",
17+
"test:watch": "vitest",
1718
"test:e2e": "yarn build && playwright test",
18-
"test:ci": "tsc --project tsconfig.json && vitest run -c vitest.config.ts --reporter=default --reporter=junit --outputFile=junit.xml && yarn test:e2e",
19-
"coverage": "vitest run --coverage",
19+
"test:ci": "yarn test --reporter=default --reporter=junit --outputFile=junit.xml && yarn test:e2e",
20+
"coverage": "yarn test --coverage",
2021
"lint": "eslint .",
2122
"lint:ci": "eslint --output-file eslint-report.json --format json .",
2223
"prettier": "prettier --check .",
2324
"format": "yarn lint --fix && yarn prettier --write",
24-
"preversion": "yarn lint && yarn prettier && yarn vitest run -c vitest.config.ts && yarn build"
25+
"preversion": "yarn lint && yarn prettier && yarn test && yarn build"
2526
},
2627
"files": [
2728
"dist"
@@ -37,7 +38,7 @@
3738
},
3839
"dependencies": {
3940
"@floating-ui/dom": "^1.6.8",
40-
"canonical-json": "^0.0.4",
41+
"fast-equals": "^5.2.2",
4142
"js-cookie": "^3.0.5",
4243
"preact": "^10.22.1"
4344
},

0 commit comments

Comments
 (0)