This is page 2 of 7. Use http://codebase.md/bucketco/bucket-javascript-sdk?page={x} to view the full context.
# Directory Structure
```
├── .editorconfig
├── .gitattributes
├── .github
│ └── workflows
│ ├── package-ci.yml
│ └── publish.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── .yarnrc.yml
├── docs.sh
├── lerna.json
├── LICENSE
├── package.json
├── packages
│ ├── browser-sdk
│ │ ├── .prettierignore
│ │ ├── eslint.config.js
│ │ ├── example
│ │ │ ├── feedback
│ │ │ │ ├── feedback.html
│ │ │ │ └── Feedback.jsx
│ │ │ └── typescript
│ │ │ ├── app.ts
│ │ │ └── index.html
│ │ ├── FEEDBACK.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── playwright.config.ts
│ │ ├── postcss.config.js
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── client.ts
│ │ │ ├── config.ts
│ │ │ ├── context.ts
│ │ │ ├── feedback
│ │ │ │ ├── feedback.ts
│ │ │ │ ├── prompts.ts
│ │ │ │ ├── promptStorage.ts
│ │ │ │ └── ui
│ │ │ │ ├── Button.css
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── config
│ │ │ │ │ └── defaultTranslations.tsx
│ │ │ │ ├── css.d.ts
│ │ │ │ ├── FeedbackDialog.css
│ │ │ │ ├── FeedbackDialog.tsx
│ │ │ │ ├── FeedbackForm.css
│ │ │ │ ├── FeedbackForm.tsx
│ │ │ │ ├── hooks
│ │ │ │ │ └── useTimer.ts
│ │ │ │ ├── index.css
│ │ │ │ ├── index.ts
│ │ │ │ ├── Plug.tsx
│ │ │ │ ├── RadialProgress.css
│ │ │ │ ├── RadialProgress.tsx
│ │ │ │ ├── StarRating.css
│ │ │ │ ├── StarRating.tsx
│ │ │ │ └── types.ts
│ │ │ ├── flag
│ │ │ │ ├── flagCache.ts
│ │ │ │ └── flags.ts
│ │ │ ├── hooksManager.ts
│ │ │ ├── httpClient.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.ts
│ │ │ ├── rateLimiter.ts
│ │ │ ├── sse.ts
│ │ │ ├── toolbar
│ │ │ │ ├── Flags.css
│ │ │ │ ├── Flags.tsx
│ │ │ │ ├── index.css
│ │ │ │ ├── index.ts
│ │ │ │ ├── Switch.css
│ │ │ │ ├── Switch.tsx
│ │ │ │ ├── Toolbar.css
│ │ │ │ └── Toolbar.tsx
│ │ │ └── ui
│ │ │ ├── constants.ts
│ │ │ ├── Dialog.css
│ │ │ ├── Dialog.tsx
│ │ │ ├── icons
│ │ │ │ ├── Check.tsx
│ │ │ │ ├── CheckCircle.tsx
│ │ │ │ ├── Close.tsx
│ │ │ │ ├── Dissatisfied.tsx
│ │ │ │ ├── Logo.tsx
│ │ │ │ ├── Neutral.tsx
│ │ │ │ ├── Satisfied.tsx
│ │ │ │ ├── VeryDissatisfied.tsx
│ │ │ │ └── VerySatisfied.tsx
│ │ │ ├── packages
│ │ │ │ └── floating-ui-preact-dom
│ │ │ │ ├── arrow.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── README.md
│ │ │ │ ├── types.ts
│ │ │ │ ├── useFloating.ts
│ │ │ │ └── utils
│ │ │ │ ├── deepEqual.ts
│ │ │ │ ├── getDPR.ts
│ │ │ │ ├── roundByDPR.ts
│ │ │ │ └── useLatestRef.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── test
│ │ │ ├── client.test.ts
│ │ │ ├── e2e
│ │ │ │ ├── acceptance.browser.spec.ts
│ │ │ │ ├── empty.html
│ │ │ │ ├── feedback-widget.browser.spec.ts
│ │ │ │ └── give-feedback-button.html
│ │ │ ├── flagCache.test.ts
│ │ │ ├── flags.test.ts
│ │ │ ├── hooksManager.test.ts
│ │ │ ├── httpClient.test.ts
│ │ │ ├── init.test.ts
│ │ │ ├── mocks
│ │ │ │ ├── handlers.ts
│ │ │ │ └── server.ts
│ │ │ ├── prompts.test.ts
│ │ │ ├── promptStorage.test.ts
│ │ │ ├── rateLimiter.test.ts
│ │ │ ├── sse.test.ts
│ │ │ ├── testLogger.ts
│ │ │ └── usage.test.ts
│ │ ├── tsconfig.build.json
│ │ ├── tsconfig.eslint.json
│ │ ├── tsconfig.json
│ │ ├── typedoc.json
│ │ ├── vite.config.mjs
│ │ ├── vite.e2e.config.js
│ │ └── vitest.setup.ts
│ ├── cli
│ │ ├── .prettierignore
│ │ ├── commands
│ │ │ ├── apps.ts
│ │ │ ├── auth.ts
│ │ │ ├── flags.ts
│ │ │ ├── init.ts
│ │ │ ├── mcp.ts
│ │ │ ├── new.ts
│ │ │ └── rules.ts
│ │ ├── eslint.config.js
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── schema.json
│ │ ├── services
│ │ │ ├── bootstrap.ts
│ │ │ ├── flags.ts
│ │ │ ├── mcp.ts
│ │ │ └── rules.ts
│ │ ├── stores
│ │ │ ├── auth.ts
│ │ │ └── config.ts
│ │ ├── test
│ │ │ └── json.test.ts
│ │ ├── tsconfig.eslint.json
│ │ ├── tsconfig.json
│ │ ├── utils
│ │ │ ├── auth.ts
│ │ │ ├── commander.ts
│ │ │ ├── constants.ts
│ │ │ ├── errors.ts
│ │ │ ├── file.ts
│ │ │ ├── gen.ts
│ │ │ ├── json.ts
│ │ │ ├── options.ts
│ │ │ ├── schemas.ts
│ │ │ ├── types.ts
│ │ │ ├── urls.ts
│ │ │ └── version.ts
│ │ └── vite.config.js
│ ├── eslint-config
│ │ ├── base.js
│ │ └── package.json
│ ├── flag-evaluation
│ │ ├── .prettierignore
│ │ ├── eslint.config.js
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── src
│ │ │ └── index.ts
│ │ ├── test
│ │ │ └── index.test.ts
│ │ ├── tsconfig.build.json
│ │ ├── tsconfig.eslint.json
│ │ └── tsconfig.json
│ ├── node-sdk
│ │ ├── .prettierignore
│ │ ├── docs
│ │ │ ├── type-check-failed.png
│ │ │ └── type-check-payload-failed.png
│ │ ├── eslint.config.js
│ │ ├── examples
│ │ │ ├── cloudflare-worker
│ │ │ │ ├── .gitignore
│ │ │ │ ├── .prettierignore
│ │ │ │ ├── .vscode
│ │ │ │ │ └── settings.json
│ │ │ │ ├── package.json
│ │ │ │ ├── README.md
│ │ │ │ ├── src
│ │ │ │ │ └── index.ts
│ │ │ │ ├── tsconfig.json
│ │ │ │ ├── vitest.config.mts
│ │ │ │ ├── worker-configuration.d.ts
│ │ │ │ ├── wrangler.jsonc
│ │ │ │ └── yarn.lock
│ │ │ └── express
│ │ │ ├── app.test.ts
│ │ │ ├── app.ts
│ │ │ ├── bucket.ts
│ │ │ ├── bucketConfig.json
│ │ │ ├── package.json
│ │ │ ├── README.md
│ │ │ ├── serve.ts
│ │ │ ├── tsconfig.json
│ │ │ └── yarn.lock
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── batch-buffer.ts
│ │ │ ├── client.ts
│ │ │ ├── config.ts
│ │ │ ├── edgeClient.ts
│ │ │ ├── fetch-http-client.ts
│ │ │ ├── flusher.ts
│ │ │ ├── index.ts
│ │ │ ├── inRequestCache.ts
│ │ │ ├── periodicallyUpdatingCache.ts
│ │ │ ├── rate-limiter.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── test
│ │ │ ├── batch-buffer.test.ts
│ │ │ ├── client.test.ts
│ │ │ ├── config.test.ts
│ │ │ ├── fetch-http-client.test.ts
│ │ │ ├── flusher.test.ts
│ │ │ ├── inRequestCache.test.ts
│ │ │ ├── periodicallyUpdatingCache.test.ts
│ │ │ ├── rate-limiter.test.ts
│ │ │ ├── testConfig.json
│ │ │ └── utils.test.ts
│ │ ├── tsconfig.build.json
│ │ ├── tsconfig.eslint.json
│ │ ├── tsconfig.json
│ │ ├── typedoc.json
│ │ └── vite.config.js
│ ├── openfeature-browser-provider
│ │ ├── .prettierignore
│ │ ├── eslint.config.js
│ │ ├── example
│ │ │ ├── .eslintrc.json
│ │ │ ├── .gitignore
│ │ │ ├── app
│ │ │ │ ├── featureManagement.ts
│ │ │ │ ├── globals.css
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── components
│ │ │ │ ├── Context.tsx
│ │ │ │ ├── HuddleFeature.tsx
│ │ │ │ └── OpenFeatureProvider.tsx
│ │ │ ├── next.config.mjs
│ │ │ ├── package.json
│ │ │ ├── postcss.config.mjs
│ │ │ ├── README.md
│ │ │ ├── tailwind.config.ts
│ │ │ └── tsconfig.json
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── index.test.ts
│ │ │ └── index.ts
│ │ ├── tsconfig.build.json
│ │ ├── tsconfig.eslint.json
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ ├── openfeature-node-provider
│ │ ├── .prettierignore
│ │ ├── eslint.config.js
│ │ ├── example
│ │ │ ├── app.ts
│ │ │ ├── package.json
│ │ │ ├── README.md
│ │ │ ├── reflag.ts
│ │ │ ├── serve.ts
│ │ │ ├── tsconfig.json
│ │ │ └── yarn.lock
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── index.test.ts
│ │ │ └── index.ts
│ │ ├── tsconfig.build.json
│ │ ├── tsconfig.eslint.json
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ ├── react-sdk
│ │ ├── .prettierignore
│ │ ├── dev
│ │ │ ├── .env
│ │ │ ├── nextjs-bootstrap-demo
│ │ │ │ ├── .eslintrc.json
│ │ │ │ ├── .gitignore
│ │ │ │ ├── app
│ │ │ │ │ ├── client.ts
│ │ │ │ │ ├── favicon.ico
│ │ │ │ │ ├── globals.css
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── components
│ │ │ │ │ └── Flags.tsx
│ │ │ │ ├── next.config.mjs
│ │ │ │ ├── package.json
│ │ │ │ ├── postcss.config.mjs
│ │ │ │ ├── public
│ │ │ │ │ ├── next.svg
│ │ │ │ │ └── vercel.svg
│ │ │ │ ├── README.md
│ │ │ │ ├── tailwind.config.ts
│ │ │ │ └── tsconfig.json
│ │ │ ├── nextjs-flag-demo
│ │ │ │ ├── .eslintrc.json
│ │ │ │ ├── .gitignore
│ │ │ │ ├── app
│ │ │ │ │ ├── favicon.ico
│ │ │ │ │ ├── globals.css
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── components
│ │ │ │ │ ├── Flags.tsx
│ │ │ │ │ └── Providers.tsx
│ │ │ │ ├── next.config.mjs
│ │ │ │ ├── package.json
│ │ │ │ ├── postcss.config.mjs
│ │ │ │ ├── public
│ │ │ │ │ ├── next.svg
│ │ │ │ │ └── vercel.svg
│ │ │ │ ├── README.md
│ │ │ │ ├── tailwind.config.ts
│ │ │ │ └── tsconfig.json
│ │ │ └── plain
│ │ │ ├── app.tsx
│ │ │ ├── index.html
│ │ │ ├── index.tsx
│ │ │ ├── tsconfig.json
│ │ │ └── vite-env.d.ts
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ └── index.tsx
│ │ ├── test
│ │ │ └── usage.test.tsx
│ │ ├── tsconfig.build.json
│ │ ├── tsconfig.eslint.json
│ │ ├── tsconfig.json
│ │ ├── typedoc.json
│ │ └── vite.config.mjs
│ ├── tsconfig
│ │ ├── library.json
│ │ └── package.json
│ └── vue-sdk
│ ├── .prettierignore
│ ├── dev
│ │ └── plain
│ │ ├── App.vue
│ │ ├── components
│ │ │ ├── Events.vue
│ │ │ ├── FlagsList.vue
│ │ │ ├── MissingKeyMessage.vue
│ │ │ ├── RequestFeedback.vue
│ │ │ ├── Section.vue
│ │ │ ├── StartHuddlesButton.vue
│ │ │ └── Track.vue
│ │ ├── env.d.ts
│ │ ├── index.html
│ │ └── index.ts
│ ├── eslint.config.js
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── hooks.ts
│ │ ├── index.ts
│ │ ├── ReflagBootstrappedProvider.vue
│ │ ├── ReflagClientProvider.vue
│ │ ├── ReflagProvider.vue
│ │ ├── types.ts
│ │ ├── version.ts
│ │ └── vue.d.ts
│ ├── test
│ │ └── usage.test.ts
│ ├── tsconfig.build.json
│ ├── tsconfig.eslint.json
│ ├── tsconfig.json
│ ├── typedoc.json
│ └── vite.config.mjs
├── README.md
├── typedoc.json
├── vitest.workspace.js
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/packages/cli/utils/file.ts:
--------------------------------------------------------------------------------
```typescript
import { access, constants } from "node:fs/promises";
import os from "node:os";
import { join } from "node:path";
/**
* Checks if a file exists at the given path.
* @param path The path to the file.
* @returns True if the file exists, false otherwise.
*/
export async function fileExists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
// Helper to resolve home directory
export const resolvePath = (p: string) => {
return join(
...p.split("/").map((part) => {
if (part === "~") {
return os.homedir();
} else if (part === "@") {
return process.env.APPDATA ?? "";
} else {
return part;
}
}),
);
};
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/config/defaultTranslations.tsx:
--------------------------------------------------------------------------------
```typescript
import { FeedbackTranslations } from "../types";
/**
* {@includeCode ./defaultTranslations.tsx}
*/
export const DEFAULT_TRANSLATIONS: FeedbackTranslations = {
DefaultQuestionLabel: "How satisfied are you with this feature?",
QuestionPlaceholder: "Write a comment",
ScoreStatusDescription: "Pick a score and leave a comment",
ScoreStatusLoading: "Saving score, please wait...",
ScoreStatusReceived: "Score has been received!",
ScoreVeryDissatisfiedLabel: "Very dissatisfied (1/5)",
ScoreDissatisfiedLabel: "Dissatisfied (2/5)",
ScoreNeutralLabel: "Neutral (3/5)",
ScoreSatisfiedLabel: "Satisfied (4/5)",
ScoreVerySatisfiedLabel: "Very satisfied (5/5)",
SuccessMessage: "Feedback received, thank you!",
SendButton: "Send feedback",
};
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/rateLimiter.ts:
--------------------------------------------------------------------------------
```typescript
import { Logger } from "./logger";
const oneMinute = 60 * 1000;
export default class RateLimiter {
private eventsByKey: Record<string, number[]> = {};
constructor(
private eventsPerMinute: number,
private logger: Logger,
) {}
public rateLimited<R>(key: string, func: () => R): R | undefined {
const now = Date.now();
if (!this.eventsByKey[key]) {
this.eventsByKey[key] = [];
}
const events = this.eventsByKey[key];
while (events.length && now - events[0] > oneMinute) {
events.shift();
}
const limitExceeded = events.length >= this.eventsPerMinute;
if (limitExceeded) {
this.logger.debug("Rate limit exceeded", { key });
return;
}
events.push(now);
return func();
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/types.ts:
--------------------------------------------------------------------------------
```typescript
import { Placement } from "./packages/floating-ui-preact-dom/types";
export type DialogPlacement =
| "bottom-right"
| "bottom-left"
| "top-right"
| "top-left";
export type PopoverPlacement = Placement;
export type Offset = {
/**
* Offset from the nearest horizontal screen edge after placement is resolved
*/
x?: string | number;
/**
* Offset from the nearest vertical screen edge after placement is resolved
*/
y?: string | number;
};
export type Position =
| { type: "MODAL" }
| {
type: "DIALOG";
placement: DialogPlacement;
offset?: Offset;
}
| {
type: "POPOVER";
anchor: HTMLElement | null;
placement?: PopoverPlacement;
};
export interface ToolbarPosition {
placement: DialogPlacement;
offset?: Offset;
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/playwright.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig, devices } from "@playwright/test";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./test/e2e",
testMatch: "**/*.spec.?(c|m)[jt]s?(x)",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: "list",
use: {
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
],
webServer: {
// separate port to let the app run alongside the tracking sdk tests
command: "npx http-server . -p 8001",
timeout: 120 * 1000,
port: 8001,
},
});
```
--------------------------------------------------------------------------------
/packages/cli/utils/constants.ts:
--------------------------------------------------------------------------------
```typescript
import os from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
export const CLIENT_VERSION_HEADER_NAME = "reflag-sdk-version";
export const CLIENT_VERSION_HEADER_VALUE = (version: string) =>
`cli/${version}`;
export const CONFIG_FILE_NAME = "reflag.config.json";
export const AUTH_FILE = join(os.homedir(), ".reflag-auth");
export const SCHEMA_URL = `https://unpkg.com/@reflag/cli@latest/schema.json`;
export const DEFAULT_BASE_URL = "https://app.reflag.com";
export const DEFAULT_API_URL = `${DEFAULT_BASE_URL}/api`;
export const DEFAULT_TYPES_OUTPUT = join("gen", "flags.d.ts");
export const DEFAULT_AUTH_TIMEOUT = 60000; // 60 seconds
export const MODULE_ROOT = fileURLToPath(import.meta.url).substring(
0,
fileURLToPath(import.meta.url).lastIndexOf("cli") + 3,
);
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/toolbar/Switch.css:
--------------------------------------------------------------------------------
```css
.switch {
cursor: pointer;
position: relative;
}
.switch-input {
border: 0px;
clip: rect(0px, 0px, 0px, 0px);
height: 1px;
width: 1px;
margin: -1px;
padding: 0px;
overflow: hidden;
white-space: nowrap;
position: absolute;
}
.switch-track {
position: relative;
transition: background 0.1s ease;
background: var(--gray600);
border-radius: 999px;
transition: transform 0.1s ease-out;
}
.switch-input:focus-visible + .switch-track {
outline: 1px solid #fff;
outline-offset: 1px;
}
.switch[data-enabled="true"] .switch-track {
background: white;
}
.switch-dot {
background: white;
border-radius: 50%;
position: absolute;
top: 1px;
transition: transform 0.1s ease-out;
box-shadow: 0 0px 5px rgba(0, 0, 0, 0.2);
}
.switch[data-enabled="true"] .switch-dot {
background: var(--black);
}
```
--------------------------------------------------------------------------------
/packages/openfeature-browser-provider/vite.config.js:
--------------------------------------------------------------------------------
```javascript
import { resolve } from "path";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
test: {
environment: "jsdom",
exclude: [
"test/e2e/**",
"**/node_modules/**",
"**/dist/**",
"**/cypress/**",
"**/.{idea,git,cache,output,temp}/**",
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*",
],
},
plugins: [dts({ insertTypesEntry: true })],
build: {
exclude: ["**/node_modules/**", "test/e2e/**", "**/*.test.ts"],
sourcemap: true,
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, "src/index.ts"),
name: "ReflagOpenFeatureBrowserProvider",
// the proper extensions will be added
fileName: "reflag-openfeature-browser-provider",
},
},
});
```
--------------------------------------------------------------------------------
/packages/react-sdk/vite.config.mjs:
--------------------------------------------------------------------------------
```
import { resolve } from "path";
import preserveDirectives from "rollup-preserve-directives";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
test: {
root: __dirname,
environment: "jsdom",
},
optimizeDeps: {
include: ["@reflag/browser-sdk"],
},
plugins: [
dts({ insertTypesEntry: true, exclude: ["dev"] }),
preserveDirectives(),
],
build: {
exclude: ["**/node_modules/**", "test/e2e/**", "dev"],
sourcemap: true,
lib: {
entry: resolve(__dirname, "src/index.tsx"),
name: "ReflagReactSDK",
fileName: "reflag-react-sdk",
formats: ["es", "umd"],
},
rollupOptions: {
external: ["react", "react-dom"],
output: {
globals: {
react: "React",
},
},
},
},
server: {
open: "/dev/plain/index.html",
},
});
```
--------------------------------------------------------------------------------
/packages/node-sdk/src/edgeClient.ts:
--------------------------------------------------------------------------------
```typescript
import { ReflagClient } from "./client";
import { ClientOptions } from "./types";
export type EdgeClientOptions = Omit<
ClientOptions,
"cacheStrategy" | "flushIntervalMs" | "batchOptions"
>;
/**
* The EdgeClient is ReflagClient pre-configured to be used in edge runtimes, like
* Cloudflare Workers.
*
* @example
* ```ts
* // set the REFLAG_SECRET_KEY environment variable or pass the secret key in the constructor
* const client = new EdgeClient();
*
* // evaluate a flag
* const context = {
* user: { id: "user-id" },
* company: { id: "company-id" },
* }
* const { isEnabled } = client.getFlag(context, "flag-key");
*
* ```
*/
export class EdgeClient extends ReflagClient {
constructor(options: EdgeClientOptions = {}) {
const opts = {
...options,
cacheStrategy: "in-request" as const,
batchOptions: {
intervalMs: 0,
},
};
super(opts);
}
}
```
--------------------------------------------------------------------------------
/packages/vue-sdk/dev/plain/components/StartHuddlesButton.vue:
--------------------------------------------------------------------------------
```vue
<script setup lang="ts">
import { useFlag } from "../../../src";
import Section from "./Section.vue";
const { isLoading, isEnabled, config, requestFeedback, track } =
useFlag("huddles");
</script>
<template>
<Section title="Huddles">
<div style="display: flex; gap: 10px; flex-wrap: wrap">
<div>huddles enabled: {{ isEnabled }}</div>
<div v-if="isLoading">Loading...</div>
<div v-else style="display: flex; gap: 10px; flex-wrap: wrap">
<div>
<button @click="track()">
{{ config?.payload?.buttonTitle ?? "Start Huddles (track event)" }}
</button>
</div>
<div>
<button
@click="
(e) =>
requestFeedback({
title: 'Do you like huddles?',
})
"
>
Trigger survey
</button>
</div>
</div>
</div>
</Section>
</template>
```
--------------------------------------------------------------------------------
/packages/vue-sdk/vite.config.mjs:
--------------------------------------------------------------------------------
```
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";
import preserveDirectives from "rollup-preserve-directives";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
test: {
environment: "jsdom",
},
optimizeDeps: {
include: ["@reflag/browser-sdk"],
},
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler.js",
},
},
plugins: [
vue(),
dts({ insertTypesEntry: true, exclude: ["dev"] }),
preserveDirectives(),
],
build: {
exclude: ["**/node_modules/**", "test/e2e/**", "dev"],
sourcemap: true,
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "ReflagVueSDK",
fileName: "reflag-vue-sdk",
formats: ["es", "umd"],
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "Vue",
},
},
},
},
server: {
open: "/dev/plain/index.html",
},
});
```
--------------------------------------------------------------------------------
/packages/cli/commands/apps.ts:
--------------------------------------------------------------------------------
```typescript
import chalk from "chalk";
import { Command } from "commander";
import ora from "ora";
import { listApps } from "../services/bootstrap.js";
import { configStore } from "../stores/config.js";
import { handleError } from "../utils/errors.js";
export const listAppsAction = async () => {
const baseUrl = configStore.getConfig("baseUrl");
const spinner = ora(`Loading apps from ${chalk.cyan(baseUrl)}...`).start();
try {
const apps = listApps();
spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}.`);
console.table(apps.map(({ name, id, demo }) => ({ name, id, demo })));
} catch (error) {
spinner.fail("Failed to list apps.");
handleError(error, "Apps List");
}
};
export function registerAppCommands(cli: Command) {
const appsCommand = new Command("apps").description("Manage apps.");
appsCommand
.command("list")
.alias("ls")
.description("List all available apps.")
.action(listAppsAction);
cli.addCommand(appsCommand);
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "workspaces",
"version": "0.0.1",
"private": true,
"license": "MIT",
"workspaces": [
"packages/*",
"packages/react-sdk/dev/*",
"packages/openfeature-browser-provider/example"
],
"scripts": {
"dev": "lerna run dev --parallel",
"build": "lerna run build --stream",
"test:ci": "lerna run test:ci --stream",
"test": "lerna run test --stream",
"format": "lerna run format --stream",
"prettier": "lerna run prettier --stream",
"prettier:fix": "lerna run prettier -- --write",
"lint": "lerna run lint --stream",
"lint:ci": "lerna run lint:ci --stream",
"version": "lerna version --exact --no-push",
"docs": "./docs.sh"
},
"packageManager": "[email protected]",
"devDependencies": {
"lerna": "^8.1.3",
"prettier": "^3.5.2",
"typedoc": "0.27.6",
"typedoc-plugin-frontmatter": "^1.1.2",
"typedoc-plugin-markdown": "^4.4.2",
"typedoc-plugin-mdn-links": "^4.0.7",
"typescript": "^5.7.3"
}
}
```
--------------------------------------------------------------------------------
/packages/openfeature-browser-provider/example/app/featureManagement.ts:
--------------------------------------------------------------------------------
```typescript
"use client";
import { ReflagBrowserSDKProvider } from "@reflag/openfeature-browser-provider";
import { OpenFeature } from "@openfeature/react-sdk";
const publishableKey = process.env.NEXT_PUBLIC_REFLAG_PUBLISHABLE_KEY;
let reflagProvider: ReflagBrowserSDKProvider | null = null;
let initialized = false;
export async function initOpenFeature() {
if (initialized) {
return;
}
initialized = true;
if (!publishableKey) {
console.error("No publishable key set for Reflag");
return;
}
reflagProvider = new ReflagBrowserSDKProvider({
publishableKey,
fallbackFlags: {
huddle: {
key: "zoom", // huddleMeetingProvider
payload: {
joinUrl: "https://zoom.us/join",
},
},
},
});
return OpenFeature.setProviderAndWait(reflagProvider);
}
export function track(event: string, attributes?: { [key: string]: any }) {
console.log("Tracking event", event, attributes);
reflagProvider?.client?.track(event, attributes);
}
```
--------------------------------------------------------------------------------
/packages/cli/utils/urls.ts:
--------------------------------------------------------------------------------
```typescript
import chalk from "chalk";
import slugMod from "slug";
import { DEFAULT_BASE_URL } from "./constants.js";
export type UrlArgs = { id: string; name: string };
export function slug({ id, name }: UrlArgs) {
return `${slugMod(name).substring(0, 15)}-${id}`;
}
export function stripTrailingSlash<T extends string | undefined>(str: T): T {
return str?.endsWith("/") ? (str.slice(0, -1) as T) : str;
}
export const successUrl = (baseUrl: string) => `${baseUrl}/cli-login/success`;
export const errorUrl = (baseUrl: string, error: string) =>
`${baseUrl}/cli-login/error?error=${error}`;
export const baseUrlSuffix = (baseUrl: string) => {
return baseUrl !== DEFAULT_BASE_URL ? ` at ${chalk.cyan(baseUrl)}` : "";
};
export function environmentUrl(baseUrl: string, environment: UrlArgs): string {
return `${baseUrl}/envs/${slug(environment)}`;
}
export function featureUrl(
baseUrl: string,
env: UrlArgs,
feature: UrlArgs,
): string {
return `${environmentUrl(baseUrl, env)}/features/${slug(feature)}`;
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
export interface Logger {
debug(message: string, ...args: any[]): void;
info(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
}
export const quietConsoleLogger = {
debug(_: string) {
// do nothing
},
info(_: string) {
// do nothing
},
warn(message: string, ...args: any[]) {
console.warn(message, ...args);
},
error(message: string, ...args: any[]) {
console.error(message, ...args);
},
};
export function loggerWithPrefix(logger: Logger, prefix: string): Logger {
return {
debug(message: string, ...args: any[]) {
logger.debug(`${prefix} ${message}`, ...args);
},
info(message: string, ...args: any[]) {
logger.info(`${prefix} ${message}`, ...args);
},
warn(message: string, ...args: any[]) {
logger.warn(`${prefix} ${message}`, ...args);
},
error(message: string, ...args: any[]) {
logger.error(`${prefix} ${message}`, ...args);
},
};
}
```
--------------------------------------------------------------------------------
/packages/node-sdk/examples/express/app.test.ts:
--------------------------------------------------------------------------------
```typescript
import request from "supertest";
import app, { todos } from "./app";
import { beforeEach, describe, it, expect, beforeAll } from "vitest";
import reflag from "./reflag";
beforeAll(async () => await reflag.initialize());
beforeEach(() => {
reflag.featureOverrides = {
"show-todos": true,
};
});
describe("API Tests", () => {
it("should return 200 for the root endpoint", async () => {
const response = await request(app).get("/");
expect(response.status).toBe(200);
expect(response.body).toEqual({ message: "Ready to manage some TODOs!" });
});
it("should return todos", async () => {
const response = await request(app).get("/todos");
expect(response.status).toBe(200);
expect(response.body).toEqual({ todos });
});
it("should return no todos when list is disabled", async () => {
reflag.featureOverrides = () => ({
"show-todos": false,
});
const response = await request(app).get("/todos");
expect(response.status).toBe(200);
expect(response.body).toEqual({ todos: [] });
});
});
```
--------------------------------------------------------------------------------
/packages/browser-sdk/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
const base = require("@reflag/eslint-config");
const preactConfig = require("eslint-config-preact");
const compatPlugin = require("eslint-plugin-compat");
const reactPlugin = require("eslint-plugin-react");
const reactHooksPlugin = require("eslint-plugin-react-hooks");
module.exports = [
...base,
{
// Preact projects
files: ["**/*.tsx"],
settings: {
react: {
// We only care about marking h() as being a used variable.
pragma: "h",
// We use "react 16.0" to avoid pushing folks to UNSAFE_ methods.
version: "16.0",
},
},
plugins: {
compat: compatPlugin,
react: reactPlugin,
"react-hooks": reactHooksPlugin,
},
rules: {
...preactConfig.rules,
// Ignore React attributes that are not valid in Preact.
// Alternatively, we could use the preact/compat alias or turn off the rule.
"react/no-unknown-property": ["off"],
"no-unused-vars": ["off"],
"react/no-danger": ["off"],
},
},
{ ignores: ["dist/", "example/"] },
];
```
--------------------------------------------------------------------------------
/packages/cli/schema.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Reflag cli schema",
"type": "object",
"properties": {
"baseUrl": {
"type": "string",
"pattern": "^https?://.*",
"description": "Base URL for the API. Defaults to https://app.reflag.com."
},
"apiUrl": {
"type": "string",
"pattern": "^https?://.*",
"description": "API URL for the API. Defaults to https://app.reflag.com/api."
},
"appId": {
"type": "string",
"minLength": 14,
"maxLength": 14,
"description": "Mandatory ID for the Reflag app. You can find it by calling reflag apps list."
},
"typesOutput": {
"type": "array",
"description": "List of paths to output the types. The path is relative to the current working directory.",
"items": {
"type": "object",
"properties": {
"path": { "type": "string" },
"format": { "type": "string", "enum": ["react", "node"] }
},
"required": ["path"]
}
}
},
"required": ["appId"]
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/vite.config.mjs:
--------------------------------------------------------------------------------
```
import { resolve } from "path";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import { defaultExclude } from "vitest/config";
export default defineConfig({
test: {
root: __dirname,
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
exclude: [...defaultExclude, "test/e2e/**"],
},
plugins: [dts({ insertTypesEntry: true })],
build: {
exclude: ["**/node_modules/**", "test/e2e/**"],
sourcemap: true,
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, "src/index.ts"),
name: "ReflagBrowserSDK",
// the proper extensions will be added
fileName: "reflag-browser-sdk",
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
// external: ["vue"],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
ReflagClient: "ReflagClient",
},
},
},
},
});
```
--------------------------------------------------------------------------------
/packages/flag-evaluation/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@reflag/flag-evaluation",
"version": "1.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reflagcom/javascript.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc --project tsconfig.build.json",
"test": "vitest",
"test:ci": "vitest --reporter=default --reporter=junit --outputFile=junit.xml",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn vitest run -c vite.config.js && yarn build"
},
"files": [
"dist"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"devDependencies": {
"@reflag/eslint-config": "^0.0.2",
"@reflag/tsconfig": "^0.0.2",
"@types/node": "^22.12.0",
"eslint": "^9.21.0",
"prettier": "^3.5.2",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"vitest": "^2.0.5"
},
"dependencies": {
"js-sha256": "0.11.0"
}
}
```
--------------------------------------------------------------------------------
/packages/vue-sdk/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { App } from "vue";
import ReflagBootstrappedProvider from "./ReflagBootstrappedProvider.vue";
import ReflagClientProvider from "./ReflagClientProvider.vue";
import ReflagProvider from "./ReflagProvider.vue";
export {
useClient,
useFlag,
useIsLoading,
useOnEvent,
useRequestFeedback,
useSendFeedback,
useTrack,
useUpdateCompany,
useUpdateOtherContext,
useUpdateUser,
} from "./hooks";
export type {
BootstrappedFlags,
EmptyFlagRemoteConfig,
Flag,
Flags,
FlagType,
ReflagBaseProps,
ReflagBootstrappedProps,
ReflagClientProviderProps,
ReflagInitOptionsBase,
ReflagProps,
RequestFlagFeedbackOptions,
TypedFlags,
} from "./types";
export type {
CheckEvent,
CompanyContext,
TrackEvent,
UserContext,
} from "@reflag/browser-sdk";
export { ReflagBootstrappedProvider, ReflagClientProvider, ReflagProvider };
export default {
install(app: App) {
app.component("ReflagProvider", ReflagProvider);
app.component("ReflagBootstrappedProvider", ReflagBootstrappedProvider);
app.component("ReflagClientProvider", ReflagClientProvider);
},
};
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/toolbar/Switch.tsx:
--------------------------------------------------------------------------------
```typescript
import { Fragment, h } from "preact";
interface SwitchProps extends h.JSX.HTMLAttributes<HTMLInputElement> {
checked: boolean;
width?: number;
height?: number;
}
const gutter = 1;
export function Switch({
checked,
width = 24,
height = 14,
...props
}: SwitchProps) {
return (
<>
<label class="switch" data-enabled={checked}>
<input
checked={checked}
class="switch-input"
name="enabled"
type="checkbox"
{...props}
/>
<div
class="switch-track"
style={{
width: `${width}px`,
height: `${height}px`,
borderRadius: `${height}px`,
}}
>
<div
class="switch-dot"
style={{
width: `${height - gutter * 2}px`,
height: `${height - gutter * 2}px`,
transform: checked
? `translateX(${width - (height - gutter * 2) - gutter}px)`
: `translateX(${gutter}px)`,
top: `${gutter}px`,
}}
/>
</div>
</label>
</>
);
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/packages/floating-ui-preact-dom/utils/deepEqual.ts:
--------------------------------------------------------------------------------
```typescript
// Fork of `fast-deep-equal` that only does the comparisons we need and compares
// functions
export function deepEqual(a: any, b: any) {
if (a === b) {
return true;
}
if (typeof a !== typeof b) {
return false;
}
if (typeof a === "function" && a.toString() === b.toString()) {
return true;
}
let length, i, keys;
if (a && b && typeof a == "object") {
if (Array.isArray(a)) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0; ) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length) {
return false;
}
for (i = length; i-- !== 0; ) {
if (!{}.hasOwnProperty.call(b, keys[i])) {
return false;
}
}
for (i = length; i-- !== 0; ) {
const key = keys[i];
if (key === "_owner" && a.$$typeof) {
continue;
}
if (!deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
return a !== a && b !== b;
}
```
--------------------------------------------------------------------------------
/packages/vue-sdk/dev/plain/components/Events.vue:
--------------------------------------------------------------------------------
```vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { CheckEvent, TrackEvent, useClient } from "../../../src";
import Section from "./Section.vue";
const client = useClient();
const events = ref<string[]>([]);
function checkEvent(evt: CheckEvent) {
events.value = [
...events.value,
`Check event: The feature ${evt.key} is ${evt.value} for user.`,
];
}
function featuresUpdatedEvent() {
events.value = [...events.value, `Flags Updated!`];
}
function trackEvent(evt: TrackEvent) {
events.value = [...events.value, `Track event: ${evt.eventName}`];
}
onMounted(() => {
client.on("check", checkEvent);
client.on("flagsUpdated", featuresUpdatedEvent);
client.on("track", trackEvent);
});
onUnmounted(() => {
client.off("check", checkEvent);
client.off("flagsUpdated", featuresUpdatedEvent);
client.off("track", trackEvent);
});
</script>
<template>
<Section title="Events">
<div
style="display: flex; gap: 10px; flex-wrap: wrap; flex-direction: column"
>
<div v-for="event in events" :key="event">
{{ event }}
</div>
</div>
</Section>
</template>
```
--------------------------------------------------------------------------------
/packages/node-sdk/examples/express/bucket.ts:
--------------------------------------------------------------------------------
```typescript
import { ReflagClient, Context, FlagOverrides } from "../../";
type CreateConfigPayload = {
minimumLength: number;
};
// Extending the Flags interface to define the available features
declare module "../../types" {
interface Flags {
"show-todos": boolean;
"create-todos": {
config: {
payload: CreateConfigPayload;
};
};
"delete-todos": boolean;
"some-else": {};
}
}
let featureOverrides = (_: Context): FlagOverrides => {
return {
"create-todos": {
isEnabled: true,
config: {
key: "short",
payload: {
minimumLength: 10,
},
},
},
}; // feature keys checked at compile time
};
// Create a new ReflagClient instance with the secret key and default features
// The default features will be used if the user does not have any features set
// Create a reflag.config.json file to configure the client or set environment variables
// like REFLAG_SECRET_KEY, REFLAG_FLAGS_ENABLED, REFLAG_FLAGS_DISABLED, etc.
export default new ReflagClient({
// Optional: Set a logger to log debug information, errors, etc.
logger: console,
featureOverrides, // Optional: Set feature overrides
});
```
--------------------------------------------------------------------------------
/packages/react-sdk/dev/nextjs-bootstrap-demo/app/layout.tsx:
--------------------------------------------------------------------------------
```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { getServerClient } from "./client";
import { ReflagBootstrappedProvider } from "@reflag/react-sdk";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
const publishableKey = process.env.REFLAG_PUBLISHABLE_KEY || "";
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// Get the singleton server client
const serverClient = await getServerClient();
// In a real app, you'd get user/company from your auth system
const flags = serverClient.getFlagsForBootstrap({
user: {
id: "demo-user",
email: "[email protected]",
"optin-huddles": true,
},
company: { id: "demo-company", name: "Demo Company" },
other: { source: "web" },
});
return (
<html lang="en">
<body className={inter.className}>
<ReflagBootstrappedProvider
publishableKey={publishableKey}
flags={flags}
>
{children}
</ReflagBootstrappedProvider>
</body>
</html>
);
}
```
--------------------------------------------------------------------------------
/packages/openfeature-browser-provider/example/components/HuddleFeature.tsx:
--------------------------------------------------------------------------------
```typescript
"use client";
import React from "react";
import {
useBooleanFlagValue,
useObjectFlagDetails,
} from "@openfeature/react-sdk";
import { track } from "@/app/featureManagement";
const flagKey = "huddle";
export const HuddleFeature = () => {
const isEnabled = useBooleanFlagValue(flagKey, false);
const { variant: huddleMeetingProvider, value: config } =
useObjectFlagDetails(flagKey, {
joinUrl: "https://zoom.us/join",
});
return (
<div className="border border-gray-300 p-6 rounded-xl dark:border-neutral-800 dark:bg-zinc-800/30">
<h3 className="text-xl mb-4">Huddle feature enabled:</h3>
<pre>
<code className="font-mono font-bold">{JSON.stringify(isEnabled)}</code>
</pre>
<h3 className="text-xl mb-4">
Huddle using <strong>{huddleMeetingProvider}</strong>:
</h3>
<pre>
<code className="font-mono font-bold">
Join the huddle at <a href={config.joinUrl}>{config.joinUrl}</a>
</code>
</pre>
<button
className="border-solid m-auto max-w-60 border-2 border-indigo-600 rounded-lg p-2 mt-4 disabled:opacity-50"
onClick={() => track(flagKey)}
>
Track usage
</button>
</div>
);
};
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/index.ts:
--------------------------------------------------------------------------------
```typescript
export type {
Config,
Flag,
FlagRemoteConfig,
InitOptions,
ToolbarOptions,
} from "./client";
export { ReflagClient } from "./client";
export type {
CompanyContext,
ReflagContext,
ReflagDeprecatedContext,
UserContext,
} from "./context";
export type {
Feedback,
FeedbackOptions,
FeedbackPrompt,
FeedbackPromptHandler,
FeedbackPromptHandlerCallbacks,
FeedbackPromptHandlerOpenFeedbackFormOptions,
FeedbackPromptReply,
FeedbackPromptReplyHandler,
RequestFeedbackData,
RequestFeedbackOptions,
UnassignedFeedback,
} from "./feedback/feedback";
export type { DEFAULT_TRANSLATIONS } from "./feedback/ui/config/defaultTranslations";
export type {
FeedbackScoreSubmission,
FeedbackSubmission,
FeedbackTranslations,
OnScoreSubmitResult,
OpenFeedbackFormOptions,
} from "./feedback/ui/types";
export type {
CheckEvent,
FallbackFlagOverride,
FlagOverrides,
RawFlag,
RawFlags,
} from "./flag/flags";
export type { HookArgs, State, TrackEvent } from "./hooksManager";
export type { Logger } from "./logger";
export { feedbackContainerId, propagatedEvents } from "./ui/constants";
export type {
DialogPlacement,
Offset,
PopoverPlacement,
Position,
ToolbarPosition,
} from "./ui/types";
```
--------------------------------------------------------------------------------
/packages/openfeature-browser-provider/example/components/Context.tsx:
--------------------------------------------------------------------------------
```typescript
"use client";
import { OpenFeature } from "@openfeature/react-sdk";
import React from "react";
const initialContext = {
trackingKey: "user42",
companyName: "Acme Inc.",
companyPlan: "enterprise",
companyId: "company42",
};
export const Context = () => {
const [context, setContext] = React.useState<any>(
JSON.stringify(initialContext, null, 2),
);
let validJson = true;
try {
validJson = JSON.parse(context);
} catch (e) {
validJson = false;
}
return (
<div className="flex flex-col border border-gray-300 p-6 rounded-xl size-full dark:border-neutral-800 dark:bg-zinc-800/30">
<h3 className="text-xl mb-4">Context:</h3>
<textarea
className="min-h-[200px]"
value={context}
onChange={(e) => setContext(e.target.value)}
></textarea>
<button
disabled={!validJson}
className="border-solid m-auto max-w-60 border-2 border-indigo-600 rounded-lg p-2 mt-4 disabled:opacity-50"
onClick={() => OpenFeature.setContext(JSON.parse(context))}
>
Update Context
</button>
<span className="text-gray-600">
Open the developer console to see what happens when you update the
context.
</span>
</div>
);
};
```
--------------------------------------------------------------------------------
/packages/react-sdk/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
const base = require("@reflag/eslint-config");
const reactPlugin = require("eslint-plugin-react");
const hooksPlugin = require("eslint-plugin-react-hooks");
module.exports = [
...base,
{ ignores: ["dist/", "dev/"] },
{
files: ["**/*.ts", "**/*.tsx"],
rules: {
...reactPlugin.configs.recommended.rules,
...hooksPlugin.configs.recommended.rules,
"react/jsx-key": [
"error",
{
checkFragmentShorthand: true,
},
],
"react/self-closing-comp": ["error"],
"react/prefer-es6-class": ["error"],
"react/prefer-stateless-function": ["warn"],
"react/no-did-mount-set-state": ["error"],
"react/no-did-update-set-state": ["error"],
"react/jsx-filename-extension": [
"warn",
{
extensions: [".mdx", ".jsx", ".tsx"],
},
],
"react/react-in-jsx-scope": ["off"],
"react/jsx-sort-props": [
"error",
{
callbacksLast: true,
shorthandFirst: false,
shorthandLast: true,
ignoreCase: true,
noSortAlphabetically: false,
reservedFirst: true,
},
],
},
plugins: {
react: reactPlugin,
"react-hooks": hooksPlugin,
},
},
];
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/promptStorage.ts:
--------------------------------------------------------------------------------
```typescript
import Cookies from "js-cookie";
export const markPromptMessageCompleted = (
userId: string,
promptId: string,
expiresAt: Date,
) => {
Cookies.set(`reflag-prompt-${userId}`, promptId, {
expires: expiresAt,
sameSite: "strict",
secure: true,
});
};
export const checkPromptMessageCompleted = (
userId: string,
promptId: string,
) => {
const id =
Cookies.get(`reflag-prompt-${userId}`) ||
Cookies.get(`bucket-prompt-${userId}`); // Legacy cookie name
return id === promptId;
};
export const rememberAuthToken = (
userId: string,
channel: string,
token: string,
expiresAt: Date,
) => {
Cookies.set(`reflag-token-${userId}`, JSON.stringify({ channel, token }), {
expires: expiresAt,
sameSite: "strict",
secure: true,
});
};
export const getAuthToken = (userId: string) => {
const val = Cookies.get(`reflag-token-${userId}`);
if (!val) {
return undefined;
}
try {
const { channel, token } = JSON.parse(val) as {
channel: string;
token: string;
};
if (!channel?.length || !token?.length) {
return undefined;
}
return {
channel,
token,
};
} catch {
return undefined;
}
};
export const forgetAuthToken = (userId: string) => {
Cookies.remove(`reflag-token-${userId}`);
};
```
--------------------------------------------------------------------------------
/packages/vue-sdk/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
const base = require("@reflag/eslint-config");
const importsPlugin = require("eslint-plugin-import");
const vuePlugin = require("eslint-plugin-vue");
const vueParser = require("vue-eslint-parser");
module.exports = [
...base,
{
ignores: ["dist/"],
},
{
// Vue files
files: ["**/*.vue"],
plugins: {
vue: vuePlugin,
import: importsPlugin,
},
languageOptions: {
parser: vueParser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
parser: {
ts: require("@typescript-eslint/parser"),
},
},
},
settings: {
"import/resolver": {
typescript: {
alwaysTryTypes: true,
project: "./tsconfig.eslint.json",
},
},
},
rules: {
...vuePlugin.configs.recommended.rules,
...vuePlugin.configs["vue3-recommended"].rules,
// Vue specific rules
"vue/multi-word-component-names": "off",
"vue/no-unused-vars": "warn",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/no-v-html": "off",
// Import rules for Vue files
"import/no-unresolved": "off", // Disable for now since we're using TypeScript resolver
},
},
];
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/StarRating.css:
--------------------------------------------------------------------------------
```css
.star-rating {
display: flex;
flex-direction: column;
}
.star-rating-icons {
display: flex;
width: 0;
> input {
border: 0px;
clip: rect(0px, 0px, 0px, 0px);
height: 1px;
width: 1px;
margin: -1px;
padding: 0px;
overflow: hidden;
white-space: nowrap;
position: absolute;
}
> .button {
border: 1px solid;
border-color: var(--reflag-feedback-dialog-input-border-color, #d8d9df);
padding: 0px 7px;
&:not(:first-of-type) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&:not(:last-of-type) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
margin-inline-end: -1px;
}
+ .button-tooltip {
pointer-events: none;
opacity: 0;
background: var(--reflag-feedback-dialog-tooltip-background-color, #000);
color: var(--reflag-feedback-dialog-tooltip-color, #fff);
padding: 6px 8px;
border-radius: 4px;
font-size: 13px;
}
&:hover + .button-tooltip {
opacity: 1;
}
> svg {
transition: transform 200ms ease-in-out;
}
}
}
.button-tooltip-arrow {
position: absolute;
width: 10px;
height: 10px;
background-color: var(
--reflag-feedback-dialog-tooltip-background-color,
#000
);
transform: rotate(45deg);
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/icons/VerySatisfied.tsx:
--------------------------------------------------------------------------------
```typescript
import { FunctionComponent, h } from "preact";
export const VerySatisfied: FunctionComponent<
h.JSX.SVGAttributes<SVGSVGElement>
> = (props) => (
<svg
fill="none"
height="22"
viewBox="0 0 24 24"
width="22"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2ZM12 4C9.87827 4 7.84344 4.84285 6.34315 6.34315C4.84285 7.84344 4 9.87827 4 12C4 14.1217 4.84285 16.1566 6.34315 17.6569C7.84344 19.1571 9.87827 20 12 20C14.1217 20 16.1566 19.1571 17.6569 17.6569C19.1571 16.1566 20 14.1217 20 12C20 9.87827 19.1571 7.84344 17.6569 6.34315C16.1566 4.84285 14.1217 4 12 4ZM12 12C14 12 15.667 12.333 17 13C17 14.3261 16.4732 15.5979 15.5355 16.5355C14.5979 17.4732 13.3261 18 12 18C10.6739 18 9.40215 17.4732 8.46447 16.5355C7.52678 15.5979 7 14.3261 7 13C8.333 12.333 10 12 12 12ZM8.5 7C9.07633 6.99988 9.63499 7.19889 10.0815 7.56335C10.5279 7.9278 10.8347 8.43532 10.95 9H6.05C6.16526 8.43532 6.47209 7.9278 6.91855 7.56335C7.36501 7.19889 7.92367 6.99988 8.5 7ZM15.5 7C16.0763 6.99988 16.635 7.19889 17.0814 7.56335C17.5279 7.9278 17.8347 8.43532 17.95 9H13.05C13.1653 8.43532 13.4721 7.9278 13.9185 7.56335C14.365 7.19889 14.9237 6.99988 15.5 7Z"
fill="currentColor"
/>
</svg>
);
```
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
{
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": ["source.formatDocument", "source.fixAll.eslint"],
"editor.formatOnSave": false,
"editor.insertSpaces": true,
"editor.tabSize": 2,
"eslint.workingDirectories": [{ "mode": "location" }],
"files.exclude": {
"**/node_modules": true
},
"search.exclude": {
"**/.next": true,
"**/build": true,
"**/dist": true,
"**/coverage": true,
"**/node_modules": true,
"**/*.lock": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"cSpell.words": [
"booleanish",
"bucketco",
"npmjs",
"nvmrc",
"openfeature",
"PKCE",
"Reflag",
"reflagcom"
]
}
```
--------------------------------------------------------------------------------
/packages/cli/commands/new.ts:
--------------------------------------------------------------------------------
```typescript
import { Command } from "commander";
import { findUp } from "find-up";
import { configStore } from "../stores/config.js";
import { CONFIG_FILE_NAME } from "../utils/constants.js";
import {
appIdOption,
flagKeyOption,
flagNameArgument,
typesFormatOption,
typesOutOption,
} from "../utils/options.js";
import { createFlagAction, generateTypesAction } from "./flags.js";
import { initAction } from "./init.js";
type NewArgs = {
appId?: string;
out: string;
key?: string;
};
export const newAction = async (name: string | undefined, { key }: NewArgs) => {
if (!(await findUp(CONFIG_FILE_NAME))) {
await initAction();
}
await createFlagAction(name, {
key,
});
await generateTypesAction();
};
export function registerNewCommand(cli: Command) {
cli
.command("new")
.description(
"Initialize the Reflag CLI, authenticates, and creates a new flag.",
)
.addOption(appIdOption)
.addOption(typesOutOption)
.addOption(typesFormatOption)
.addOption(flagKeyOption)
.addArgument(flagNameArgument)
.action(newAction);
// Update the config with the cli override values
cli.hook("preAction", (command) => {
const { appId, out, format } = command.opts();
configStore.setConfig({
appId,
typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
});
});
}
```
--------------------------------------------------------------------------------
/packages/vue-sdk/dev/plain/components/RequestFeedback.vue:
--------------------------------------------------------------------------------
```vue
<script setup lang="ts">
import { useRequestFeedback } from "../../../src";
import Section from "./Section.vue";
const requestFeedback = useRequestFeedback();
</script>
<template>
<Section title="Request Feedback">
<div style="display: flex; gap: 10px; flex-wrap: wrap">
<button
@click="
(e) =>
requestFeedback({
flagKey: 'demo-feature',
title: 'How satisfied are you with this feature?',
position: {
type: 'POPOVER',
anchor: e.currentTarget as HTMLElement,
},
})
"
>
Request Feedback (Popover)
</button>
<button
@click="
requestFeedback({
flagKey: 'demo-feature',
title: 'How was your experience?',
position: {
type: 'MODAL',
},
})
"
>
Request Feedback (Modal)
</button>
<button
@click="
requestFeedback({
flagKey: 'demo-feature',
title: 'What do you think about our product?',
position: {
type: 'MODAL',
},
openWithCommentVisible: true,
})
"
>
Request Feedback (Modal with Comment)
</button>
</div>
</Section>
</template>
```
--------------------------------------------------------------------------------
/packages/react-sdk/dev/nextjs-bootstrap-demo/public/next.svg:
--------------------------------------------------------------------------------
```
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
```
--------------------------------------------------------------------------------
/packages/react-sdk/dev/nextjs-flag-demo/public/next.svg:
--------------------------------------------------------------------------------
```
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
```
--------------------------------------------------------------------------------
/packages/browser-sdk/example/typescript/app.ts:
--------------------------------------------------------------------------------
```typescript
import { ReflagClient, RawFlags } from "../../src";
const urlParams = new URLSearchParams(window?.location?.search);
const publishableKey = urlParams.get("publishableKey");
const flagKey = urlParams.get("flagKey") ?? "huddles";
if (!publishableKey) {
throw Error("publishableKey is missing");
}
const reflag = new ReflagClient({
publishableKey,
user: { id: "42" },
company: { id: "1" },
toolbar: {
show: true,
position: { placement: "bottom-right" },
},
});
document
.getElementById("startHuddle")
?.addEventListener("click", () => reflag.track(flagKey));
document.getElementById("giveFeedback")?.addEventListener("click", (event) =>
reflag.requestFeedback({
flagKey,
position: { type: "POPOVER", anchor: event.currentTarget as HTMLElement },
}),
);
reflag.initialize().then(() => {
console.log("Reflag initialized");
const loadingElem = document.getElementById("loading");
if (loadingElem) loadingElem.style.display = "none";
});
reflag.on("flagsUpdated", (flags: RawFlags) => {
const { isEnabled } = flags[flagKey];
const startHuddleElem = document.getElementById("start-huddle");
if (isEnabled) {
// show the start-huddle button
if (startHuddleElem) startHuddleElem.style.display = "block";
} else {
// hide the start-huddle button
if (startHuddleElem) startHuddleElem.style.display = "none";
}
});
```
--------------------------------------------------------------------------------
/packages/node-sdk/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@reflag/node-sdk",
"version": "1.1.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reflagcom/javascript.git"
},
"scripts": {
"dev": "vite",
"start": "vite",
"build": "tsc --project tsconfig.build.json",
"test": "vitest run",
"test:watch": "vitest",
"test:ci": "yarn test --reporter=default --reporter=junit --outputFile=junit.xml",
"coverage": "yarn test --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn test && yarn build"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"main": "./dist/src/index.js",
"types": "./dist/types/src/index.d.ts",
"devDependencies": {
"@babel/core": "~7.24.7",
"@reflag/eslint-config": "~0.0.2",
"@reflag/tsconfig": "~0.0.2",
"@types/node": "^22.12.0",
"@vitest/coverage-v8": "~1.6.0",
"c8": "~10.1.0",
"eslint": "^9.21.0",
"flush-promises": "~1.0.2",
"prettier": "^3.5.2",
"ts-node": "~10.9.2",
"typescript": "^5.7.3",
"vite": "~5.4.18",
"vite-plugin-dts": "~3.9.1",
"vitest": "~1.6.0"
},
"dependencies": {
"@reflag/flag-evaluation": "1.0.0"
}
}
```
--------------------------------------------------------------------------------
/packages/vue-sdk/src/ReflagBootstrappedProvider.vue:
--------------------------------------------------------------------------------
```vue
<script setup lang="ts">
import { onMounted, provide, ref, watch } from "vue";
import { ProviderSymbol, useOnEvent, useReflagClient } from "./hooks";
import type { ReflagBootstrappedProps } from "./types";
const {
flags,
initialLoading = false,
enableTracking = true,
debug,
...config
} = defineProps<ReflagBootstrappedProps>();
const client = useReflagClient(
{
...config,
...flags?.context,
enableTracking,
bootstrappedFlags: flags?.flags,
},
debug,
);
const isLoading = ref(
client.getState() !== "initialized" ? initialLoading : false,
);
useOnEvent(
"stateUpdated",
(state) => {
isLoading.value = state === "initializing";
},
client,
);
// Initialize the client if it is not already initialized
onMounted(() => {
if (client.getState() !== "idle") return;
void client.initialize().catch((e) => {
client.logger.error("failed to initialize client", e);
});
});
// Update the context if it changes
watch(
() => flags.context,
(newContext) => {
void client.setContext(newContext);
},
{ deep: true },
);
// Update the flags if they change
watch(
() => flags.flags,
(newFlags) => {
client.updateFlags(newFlags);
},
{ deep: true },
);
provide(ProviderSymbol, {
isLoading,
client,
});
</script>
<template>
<slot v-if="isLoading && $slots.loading" name="loading" />
<slot v-else />
</template>
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/Button.css:
--------------------------------------------------------------------------------
```css
.button {
appearance: none;
display: inline-flex;
justify-content: center;
align-items: center;
user-select: none;
position: relative;
white-space: nowrap;
height: 2rem;
padding-inline-start: 0.75rem;
padding-inline-end: 0.75rem;
gap: 0.5em;
justify-content: center;
border: none;
cursor: pointer;
font-family: var(--reflag-feedback-dialog-font-family);
font-size: 12px;
font-weight: 500;
box-shadow:
0 1px 2px 0 rgba(0, 0, 0, 0.06),
0 1px 1px 0 rgba(0, 0, 0, 0.01);
border-radius: var(--reflag-feedback-dialog-border-radius, 6px);
transition-duration: 200ms;
transition-property:
background-color, border-color, color, opacity, box-shadow, transform;
&.primary {
background-color: var(
--reflag-feedback-dialog-primary-button-background-color,
white
);
color: var(--reflag-feedback-dialog-primary-button-color, #1e1f24);
border: 1px solid
var(--reflag-feedback-dialog-primary-border-color, #d8d9df);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: var(--reflag-feedback-dialog-primary-border-color, #d8d9df);
transition-duration: 200ms;
transition-property:
background-color, border-color, color, opacity, box-shadow, transform;
}
&:focus {
outline: none;
border-color: var(
--reflag-feedback-dialog-input-focus-border-color,
#787c91
);
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/constants.ts:
--------------------------------------------------------------------------------
```typescript
/**
* ID of HTML DIV element which contains the feedback dialog
*/
export const feedbackContainerId = "reflag-feedback-dialog-container";
export const toolbarContainerId = "reflag-toolbar-dialog-container";
/**
* These events will be propagated to the feedback dialog
*
* @see [https://developer.mozilla.org/en-US/docs/Web/API/Element#events](https://developer.mozilla.org/en-US/docs/Web/API/Element#events)
*/
export const propagatedEvents = [
"animationcancel",
"animationend",
"animationiteration",
"animationstart",
"afterscriptexecute",
"auxclick",
"beforescriptexecute",
"blur",
"click",
"compositionend",
"compositionstart",
"compositionupdate",
"contextmenu",
"copy",
"cut",
"dblclick",
"DOMActivate",
"DOMMouseScroll",
"error",
"focusin",
"focusout",
"focus",
"fullscreenchange",
"fullscreenerror",
"gesturechange",
"gestureend",
"gesturestart",
"gotpointercapture",
"keydown",
"keypress",
"keyup",
"lostpointercapture",
"mousedown",
"mouseenter",
"mouseleave",
"mousemove",
"mouseout",
"mouseover",
"mouseup",
"mousewheel",
"paste",
"pointercancel",
"pointerdown",
"pointerenter",
"pointerleave",
"pointermove",
"pointerout",
"pointerover",
"pointerup",
"scroll",
"select",
"touchcancel",
"touchend",
"touchmove",
"touchstart",
"transitioncancel",
"transitionend",
"transitionrun",
"transitionstart",
"wheel",
];
```
--------------------------------------------------------------------------------
/packages/node-sdk/src/flusher.ts:
--------------------------------------------------------------------------------
```typescript
import { constants } from "os";
import { END_FLUSH_TIMEOUT_MS } from "./config";
import { TimeoutError, withTimeout } from "./utils";
type Callback = () => Promise<void>;
const killSignals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"] as const;
export function subscribe(
callback: Callback,
timeout: number = END_FLUSH_TIMEOUT_MS,
) {
let state: boolean | undefined;
const wrappedCallback = async () => {
if (state !== undefined) {
return;
}
state = false;
try {
await withTimeout(callback(), timeout);
} catch (error) {
if (error instanceof TimeoutError) {
console.error(
"[Reflag SDK] Timeout while flushing events on process exit.",
);
} else {
console.error(
"[Reflag SDK] An error occurred while flushing events on process exit.",
error,
);
}
}
state = true;
};
killSignals.forEach((signal) => {
const hasListeners = process.listenerCount(signal) > 0;
if (hasListeners) {
process.prependListener(signal, wrappedCallback);
} else {
process.on(signal, async () => {
await wrappedCallback();
process.exit(0x80 + constants.signals[signal]);
});
}
});
process.on("beforeExit", wrappedCallback);
process.on("exit", () => {
if (!state) {
console.error(
"[Reflag SDK] Failed to finalize the flushing of events on process exit.",
);
}
});
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/packages/floating-ui-preact-dom/arrow.ts:
--------------------------------------------------------------------------------
```typescript
import type { Middleware, Padding } from "@floating-ui/core";
import { arrow as arrowCore, MiddlewareState } from "@floating-ui/dom";
import { RefObject } from "preact";
export interface Options {
/**
* The arrow element to be positioned.
* @default undefined
*/
element: RefObject<Element | null> | Element | null;
/**
* The padding between the arrow element and the floating element edges.
* Useful when the floating element has rounded corners.
* @default 0
*/
padding?: Padding;
}
/**
* Provides data to position an inner element of the floating element so that it
* appears centered to the reference element.
* This wraps the core `arrow` middleware to allow React refs as the element.
* @see https://floating-ui.com/docs/arrow
*/
export const arrow = (
options: Options | ((state: MiddlewareState) => Options),
): Middleware => {
function isRef(value: unknown): value is RefObject<unknown> {
return {}.hasOwnProperty.call(value, "current");
}
return {
name: "arrow",
options,
fn(state) {
const { element, padding } =
typeof options === "function" ? options(state) : options;
if (element && isRef(element)) {
if (element.current != null) {
return arrowCore({ element: element.current, padding }).fn(state);
}
return {};
} else if (element) {
return arrowCore({ element, padding }).fn(state);
}
return {};
},
};
};
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/hooks/useTimer.ts:
--------------------------------------------------------------------------------
```typescript
import { useCallback, useEffect, useState } from "preact/hooks";
export const useTimer = ({
initialDuration,
enabled,
onEnd,
}: {
initialDuration: number;
enabled: boolean;
onEnd: () => void;
}): {
duration: number;
elapsedFraction: number;
startTime: number;
endTime: number;
stopped: boolean;
startWithDuration: (duration: number) => void;
stop: () => void;
} => {
const [stopped, setStopped] = useState(!enabled);
const [duration, setDuration] = useState(initialDuration);
const [startTime, setStartTime] = useState(Date.now());
const [currentTime, setCurrentTime] = useState(Date.now());
useEffect(() => {
if (stopped) return;
const t = setInterval(() => {
setCurrentTime(Date.now());
if (Date.now() >= startTime + duration) {
clearTimeout(t);
setStopped(true);
onEnd();
}
}, 25);
return () => {
clearTimeout(t);
};
}, [duration, onEnd, startTime, stopped]);
const stop = useCallback(() => {
setStopped(true);
}, []);
const startWithDuration = useCallback((nextDuration: number) => {
setStartTime(Date.now());
setDuration(nextDuration);
setStopped(false);
}, []);
const endTime = startTime + duration;
const elapsedMs = stopped ? 0 : currentTime - startTime;
const elapsedFraction = elapsedMs / duration;
return {
duration,
elapsedFraction,
startTime,
endTime,
stopped,
startWithDuration,
stop,
};
};
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/icons/Neutral.tsx:
--------------------------------------------------------------------------------
```typescript
import { FunctionComponent, h } from "preact";
export const Neutral: FunctionComponent<h.JSX.SVGAttributes<SVGSVGElement>> = (
props,
) => (
<svg
fill="none"
height="22"
viewBox="0 0 24 24"
width="22"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22ZM12 20C14.1217 20 16.1566 19.1571 17.6569 17.6569C19.1571 16.1566 20 14.1217 20 12C20 9.87827 19.1571 7.84344 17.6569 6.34315C16.1566 4.84285 14.1217 4 12 4C9.87827 4 7.84344 4.84285 6.34315 6.34315C4.84285 7.84344 4 9.87827 4 12C4 14.1217 4.84285 16.1566 6.34315 17.6569C7.84344 19.1571 9.87827 20 12 20ZM8 15C8 14.4477 8.44771 14 9 14H15C15.5523 14 16 14.4477 16 15V15C16 15.5523 15.5523 16 15 16H9C8.44771 16 8 15.5523 8 15V15ZM8 11C7.60217 11 7.22064 10.842 6.93934 10.5607C6.65803 10.2794 6.5 9.89782 6.5 9.5C6.5 9.10217 6.65803 8.72064 6.93934 8.43934C7.22064 8.15803 7.60217 8 8 8C8.39782 8 8.77935 8.15803 9.06066 8.43934C9.34196 8.72064 9.5 9.10217 9.5 9.5C9.5 9.89782 9.34196 10.2794 9.06066 10.5607C8.77935 10.842 8.39782 11 8 11ZM16 11C15.6022 11 15.2206 10.842 14.9393 10.5607C14.658 10.2794 14.5 9.89782 14.5 9.5C14.5 9.10217 14.658 8.72064 14.9393 8.43934C15.2206 8.15803 15.6022 8 16 8C16.3978 8 16.7794 8.15803 17.0607 8.43934C17.342 8.72064 17.5 9.10217 17.5 9.5C17.5 9.89782 17.342 10.2794 17.0607 10.5607C16.7794 10.842 16.3978 11 16 11Z"
fill="currentColor"
/>
</svg>
);
```
--------------------------------------------------------------------------------
/packages/node-sdk/test/config.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, expect, it } from "vitest";
import { loadConfig } from "../src/config";
describe("config tests", () => {
it("should load config file", () => {
const config = loadConfig("test/testConfig.json");
expect(config).toEqual({
flagOverrides: {
myFlag: {
isEnabled: true,
},
myFlagFalse: false,
myFlagWithConfig: {
isEnabled: true,
config: {
key: "config-1",
payload: { something: "else" },
},
},
},
secretKey: "mySecretKey",
offline: true,
apiBaseUrl: "http://localhost:3000",
});
});
it("should load ENV VARS", () => {
process.env.REFLAG_SECRET_KEY = "mySecretKeyFromEnv";
process.env.REFLAG_OFFLINE = "true";
process.env.REFLAG_API_BASE_URL = "http://localhost:4999";
process.env.REFLAG_FLAGS_ENABLED = "myNewFlag";
process.env.REFLAG_FLAGS_DISABLED = "myNewFlagFalse";
const config = loadConfig("test/testConfig.json");
expect(config).toEqual({
flagOverrides: {
myFlag: {
isEnabled: true,
},
myFlagFalse: false,
myNewFlag: true,
myNewFlagFalse: false,
myFlagWithConfig: {
isEnabled: true,
config: {
key: "config-1",
payload: { something: "else" },
},
},
},
secretKey: "mySecretKeyFromEnv",
offline: true,
apiBaseUrl: "http://localhost:4999",
});
});
});
```
--------------------------------------------------------------------------------
/packages/cli/stores/auth.ts:
--------------------------------------------------------------------------------
```typescript
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { AUTH_FILE } from "../utils/constants.js";
class AuthStore {
protected tokens: Map<string, string> = new Map();
protected apiKey: string | undefined;
async initialize() {
await this.loadTokenFile();
}
protected async loadTokenFile() {
try {
const content = await readFile(AUTH_FILE, "utf-8");
this.tokens = new Map(
content
.split("\n")
.filter(Boolean)
.map((line) => {
const [baseUrl, token] = line.split("|");
return [baseUrl, token];
}),
);
} catch {
// No tokens file found
}
}
protected async saveTokenFile(newTokens: Map<string, string>) {
const content = Array.from(newTokens.entries())
.map(([baseUrl, token]) => `${baseUrl}|${token}`)
.join("\n");
await mkdir(dirname(AUTH_FILE), { recursive: true });
await writeFile(AUTH_FILE, content);
this.tokens = newTokens;
}
getToken(baseUrl: string) {
return {
token: this.apiKey || this.tokens.get(baseUrl),
isApiKey: !!this.apiKey,
};
}
async setToken(baseUrl: string, newToken: string | null) {
if (newToken) {
this.tokens.set(baseUrl, newToken);
} else {
this.tokens.delete(baseUrl);
}
await this.saveTokenFile(this.tokens);
}
useApiKey(key: string) {
this.apiKey = key;
}
}
export const authStore = new AuthStore();
```
--------------------------------------------------------------------------------
/packages/vue-sdk/src/ReflagProvider.vue:
--------------------------------------------------------------------------------
```vue
<script setup lang="ts">
import { computed, onMounted, provide, ref, watch } from "vue";
import { ProviderSymbol, useOnEvent, useReflagClient } from "./hooks";
import type { ReflagProps } from "./types";
// any optional prop which has boolean as part of the type, will default to false
// instead of `undefined`, so we use `withDefaults` here to pass the undefined
// down into the client.
const {
context,
user,
company,
otherContext,
initialLoading = true,
enableTracking = true,
debug,
...config
} = defineProps<ReflagProps>();
const resolvedContext = computed(() => ({
user,
company,
other: otherContext,
...context,
}));
const client = useReflagClient(
{
...config,
...resolvedContext.value,
enableTracking,
},
debug,
);
const isLoading = ref(
client.getState() !== "initialized" ? initialLoading : false,
);
useOnEvent(
"stateUpdated",
(state) => {
isLoading.value = state === "initializing";
},
client,
);
// Initialize the client if it is not already initialized
onMounted(() => {
if (client.getState() !== "idle") return;
void client.initialize().catch((e) => {
client.logger.error("failed to initialize client", e);
});
});
// Update the context if it changes
watch(
() => resolvedContext,
() => {
void client.setContext(resolvedContext.value);
},
{ deep: true },
);
provide(ProviderSymbol, {
isLoading,
client,
});
</script>
<template>
<slot v-if="isLoading && $slots.loading" name="loading" />
<slot v-else />
</template>
```
--------------------------------------------------------------------------------
/packages/cli/utils/schemas.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
export const sortTypeSchema = z
.enum(["flat", "hierarchical"])
.describe("Type of sorting to apply");
export const booleanish = z.preprocess((value) => {
if (typeof value === "string") {
return value === "true" || value === "1";
}
return Boolean(value);
}, z.boolean().describe("Boolean value that can be parsed from strings like 'true' or '1'")) as z.ZodEffects<
z.ZodBoolean,
boolean,
boolean
>;
export const PaginationQueryBaseSchema = (
{
sortOrder = "asc",
pageIndex = 0,
pageSize = 20,
}: {
sortOrder?: "asc" | "desc";
pageIndex?: number;
pageSize?: number;
} = {
sortOrder: "asc",
pageIndex: 0,
pageSize: 20,
},
) =>
z.object({
sortOrder: z
.enum(["asc", "desc"])
.default(sortOrder)
.describe("Sort direction (ascending or descending)"),
pageIndex: z.coerce
.number()
.int()
.nonnegative()
.default(pageIndex)
.describe("Zero-based page index"),
pageSize: z.coerce
.number()
.int()
.nonnegative()
.min(1)
.max(100)
.default(pageSize)
.describe("Number of items per page (1-100)"),
});
export const EnvironmentQuerySchema = z
.object({
envId: z.string().min(1).describe("Environment identifier"),
})
.strict();
export type EnvironmentQuery = z.infer<typeof EnvironmentQuerySchema>;
export const ExternalIdSchema = z
.string()
.nonempty()
.max(256)
.describe("External identifier, non-empty string up to 256 characters");
```
--------------------------------------------------------------------------------
/packages/node-sdk/examples/cloudflare-worker/src/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* This is a simple example of how to use the Reflag SDK in a Cloudflare Worker.
* It demonstrates how to initialize the client and evaluate flags.
* It also shows how to flush the client and wait for any in-flight requests to complete.
*
* Set the REFLAG_SECRET_KEY environment variable in wrangler.jsonc to get started.
*
* - Run `yarn run dev` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `yarn run deploy` to publish your worker
*
*/
import { EdgeClient } from "../../../";
// set the REFLAG_SECRET_KEY environment variable or pass the secret key in the constructor
const reflag = new EdgeClient();
export default {
async fetch(request, _env, ctx): Promise<Response> {
// initialize the client and wait for it to complete
// this is not required for the edge client, but is included for completeness
await reflag.initialize();
const url = new URL(request.url);
const userId = url.searchParams.get("user.id");
const companyId = url.searchParams.get("company.id");
const f = reflag.getFlags({
user: { id: userId ?? undefined },
company: { id: companyId ?? undefined },
});
// ensure all events are flushed and any requests to refresh the feature cache
// have completed after the response is sent
ctx.waitUntil(reflag.flush());
return new Response(
`Flags for user ${userId} and company ${companyId}: ${JSON.stringify(f, null, 2)}`,
);
},
} satisfies ExportedHandler<Env>;
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
name: Publish updated packages
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Setup .npmrc file to publish to npm
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
cache-dependency-path: "**/yarn.lock"
registry-url: "https://registry.npmjs.org"
scope: "@reflag"
- name: Install dependencies
run: yarn install --immutable
- name: Build packages
run: yarn build
- name: npm login
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc
env:
NPM_TOKEN: ${{ secrets.REFLAG_NPM_TOKEN }}
- name: Publish
run: yarn lerna publish from-package --no-private --yes
- name: Build docs
run: yarn docs
- name: Checkout docs with SSH
uses: actions/checkout@v3
with:
repository: reflagcom/docs
ssh-key: ${{ secrets.DOCS_DEPLOY_KEY }}
path: reflag-docs
- name: Copy generated docs to docs repo
run: |
rm -rf reflag-docs/sdk
cp -R dist/docs reflag-docs/sdk
- name: Commit and push changes
run: |
cd reflag-docs
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@reflag.com"
git add sdk
git commit -m "Update documentation" && git push || echo "No docs changes to commit"
```
--------------------------------------------------------------------------------
/packages/node-sdk/examples/cloudflare-worker/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2021",
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
"lib": ["es2021"],
/* Specify what JSX code is generated. */
"jsx": "react-jsx",
/* Specify what module code is generated. */
"module": "es2022",
/* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "Bundler",
/* Enable importing .json files */
"resolveJsonModule": true,
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"allowJs": true,
/* Enable error reporting in type-checked JavaScript files. */
"checkJs": false,
/* Disable emitting files from a compilation. */
"noEmit": true,
/* Ensure that each file can be safely transpiled without relying on other imports. */
"isolatedModules": true,
/* Allow 'import x from y' when a module doesn't have a default export. */
"allowSyntheticDefaultImports": true,
/* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true,
/* Enable all strict type-checking options. */
"strict": true,
/* Skip type checking all .d.ts files. */
"skipLibCheck": true,
"types": ["./worker-configuration.d.ts"]
},
"exclude": ["test"],
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
}
```
--------------------------------------------------------------------------------
/packages/openfeature-node-provider/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@reflag/openfeature-node-provider",
"version": "1.0.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reflagcom/javascript.git"
},
"scripts": {
"dev": "vite",
"start": "vite",
"build": "tsc --project tsconfig.build.json",
"test": "vitest -c vite.config.js",
"test:ci": "vitest run -c vite.config.js --reporter=default --reporter=junit --outputFile=junit.xml",
"coverage": "vitest run --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn vitest run -c vite.config.js && yarn build"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"main": "./dist/index.js",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"require": "./dist/index.js"
}
},
"devDependencies": {
"@babel/core": "~7.24.7",
"@openfeature/core": "^1.5.0",
"@openfeature/server-sdk": ">=1.16.1",
"@reflag/eslint-config": "~0.0.2",
"@reflag/tsconfig": "~0.0.2",
"@types/node": "^22.12.0",
"eslint": "^9.21.0",
"flush-promises": "~1.0.2",
"prettier": "^3.5.2",
"ts-node": "~10.9.2",
"typescript": "^5.7.3",
"vite": "~5.4.18",
"vite-plugin-dts": "~3.9.1",
"vitest": "~1.6.0"
},
"dependencies": {
"@reflag/node-sdk": "1.1.0"
},
"peerDependencies": {
"@openfeature/server-sdk": ">=1.16.1"
}
}
```
--------------------------------------------------------------------------------
/packages/node-sdk/src/rate-limiter.ts:
--------------------------------------------------------------------------------
```typescript
import { ok } from "./utils";
/**
* Creates a new rate limiter.
*
* @typeparam TKey - The type of the key.
* @param windowSizeMs - The length of the time window in milliseconds.
*
* @returns The rate limiter.
**/
export function newRateLimiter(windowSizeMs: number) {
ok(
typeof windowSizeMs == "number" && windowSizeMs > 0,
"windowSizeMs must be greater than 0",
);
let lastAllowedTimestampsByKey: { [key: string]: number } = {};
function clearStale(all: boolean = false): void {
if (all) {
lastAllowedTimestampsByKey = {};
}
const expireBeforeTimestamp = Date.now() - windowSizeMs;
const keys = Object.keys(lastAllowedTimestampsByKey);
for (const key of keys) {
const lastAllowedTimestamp = lastAllowedTimestampsByKey[key];
if (
lastAllowedTimestamp &&
lastAllowedTimestamp < expireBeforeTimestamp
) {
delete lastAllowedTimestampsByKey[key];
}
}
}
function isAllowed(key: string): boolean {
const now = Date.now();
// every ~100 calls, remove all stale items from the cache.
//
// we previously used a fixed time interval here, but setTimeout
// is not available in serverless runtimes.
if (Math.random() < 0.01) {
clearStale();
}
const lastAllowedTimestamp = lastAllowedTimestampsByKey[key];
if (lastAllowedTimestamp && lastAllowedTimestamp >= now - windowSizeMs) {
return false;
}
lastAllowedTimestampsByKey[key] = now;
return true;
}
return {
clearStale,
isAllowed,
cacheSize: () => Object.keys(lastAllowedTimestampsByKey).length,
};
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/utils.ts:
--------------------------------------------------------------------------------
```typescript
import { propagatedEvents } from "./constants";
import { Offset } from "./types";
function stopPropagation(e: Event) {
e.stopPropagation();
}
export function attachContainer(containerId: string) {
let container = document.querySelector(`#${containerId}`);
if (!container) {
container = document.createElement("div");
container.attachShadow({ mode: "open" });
(container as HTMLElement).style.all = "initial";
container.id = containerId;
document.body.appendChild(container);
for (const event of propagatedEvents) {
container.addEventListener(event, stopPropagation, { passive: true });
}
}
return container.shadowRoot!;
}
function parseOffset(offsetInput?: Offset["x"] | Offset["y"]) {
if (offsetInput === undefined) return "1rem";
if (typeof offsetInput === "number") return offsetInput + "px";
return offsetInput;
}
export function parseUnanchoredPosition(position: {
offset?: Offset;
placement: string;
}) {
const offsetY = parseOffset(position.offset?.y);
const offsetX = parseOffset(position.offset?.x);
switch (position.placement) {
case "top-left":
return {
top: offsetY,
left: offsetX,
};
case "top-right":
return {
top: offsetY,
right: offsetX,
};
case "bottom-left":
return {
bottom: offsetY,
left: offsetX,
};
case "bottom-right":
return {
bottom: offsetY,
right: offsetX,
};
default:
console.error("[Reflag]", "Invalid placement", position.placement);
return parseUnanchoredPosition({ placement: "bottom-right" });
}
}
```
--------------------------------------------------------------------------------
/packages/openfeature-browser-provider/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@reflag/openfeature-browser-provider",
"version": "1.1.0",
"packageManager": "[email protected]",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reflagcom/javascript.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "vite",
"build": "tsc --project tsconfig.build.json && vite build",
"test": "vitest",
"test:ci": "vitest run --reporter=default --reporter=junit --outputFile=junit.xml",
"coverage": "vitest run --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn vitest run && yarn build"
},
"files": [
"dist"
],
"main": "./dist/reflag-openfeature-browser-provider.umd.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/reflag-openfeature-browser-provider.mjs",
"require": "./dist/reflag-openfeature-browser-provider.umd.js"
}
},
"dependencies": {
"@reflag/browser-sdk": "1.2.0"
},
"devDependencies": {
"@openfeature/core": "1.5.0",
"@openfeature/web-sdk": "^1.3.0",
"@reflag/eslint-config": "0.0.2",
"@reflag/tsconfig": "0.0.2",
"@types/node": "^22.12.0",
"eslint": "^9.21.0",
"jsdom": "^24.1.0",
"prettier": "^3.5.2",
"typescript": "^5.7.3",
"vite": "^5.3.5",
"vite-plugin-dts": "^4.0.0-beta.1",
"vitest": "^2.0.4"
},
"peerDependencies": {
"@openfeature/web-sdk": ">=1.3"
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/types.ts:
--------------------------------------------------------------------------------
```typescript
import { Position } from "../../ui/types";
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
export interface FeedbackSubmission {
question: string;
feedbackId?: string;
score: number;
comment: string;
}
export interface FeedbackScoreSubmission {
feedbackId?: string;
question: string;
score: number;
}
export interface OnScoreSubmitResult {
feedbackId: string;
}
export interface OpenFeedbackFormOptions {
key: string;
title?: string;
/**
* Control the placement and behavior of the feedback form.
*/
position?: Position;
/**
* Add your own custom translations for the feedback form.
* Undefined translation keys fall back to english defaults.
*/
translations?: Partial<FeedbackTranslations>;
/**
* Open the form with both the score and comment fields visible.
* Defaults to `false`
*/
openWithCommentVisible?: boolean;
onSubmit: (data: FeedbackSubmission) => Promise<void> | void;
onScoreSubmit?: (
data: FeedbackScoreSubmission,
) => Promise<OnScoreSubmitResult>;
onClose?: () => void;
onDismiss?: () => void;
}
/**
* You can use this to override text values in the feedback form
* with desired language translation
*/
export type FeedbackTranslations = {
/**
*
*/
DefaultQuestionLabel: string;
QuestionPlaceholder: string;
ScoreStatusDescription: string;
ScoreStatusLoading: string;
ScoreStatusReceived: string;
ScoreVeryDissatisfiedLabel: string;
ScoreDissatisfiedLabel: string;
ScoreNeutralLabel: string;
ScoreSatisfiedLabel: string;
ScoreVerySatisfiedLabel: string;
SuccessMessage: string;
SendButton: string;
};
```
--------------------------------------------------------------------------------
/packages/cli/utils/version.ts:
--------------------------------------------------------------------------------
```typescript
import { readFile } from "fs/promises";
import { join } from "path";
import { gt } from "semver";
import { MODULE_ROOT } from "./constants.js";
export async function current() {
try {
const packageJsonPath = join(MODULE_ROOT, "package.json");
const packageJsonContent = await readFile(packageJsonPath, "utf-8");
const packageInfo: {
version: string;
name: string;
} = JSON.parse(packageJsonContent);
return {
version: packageInfo.version,
name: packageInfo.name,
};
} catch (error) {
throw new Error(
`Failed to read current version: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
async function getLatestVersionFromNpm(packageName: string): Promise<string> {
try {
const response = await fetch(`https://registry.npmjs.org/${packageName}`, {
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
throw new Error(
`Failed to fetch package info: ${response.status} ${response.statusText}`,
);
}
const data: {
"dist-tags": {
latest: string;
};
} = await response.json();
return data["dist-tags"].latest;
} catch (error) {
throw new Error(
`Failed to fetch latest version from npm: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
export async function checkLatest() {
const { version: currentVersion, name: packageName } = await current();
const latestVersion = await getLatestVersionFromNpm(packageName);
const isNewerAvailable = gt(latestVersion, currentVersion);
return {
currentVersion,
latestVersion,
isNewerAvailable,
};
}
```
--------------------------------------------------------------------------------
/.github/workflows/package-ci.yml:
--------------------------------------------------------------------------------
```yaml
name: "Package CI"
permissions:
statuses: write
checks: write
contents: read
on: [push]
jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-22.04
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
cache-dependency-path: "**/yarn.lock"
- name: Restore Node.js dependencies
run: yarn install --immutable
- name: Restore package.json
# This step is necessary because the previous step may have updated package.json
run: git checkout -- package.json packages/*/package.json
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
working-directory: ./packages/browser-sdk
- id: build
name: Build the project
run: yarn build
- name: Build docs
run: yarn docs
- id: test
name: Test the project
run: yarn test:ci
- name: Report test results
uses: dorny/[email protected]
with:
name: Build & Test Report
path: ./packages/*/junit.xml
reporter: jest-junit
- id: prettier
name: Check styling
run: yarn prettier
- id: lint
name: Check for linting errors
run: yarn lint:ci
- name: Annotate from ESLint report
uses: ataylorme/eslint-annotate-action@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
report-json: ./packages/*/eslint-report.json
fail-on-warning: true
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/toolbar/Flags.css:
--------------------------------------------------------------------------------
```css
.search-input {
background: transparent;
border: none;
color: white;
width: 100%;
font-size: var(--text-size);
height: 28px;
&::placeholder {
color: var(--gray500);
}
&::-webkit-search-cancel-button {
-webkit-appearance: none;
display: inline-block;
width: 8px;
height: 8px;
margin-left: 10px;
background:
linear-gradient(
45deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) 43%,
#fff 45%,
#fff 55%,
rgba(0, 0, 0, 0) 57%,
rgba(0, 0, 0, 0) 100%
),
linear-gradient(
135deg,
transparent 0%,
transparent 43%,
#fff 45%,
#fff 55%,
transparent 57%,
transparent 100%
);
cursor: pointer;
}
}
.flags-table {
width: 100%;
border-collapse: collapse;
}
.flag-row {
&.not-visible {
visibility: hidden;
}
}
.flags-table-empty {
position: absolute;
top: 0;
left: 0;
right: 0;
color: var(--gray500);
padding: 12px 12px;
line-height: 1.5;
}
.flag-name-cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: auto;
padding: 6px 6px 6px 0;
display: flex;
align-items: center;
gap: 8px;
.flag-icon {
height: 15px;
width: 15px;
color: var(--dimmed-color);
}
}
.flag-link {
color: var(--text-color);
text-decoration: none;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
.flag-reset-cell {
width: 32px;
padding: 6px 0;
text-align: right;
}
.reset {
color: var(--gray500);
text-decoration: none;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
.flag-switch-cell {
padding: 6px 0 6px 6px;
width: 0;
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/prompts.ts:
--------------------------------------------------------------------------------
```typescript
import { FeedbackPrompt } from "./feedback";
import {
checkPromptMessageCompleted,
markPromptMessageCompleted,
} from "./promptStorage";
export const parsePromptMessage = (
message: any,
): FeedbackPrompt | undefined => {
if (
typeof message?.question !== "string" ||
!message.question.length ||
typeof message.showAfter !== "number" ||
typeof message.showBefore !== "number" ||
typeof message.promptId !== "string" ||
!message.promptId.length ||
typeof message.featureId !== "string" ||
!message.featureId.length
) {
return undefined;
} else {
return {
question: message.question,
showAfter: new Date(message.showAfter),
showBefore: new Date(message.showBefore),
promptId: message.promptId,
featureId: message.featureId,
};
}
};
export type FeedbackPromptCompletionHandler = () => void;
export type FeedbackPromptDisplayHandler = (
userId: string,
prompt: FeedbackPrompt,
completionHandler: FeedbackPromptCompletionHandler,
) => void;
export const processPromptMessage = (
userId: string,
prompt: FeedbackPrompt,
displayHandler: FeedbackPromptDisplayHandler,
) => {
const now = new Date();
const completionHandler = () => {
markPromptMessageCompleted(userId, prompt.promptId, prompt.showBefore);
};
if (checkPromptMessageCompleted(userId, prompt.promptId)) {
return false;
} else if (now > prompt.showBefore) {
return false;
} else if (now < prompt.showAfter) {
setTimeout(() => {
displayHandler(userId, prompt, completionHandler);
}, prompt.showAfter.getTime() - now.getTime());
return true;
} else {
displayHandler(userId, prompt, completionHandler);
return true;
}
};
```
--------------------------------------------------------------------------------
/packages/browser-sdk/test/flagCache.test.ts:
--------------------------------------------------------------------------------
```typescript
import {
afterAll,
beforeEach,
describe,
expect,
test,
vi,
vitest,
} from "vitest";
import { CacheResult, FlagCache } from "../src/flag/flagCache";
beforeEach(() => {
vi.useFakeTimers();
vi.resetAllMocks();
});
afterAll(() => {
vi.useRealTimers();
});
export const TEST_STALE_MS = 1000;
export const TEST_EXPIRE_MS = 2000;
export function newCache(): {
cache: FlagCache;
cacheItem: (string | null)[];
} {
const cacheItem: (string | null)[] = [null];
return {
cache: new FlagCache({
storage: {
get: () => cacheItem[0],
set: (value) => (cacheItem[0] = value),
},
staleTimeMs: TEST_STALE_MS,
expireTimeMs: TEST_EXPIRE_MS,
}),
cacheItem,
};
}
describe("cache", () => {
const flags = {
flagA: { isEnabled: true, key: "flagA", targetingVersion: 1 },
};
test("caches items", async () => {
const { cache } = newCache();
cache.set("key", { flags });
expect(cache.get("key")).toEqual({
stale: false,
flags,
} satisfies CacheResult);
});
test("sets stale", async () => {
const { cache } = newCache();
cache.set("key", { flags });
vitest.advanceTimersByTime(TEST_STALE_MS + 1);
const cacheItem = cache.get("key");
expect(cacheItem?.stale).toBe(true);
});
test("expires on set", async () => {
const { cache, cacheItem } = newCache();
cache.set("first key", {
flags,
});
expect(cacheItem[0]).not.toBeNull();
vitest.advanceTimersByTime(TEST_EXPIRE_MS + 1);
cache.set("other key", {
flags,
});
const item = cache.get("key");
expect(item).toBeUndefined();
expect(cacheItem[0]).not.toContain("first key"); // should have been removed
});
});
```
--------------------------------------------------------------------------------
/packages/node-sdk/src/inRequestCache.ts:
--------------------------------------------------------------------------------
```typescript
// This is a cache that is updated as part of the request/response cycle.
// This is useful in serverless runtimes where `setTimeout` doesn't exist or does something useless.
import { Logger } from "./types";
export default function inRequestCache<T>(
ttl: number,
logger: Logger | undefined,
fn: () => Promise<T | undefined>,
) {
let value: T | undefined = undefined;
let lastFetch = 0;
let fetching: Promise<void> | null = null;
async function refresh(): Promise<T | undefined> {
if (!fetching) {
fetching = (async () => {
try {
const result = await fn();
logger?.debug("inRequestCache: fetched value", result);
if (result !== undefined) {
value = result;
}
} catch (err) {
if (logger) {
logger.error?.("inRequestCache: error refreshing value", err);
}
} finally {
lastFetch = Date.now();
fetching = null;
}
})();
}
await fetching;
return value;
}
const waitRefresh = async () => {
if (fetching) await fetching;
};
const destroy = () => {
fetching = null;
value = undefined;
lastFetch = 0;
};
return {
get(): T | undefined {
const now = Date.now();
// If value is undefined, just return undefined
if (value === undefined) {
return undefined;
}
// If value is stale, trigger background refresh
if (now - lastFetch > ttl) {
logger?.debug(
"inRequestCache: stale value, triggering background refresh",
);
void refresh();
}
return value;
},
async refresh(): Promise<T | undefined> {
return await refresh();
},
waitRefresh,
destroy,
};
}
```
--------------------------------------------------------------------------------
/packages/vue-sdk/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@reflag/vue-sdk",
"version": "1.2.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reflagcom/javascript.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "vite",
"build": "tsc --project tsconfig.build.json && vite build",
"test": "vitest run",
"test:watch": "vitest",
"test:ci": "yarn test --reporter=default --reporter=junit --outputFile=junit.xml",
"coverage": "yarn test --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn test && yarn build"
},
"files": [
"dist"
],
"main": "./dist/reflag-vue-sdk.umd.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/reflag-vue-sdk.mjs",
"require": "./dist/reflag-vue-sdk.umd.js",
"types": "./dist/index.d.ts"
}
},
"dependencies": {
"@reflag/browser-sdk": "1.2.0"
},
"peerDependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"@reflag/eslint-config": "^0.0.2",
"@reflag/tsconfig": "^0.0.2",
"@types/jsdom": "^21.1.6",
"@types/node": "^22.12.0",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/test-utils": "^2.3.2",
"eslint": "^9.21.0",
"eslint-plugin-vue": "^9.28.0",
"jsdom": "^24.1.0",
"msw": "^2.3.5",
"prettier": "^3.5.2",
"rollup": "^4.2.0",
"rollup-preserve-directives": "^1.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"vite": "^5.0.13",
"vite-plugin-dts": "^4.5.4",
"vitest": "^2.0.4",
"vue": "^3.5.16",
"vue-eslint-parser": "^9.4.2"
}
}
```
--------------------------------------------------------------------------------
/packages/cli/commands/auth.ts:
--------------------------------------------------------------------------------
```typescript
import chalk from "chalk";
import { Command } from "commander";
import ora from "ora";
import { authStore } from "../stores/auth.js";
import { configStore } from "../stores/config.js";
import { waitForAccessToken } from "../utils/auth.js";
import { handleError } from "../utils/errors.js";
export const loginAction = async () => {
const { baseUrl, apiUrl } = configStore.getConfig();
const { token, isApiKey } = authStore.getToken(baseUrl);
if (isApiKey) {
handleError(
"Login is not allowed when an API token was supplied.",
"Login",
);
}
if (token) {
console.log("Already logged in, nothing to do.");
return;
}
try {
const { accessToken } = await waitForAccessToken(baseUrl, apiUrl);
await authStore.setToken(baseUrl, accessToken);
console.log(`Logged in to ${chalk.cyan(baseUrl)} successfully!`);
} catch (error) {
console.error("Login failed.");
handleError(error, "Login");
}
};
export const logoutAction = async () => {
const baseUrl = configStore.getConfig("baseUrl");
const { token, isApiKey } = authStore.getToken(baseUrl);
if (isApiKey) {
handleError(
"Logout is not allowed when an API token was supplied.",
"Logout",
);
}
if (!token) {
console.log("Not logged in, nothing to do.");
return;
}
const spinner = ora("Logging out...").start();
try {
await authStore.setToken(baseUrl, null);
spinner.succeed("Logged out successfully!");
} catch (error) {
spinner.fail("Logout failed.");
handleError(error, "Logout");
}
};
export function registerAuthCommands(cli: Command) {
cli.command("login").description("Login to Reflag.").action(loginAction);
cli.command("logout").description("Logout from Reflag.").action(logoutAction);
}
```
--------------------------------------------------------------------------------
/packages/cli/services/bootstrap.ts:
--------------------------------------------------------------------------------
```typescript
import { authRequest } from "../utils/auth.js";
import { KeyFormat } from "../utils/gen.js";
export type Environment = {
id: string;
name: string;
isProduction: boolean;
order: number;
};
export type App = {
id: string;
name: string;
demo: boolean;
environments: Environment[];
};
export type ReflagUser = {
id: string;
email: string;
name: string;
};
export type Org = {
id: string;
name: string;
apps: App[];
featureKeyFormat: KeyFormat;
};
export type BootstrapResponse = {
org: Org;
user: ReflagUser;
};
let bootstrapResponse: BootstrapResponse | null = null;
export async function bootstrap(): Promise<BootstrapResponse> {
if (!bootstrapResponse) {
bootstrapResponse = await authRequest<BootstrapResponse>(`/bootstrap`);
}
return bootstrapResponse;
}
export function getOrg(): Org {
if (!bootstrapResponse) {
throw new Error("CLI has not been bootstrapped.");
}
if (!bootstrapResponse.org) {
throw new Error("No organization found.");
}
return bootstrapResponse.org;
}
export function listApps(): App[] {
if (!bootstrapResponse) {
throw new Error("CLI has not been bootstrapped.");
}
const org = bootstrapResponse.org;
if (!org) {
throw new Error("No organization found.");
}
if (!org.apps?.length) {
throw new Error("No apps found.");
}
return bootstrapResponse.org.apps;
}
export function getApp(id: string): App {
const apps = listApps();
const app = apps.find((a) => a.id === id);
if (!app) {
throw new Error(`App with id ${id} not found`);
}
return app;
}
export function getReflagUser(): ReflagUser {
if (!bootstrapResponse) {
throw new Error("CLI has not been bootstrapped.");
}
if (!bootstrapResponse.user) {
throw new Error("No user found.");
}
return bootstrapResponse.user;
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/icons/Dissatisfied.tsx:
--------------------------------------------------------------------------------
```typescript
import { FunctionComponent, h } from "preact";
export const Dissatisfied: FunctionComponent<
h.JSX.SVGAttributes<SVGSVGElement>
> = (props) => (
<svg
fill="none"
height="22"
viewBox="0 0 24 24"
width="22"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22ZM12 20C14.1217 20 16.1566 19.1571 17.6569 17.6569C19.1571 16.1566 20 14.1217 20 12C20 9.87827 19.1571 7.84344 17.6569 6.34315C16.1566 4.84285 14.1217 4 12 4C9.87827 4 7.84344 4.84285 6.34315 6.34315C4.84285 7.84344 4 9.87827 4 12C4 14.1217 4.84285 16.1566 6.34315 17.6569C7.84344 19.1571 9.87827 20 12 20Z"
fill="currentColor"
/>
<path
d="M12 15C13.1835 15 14.2712 15.4113 15.1279 16.0991C15.4665 16.3709 15.4398 16.8738 15.1187 17.166C14.8962 17.3685 14.5788 17.4225 14.2915 17.3332C13.605 17.1198 12.8259 17 12 17C11.1741 17 10.3949 17.1205 9.70841 17.3331C9.42116 17.422 9.104 17.3677 8.88161 17.1653C8.56025 16.8728 8.53318 16.3695 8.87206 16.0976C9.2087 15.8274 9.57986 15.6014 9.97666 15.426C10.6139 15.1442 11.3032 14.9991 12 15ZM8.5 10C8.89783 10 9.27936 10.158 9.56066 10.4393C9.84197 10.7206 10 11.1022 10 11.5C10 11.8978 9.84197 12.2794 9.56066 12.5607C9.27936 12.842 8.89783 13 8.5 13C8.10218 13 7.72065 12.842 7.43934 12.5607C7.15804 12.2794 7 11.8978 7 11.5C7 11.1022 7.15804 10.7206 7.43934 10.4393C7.72065 10.158 8.10218 10 8.5 10ZM15.5 10C15.8978 10 16.2794 10.158 16.5607 10.4393C16.842 10.7206 17 11.1022 17 11.5C17 11.8978 16.842 12.2794 16.5607 12.5607C16.2794 12.842 15.8978 13 15.5 13C15.1022 13 14.7206 12.842 14.4393 12.5607C14.158 12.2794 14 11.8978 14 11.5C14 11.1022 14.158 10.7206 14.4393 10.4393C14.7206 10.158 15.1022 10 15.5 10Z"
fill="currentColor"
/>
</svg>
);
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/icons/Satisfied.tsx:
--------------------------------------------------------------------------------
```typescript
import { FunctionComponent, h } from "preact";
export const Satisfied: FunctionComponent<
h.JSX.SVGAttributes<SVGSVGElement>
> = (props) => (
<svg
fill="none"
height="22"
viewBox="0 0 24 24"
width="22"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22ZM12 20C14.1217 20 16.1566 19.1571 17.6569 17.6569C19.1571 16.1566 20 14.1217 20 12C20 9.87827 19.1571 7.84344 17.6569 6.34315C16.1566 4.84285 14.1217 4 12 4C9.87827 4 7.84344 4.84285 6.34315 6.34315C4.84285 7.84344 4 9.87827 4 12C4 14.1217 4.84285 16.1566 6.34315 17.6569C7.84344 19.1571 9.87827 20 12 20ZM8 11C7.60217 11 7.22064 10.842 6.93934 10.5607C6.65803 10.2794 6.5 9.89782 6.5 9.5C6.5 9.10217 6.65803 8.72064 6.93934 8.43934C7.22064 8.15803 7.60217 8 8 8C8.39782 8 8.77935 8.15803 9.06066 8.43934C9.34196 8.72064 9.5 9.10217 9.5 9.5C9.5 9.89782 9.34196 10.2794 9.06066 10.5607C8.77935 10.842 8.39782 11 8 11ZM16 11C15.6022 11 15.2206 10.842 14.9393 10.5607C14.658 10.2794 14.5 9.89782 14.5 9.5C14.5 9.10217 14.658 8.72064 14.9393 8.43934C15.2206 8.15803 15.6022 8 16 8C16.3978 8 16.7794 8.15803 17.0607 8.43934C17.342 8.72064 17.5 9.10217 17.5 9.5C17.5 9.89782 17.342 10.2794 17.0607 10.5607C16.7794 10.842 16.3978 11 16 11Z"
fill="currentColor"
/>
<path
d="M7.79862 15.4322C8.85269 16.5065 10.4964 17.4971 11.9993 17.4971C13.5011 17.4971 15.1701 16.5079 16.2097 15.4351C16.5083 15.1269 16.4581 14.6416 16.1408 14.3528C15.9042 14.1375 15.5656 14.0777 15.2972 14.2517C14.5161 14.7578 13.4271 15.7002 11.9993 15.7002C10.5688 15.7002 9.47831 14.7549 8.69694 14.2486C8.43116 14.0764 8.09564 14.1353 7.86141 14.3485C7.5435 14.6378 7.49757 15.1254 7.79862 15.4322Z"
fill="currentColor"
/>
</svg>
);
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/index.ts:
--------------------------------------------------------------------------------
```typescript
import { h, render } from "preact";
import { feedbackContainerId, propagatedEvents } from "../../ui/constants";
import { Position } from "../../ui/types";
import { FeedbackDialog } from "./FeedbackDialog";
import { OpenFeedbackFormOptions } from "./types";
export const DEFAULT_POSITION: Position = {
type: "DIALOG",
placement: "bottom-right",
};
function stopPropagation(e: Event) {
e.stopPropagation();
}
function attachDialogContainer() {
let container = document.querySelector(`#${feedbackContainerId}`);
if (!container) {
container = document.createElement("div");
container.attachShadow({ mode: "open" });
(container as HTMLElement).style.all = "initial";
container.id = feedbackContainerId;
document.body.appendChild(container);
for (const event of propagatedEvents) {
container.addEventListener(event, stopPropagation, { passive: true });
}
}
return container.shadowRoot!;
}
// this is a counter that increases every time the feedback form is opened
// and since it's passed as a key to the FeedbackDialog component,
// it forces a re-render on every form open
let openInstances = 0;
export function openFeedbackForm(options: OpenFeedbackFormOptions): void {
const shadowRoot = attachDialogContainer();
const position = options.position || DEFAULT_POSITION;
if (position.type === "POPOVER") {
if (!position.anchor) {
console.warn(
"[Reflag]",
"Unable to open popover. Anchor must be a defined DOM-element",
);
return;
}
if (!document.body.contains(position.anchor)) {
console.warn(
"[Reflag]",
"Unable to open popover. Anchor must be an attached DOM-element",
);
return;
}
}
openInstances++;
render(
h(FeedbackDialog, { ...options, position, key: openInstances.toString() }),
shadowRoot,
);
}
```
--------------------------------------------------------------------------------
/packages/react-sdk/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@reflag/react-sdk",
"version": "1.2.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reflagcom/javascript.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "vite",
"build": "tsc --project tsconfig.build.json && vite build",
"test": "vitest run",
"test:watch": "vitest",
"test:ci": "yarn test --reporter=default --reporter=junit --outputFile=junit.xml",
"coverage": "yarn test --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn test && yarn build"
},
"files": [
"dist"
],
"main": "./dist/reflag-react-sdk.umd.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/reflag-react-sdk.mjs",
"require": "./dist/reflag-react-sdk.umd.js",
"types": "./dist/index.d.ts"
}
},
"dependencies": {
"@reflag/browser-sdk": "1.2.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
},
"devDependencies": {
"@reflag/eslint-config": "^0.0.2",
"@reflag/tsconfig": "^0.0.2",
"@testing-library/react": "^15.0.7",
"@types/jsdom": "^21.1.6",
"@types/node": "^22.12.0",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@types/webpack": "^5.28.5",
"eslint": "^9.21.0",
"jsdom": "^24.1.0",
"msw": "^2.3.5",
"prettier": "^3.5.2",
"react": "*",
"react-dom": "*",
"rollup": "^4.2.0",
"rollup-preserve-directives": "^1.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"vite": "^5.0.13",
"vite-plugin-dts": "^4.0.0-beta.1",
"vitest": "^2.0.4",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/icons/VeryDissatisfied.tsx:
--------------------------------------------------------------------------------
```typescript
import { FunctionComponent, h } from "preact";
export const VeryDissatisfied: FunctionComponent<
h.JSX.SVGAttributes<SVGSVGElement>
> = (props) => (
<svg
fill="none"
height="22"
viewBox="0 0 24 24"
width="22"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22ZM12 20C14.1217 20 16.1566 19.1571 17.6569 17.6569C19.1571 16.1566 20 14.1217 20 12C20 9.87827 19.1571 7.84344 17.6569 6.34315C16.1566 4.84285 14.1217 4 12 4C9.87827 4 7.84344 4.84285 6.34315 6.34315C4.84285 7.84344 4 9.87827 4 12C4 14.1217 4.84285 16.1566 6.34315 17.6569C7.84344 19.1571 9.87827 20 12 20ZM8 17C7.44771 17 6.98992 16.5479 7.09965 16.0066C7.29346 15.0506 7.76447 14.1645 8.46447 13.4645C9.40215 12.5268 10.6739 12 12 12C13.3261 12 14.5979 12.5268 15.5355 13.4645C16.2355 14.1645 16.7065 15.0506 16.9003 16.0066C17.0101 16.5479 16.5523 17 16 17V17C15.4477 17 15.0156 16.5403 14.8349 16.0184C14.6877 15.5934 14.4454 15.2028 14.1213 14.8787C13.5587 14.3161 12.7956 14 12 14C11.2043 14 10.4413 14.3161 9.87868 14.8787C9.55459 15.2028 9.31232 15.5934 9.16513 16.0184C8.98442 16.5403 8.55228 17 8 17V17ZM8 11C7.60217 11 7.22064 10.842 6.93934 10.5607C6.65803 10.2794 6.5 9.89782 6.5 9.5C6.5 9.10217 6.65803 8.72064 6.93934 8.43934C7.22064 8.15803 7.60217 8 8 8C8.39782 8 8.77935 8.15803 9.06066 8.43934C9.34196 8.72064 9.5 9.10217 9.5 9.5C9.5 9.89782 9.34196 10.2794 9.06066 10.5607C8.77935 10.842 8.39782 11 8 11ZM16 11C15.6022 11 15.2206 10.842 14.9393 10.5607C14.658 10.2794 14.5 9.89782 14.5 9.5C14.5 9.10217 14.658 8.72064 14.9393 8.43934C15.2206 8.15803 15.6022 8 16 8C16.3978 8 16.7794 8.15803 17.0607 8.43934C17.342 8.72064 17.5 9.10217 17.5 9.5C17.5 9.89782 17.342 10.2794 17.0607 10.5607C16.7794 10.842 16.3978 11 16 11Z"
fill="currentColor"
/>
</svg>
);
```
--------------------------------------------------------------------------------
/packages/cli/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@reflag/cli",
"version": "1.0.4",
"packageManager": "[email protected]",
"description": "CLI for Reflag service",
"main": "./dist/index.js",
"type": "module",
"license": "MIT",
"author": "Reflag.",
"homepage": "https://docs.reflag.com/",
"repository": {
"type": "git",
"url": "https://github.com/reflagcom/javascript.git"
},
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=18.0.0"
},
"bin": {
"reflag": "./dist/index.js"
},
"files": [
"dist",
"schema.json"
],
"exports": {
".": "./dist/index.js"
},
"scripts": {
"build": "tsc && shx chmod +x dist/index.js",
"reflag": "yarn build && ./dist/index.js",
"test": "vitest run",
"test:watch": "vitest",
"test:ci": "yarn test --reporter=default --reporter=junit --outputFile=junit.xml",
"coverage": "vitest run --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn vitest run -c vite.config.js && yarn build"
},
"dependencies": {
"@inquirer/prompts": "^7.9.0",
"ajv": "^8.17.1",
"chalk": "^5.3.0",
"change-case": "^5.4.4",
"commander": "^12.1.0",
"comment-json": "^4.2.5",
"express": "^4.21.2",
"fast-deep-equal": "^3.1.3",
"find-up": "^7.0.0",
"open": "^10.1.0",
"ora": "^8.1.0",
"semver": "^7.7.2",
"slug": "^10.0.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@reflag/eslint-config": "^0.0.2",
"@reflag/tsconfig": "^0.0.2",
"@types/express": "^5.0.0",
"@types/node": "^22.5.1",
"@types/semver": "^7.7.0",
"@types/slug": "^5.0.9",
"eslint": "^9.21.0",
"prettier": "^3.5.2",
"shx": "^0.3.4",
"typescript": "^5.5.4",
"vitest": "^3.0.8"
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/hooksManager.ts:
--------------------------------------------------------------------------------
```typescript
import { CheckEvent, RawFlags } from "./flag/flags";
import { CompanyContext, UserContext } from "./context";
/**
* State of the client.
*/
export type State = "idle" | "initializing" | "initialized" | "stopped";
export interface HookArgs {
stateUpdated: State;
check: CheckEvent;
flagsUpdated: RawFlags;
/**
* @deprecated Use `flagsUpdated` instead.
*/
featuresUpdated: RawFlags;
user: UserContext;
company: CompanyContext;
track: TrackEvent;
}
export type TrackEvent = {
user: UserContext;
company?: CompanyContext;
eventName: string;
attributes?: Record<string, any> | null;
};
/**
* Hooks manager.
* @internal
*/
export class HooksManager {
private hooks: {
stateUpdated: ((arg0: State) => void)[];
check: ((arg0: CheckEvent) => void)[];
flagsUpdated: ((arg0: RawFlags) => void)[];
user: ((arg0: UserContext) => void)[];
company: ((arg0: CompanyContext) => void)[];
track: ((arg0: TrackEvent) => void)[];
} = {
stateUpdated: [],
check: [],
flagsUpdated: [],
user: [],
company: [],
track: [],
};
private _adjustEvent(event: keyof HookArgs) {
return event === "featuresUpdated" ? "flagsUpdated" : event;
}
addHook<THookType extends keyof HookArgs>(
event: THookType,
cb: (arg0: HookArgs[THookType]) => void,
): () => void {
(this.hooks[this._adjustEvent(event)] as any[]).push(cb);
return () => {
this.removeHook(event, cb);
};
}
removeHook<THookType extends keyof HookArgs>(
event: THookType,
cb: (arg0: HookArgs[THookType]) => void,
): void {
this.hooks[this._adjustEvent(event)] = this.hooks[
this._adjustEvent(event)
].filter((hook) => hook !== cb) as any;
}
trigger<THookType extends keyof HookArgs>(
event: THookType,
arg: HookArgs[THookType],
): void {
this.hooks[this._adjustEvent(event)].forEach((hook) => hook(arg as any));
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@reflag/browser-sdk",
"version": "1.2.0",
"packageManager": "[email protected]",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reflagcom/javascript.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "vite",
"build": "tsc --project tsconfig.build.json && vite build",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "yarn build && playwright test",
"test:ci": "yarn test --reporter=default --reporter=junit --outputFile=junit.xml && yarn test:e2e",
"coverage": "yarn test --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
"prettier": "prettier --check .",
"format": "yarn lint --fix && yarn prettier --write",
"preversion": "yarn lint && yarn prettier && yarn test && yarn build"
},
"files": [
"dist"
],
"main": "./dist/reflag-browser-sdk.umd.js",
"types": "./dist/types/src/index.d.ts",
"exports": {
".": {
"import": "./dist/reflag-browser-sdk.mjs",
"require": "./dist/reflag-browser-sdk.umd.js",
"types": "./dist/types/src/index.d.ts"
}
},
"dependencies": {
"@floating-ui/dom": "^1.6.8",
"fast-equals": "^5.2.2",
"js-cookie": "^3.0.5",
"preact": "^10.22.1"
},
"devDependencies": {
"@playwright/test": "^1.49.1",
"@reflag/eslint-config": "0.0.2",
"@reflag/tsconfig": "0.0.2",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.12.0",
"@vitest/coverage-v8": "^2.0.4",
"c8": "~10.1.3",
"eslint": "^9.21.0",
"eslint-config-preact": "^1.5.0",
"http-server": "^14.1.1",
"jsdom": "^24.1.0",
"msw": "^2.3.4",
"nock": "^14.0.1",
"postcss": "^8.4.33",
"postcss-nesting": "^12.0.2",
"postcss-preset-env": "^9.3.0",
"prettier": "^3.5.2",
"typescript": "^5.7.3",
"vite": "^5.3.5",
"vite-plugin-dts": "^4.0.0-beta.1",
"vitest": "^2.0.4"
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/test/httpClient.test.ts:
--------------------------------------------------------------------------------
```typescript
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { HttpClient } from "../src/httpClient";
const cases = [
["https://front.reflag.com", "https://front.reflag.com/path"],
["https://front.reflag.com/", "https://front.reflag.com/path"],
[
"https://front.reflag.com/basepath",
"https://front.reflag.com/basepath/path",
],
];
test.each(cases)(
"url construction with `/path`: %s -> %s",
(base, expected) => {
const client = new HttpClient("publishableKey", { baseUrl: base });
expect(client.getUrl("/path").toString()).toBe(expected);
},
);
test.each(cases)("url construction with `path`: %s -> %s", (base, expected) => {
const client = new HttpClient("publishableKey", { baseUrl: base });
expect(client.getUrl("path").toString()).toBe(expected);
});
describe("sets `credentials`", () => {
beforeEach(() => {
vi.spyOn(global, "fetch").mockResolvedValue(new Response());
});
afterEach(() => {
vi.resetAllMocks();
});
test("default credentials", async () => {
const client = new HttpClient("publishableKey");
await client.get({ path: "/test" });
expect(global.fetch).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({ credentials: undefined }),
);
await client.post({ path: "/test", body: {} });
expect(global.fetch).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({ credentials: undefined }),
);
});
test("custom credentials", async () => {
const client = new HttpClient("publishableKey", { credentials: "include" });
await client.get({ path: "/test" });
expect(global.fetch).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({ credentials: "include" }),
);
await client.post({ path: "/test", body: {} });
expect(global.fetch).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({ credentials: "include" }),
);
});
});
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/FeedbackDialog.css:
--------------------------------------------------------------------------------
```css
.dialog {
position: fixed;
width: 210px;
padding: 16px 22px 10px;
font-size: var(--reflag-feedback-dialog-font-size, 1rem);
font-family: var(
--reflag-feedback-dialog-font-family,
InterVariable,
Inter,
system-ui,
Open Sans,
sans-serif
);
color: var(--reflag-feedback-dialog-color, #1e1f24);
border: 1px solid;
border-color: var(--reflag-feedback-dialog-border, #d8d9df);
border-radius: var(--reflag-feedback-dialog-border-radius, 6px);
box-shadow: var(
--reflag-feedback-dialog-box-shadow,
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05)
);
background-color: var(--reflag-feedback-dialog-background-color, #fff);
z-index: 2147410000;
&:not(.modal) {
margin: unset;
top: unset;
right: unset;
left: unset;
bottom: unset;
}
}
.arrow {
background-color: var(--reflag-feedback-dialog-background-color, #fff);
box-shadow: var(--reflag-feedback-dialog-border, #d8d9df) -1px -1px 1px 0px;
&.bottom {
box-shadow: var(--reflag-feedback-dialog-border, #d8d9df) -1px -1px 1px 0px;
}
&.top {
box-shadow: var(--reflag-feedback-dialog-border, #d8d9df) 1px 1px 1px 0px;
}
&.left {
box-shadow: var(--reflag-feedback-dialog-border, #d8d9df) 1px -1px 1px 0px;
}
&.right {
box-shadow: var(--reflag-feedback-dialog-border, #d8d9df) -1px 1px 1px 0px;
}
}
.close {
position: absolute;
top: 6px;
right: 6px;
width: 28px;
height: 28px;
padding: 0;
margin: 0;
background: none;
border: none;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
color: var(--reflag-feedback-dialog-color, #1e1f24);
svg {
position: absolute;
}
}
.plug {
font-size: 0.75em;
text-align: center;
margin-top: 7px;
width: 100%;
}
.plug a {
opacity: 0.5;
color: var(--reflag-feedback-dialog-color, #1e1f24);
text-decoration: none;
transition: opacity 200ms;
}
.plug a:hover {
opacity: 0.7;
}
```
--------------------------------------------------------------------------------
/docs.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/sh
set -e
typedoc --treatWarningsAsErrors
# We need to fix the links in the generated markdown files.
# Typedoc generates anchors for properties in tables which can collide with anchors for types.
# For example we can have property `logger` and type `Logger` which will both have anchor `logger`.
# typedoc-plugin-markdown will generate links trying to deduplicate the anchors by adding a number at the end.
# Example: `globals.md#logger-1` and `globals.md#logger-2`.
#
# We don't need the anchors for properties in the markdown files
# and they won't even work in Gitbook because they are generated as html <a> anchors.
# We can fix this by removing the number at the end of the anchor.
SEDCOMMAND='s/globals.md#(.*)-[0-9]+/globals.md#\1/g'
# Find all markdown files including globals.md
FILES=$(find dist/docs/@reflag -name "*.md")
echo "Processing markdown files..."
for file in $FILES
do
echo "Processing $file..."
# Fix anchor links in globals.md files
if [[ "$file" == *"globals.md" ]]; then
sed -r "$SEDCOMMAND" "$file" > "$file.fixed"
rm "$file"
mv "$file.fixed" "$file"
fi
# Create a temporary file for processing
tmp_file="${file}.tmp"
# Process NOTE blocks - handle multi-line
awk '
BEGIN { in_block = 0; content = ""; }
/^> \[!NOTE\]/ { in_block = 1; print "{% hint style=\"info\" %}"; next; }
/^> \[!TIP\]/ { in_block = 1; print "{% hint style=\"success\" %}"; next; }
/^> \[!IMPORTANT\]/ { in_block = 1; print "{% hint style=\"warning\" %}"; next; }
/^> \[!WARNING\]/ { in_block = 1; print "{% hint style=\"warning\" %}"; next; }
/^> \[!CAUTION\]/ { in_block = 1; print "{% hint style=\"danger\" %}"; next; }
in_block && /^>/ {
content = content substr($0, 3) "\n";
next;
}
in_block && !/^>/ {
printf "%s", content;
print "{% endhint %}";
in_block = 0;
content = "";
}
!in_block { print; }
' "$file" > "$tmp_file"
mv "$tmp_file" "$file"
done
echo "Processing complete!"
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/context.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Context is a set of key-value pairs.
* This is used to determine if feature targeting matches and to track events.
* Id should always be present so that it can be referenced to an existing company.
*/
export interface CompanyContext {
/**
* Company id
*/
id: string | number | undefined;
/**
* Company name
*/
name?: string | undefined;
/**
* Other company attributes
*/
[key: string]: string | number | undefined;
}
/**
* Context is a set of key-value pairs.
* This is used to determine if feature targeting matches and to track events.
* Id should always be present so that it can be referenced to an existing user.
*/
export interface UserContext {
/**
* User id
*/
id: string | number | undefined;
/**
* User name
*/
name?: string | undefined;
/**
* User email
*/
email?: string | undefined;
/**
* Other user attributes
*/
[key: string]: string | number | undefined;
}
/**
* Context is a set of key-value pairs.
* This is used to determine if feature targeting matches and to track events.
*/
export interface ReflagContext {
/**
* Company related context. If you provide `id` Reflag will enrich the evaluation context with
* company attributes on Reflag servers.
*/
company?: CompanyContext;
/**
* User related context. If you provide `id` Reflag will enrich the evaluation context with
* user attributes on Reflag servers.
*/
user?: UserContext;
/**
* Context which is not related to a user or a company.
*/
other?: Record<string, string | number | undefined>;
}
/**
* @deprecated Use `ReflagContext` instead, this interface will be removed in the next major version
* @internal
*/
export interface ReflagDeprecatedContext extends ReflagContext {
/**
* Context which is not related to a user or a company.
* @deprecated Use `other` instead, this property will be removed in the next major version
*/
otherContext?: Record<string, string | number | undefined>;
}
```
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://typedoc-plugin-markdown.org/schema.json",
"plugin": [
"typedoc-plugin-markdown",
"typedoc-plugin-frontmatter",
"typedoc-plugin-mdn-links"
],
"entryPointStrategy": "packages",
"entryPoints": [
"packages/browser-sdk/",
"packages/node-sdk/",
"packages/react-sdk/",
"packages/vue-sdk/"
],
"packageOptions": {
"entryPoints": ["src/index.ts"]
},
"projectDocuments": [
"packages/browser-sdk/FEEDBACK.md",
"packages/cli/README.md"
],
"readme": "none",
"useHTMLAnchors": false,
"outputs": [
{
"name": "markdown",
"path": "./dist/docs"
}
],
"outputFileStrategy": "modules",
"membersWithOwnFile": [],
"navigation": {
"includeCategories": false,
"includeGroups": false,
"includeFolders": false,
"excludeReferences": true
},
"disableSources": true,
"categorizeByGroup": false,
"groupReferencesByType": true,
"commentStyle": "block",
"useCodeBlocks": true,
"expandObjects": true,
"expandParameters": true,
"typeDeclarationVisibility": "verbose",
"indexFormat": "htmlTable",
"parametersFormat": "htmlTable",
"interfacePropertiesFormat": "htmlTable",
"classPropertiesFormat": "htmlTable",
"enumMembersFormat": "htmlTable",
"propertiesFormat": "htmlTable",
"propertyMembersFormat": "htmlTable",
"typeDeclarationFormat": "htmlTable",
"hideGroupHeadings": false,
"hideBreadcrumbs": true,
"hidePageTitle": false,
"hidePageHeader": true,
"tableColumnSettings": {
"hideDefaults": false,
"hideInherited": true,
"hideModifiers": false,
"hideOverrides": false,
"hideSources": true,
"hideValues": false,
"leftAlignHeaders": false
},
"frontmatterGlobals": {
"layout": {
"visible": true
},
"title": {
"visible": true
},
"description": {
"visible": false
},
"tableOfContents": {
"visible": true
},
"outline": {
"visible": true
},
"pagination": {
"visible": true
}
}
}
```
--------------------------------------------------------------------------------
/packages/node-sdk/src/periodicallyUpdatingCache.ts:
--------------------------------------------------------------------------------
```typescript
import { Cache, Logger } from "./types";
/**
* Create a cached function that updates the value asynchronously.
*
* The value is updated every `ttl` milliseconds.
* If the value is older than `staleTtl` milliseconds, a warning is logged.
*
* @typeParam T - The type of the value.
* @param ttl - The time-to-live in milliseconds.
* @param staleTtl - The time-to-live after which a warning is logged.
* @param logger - The logger to use.
* @param fn - The function to call to get the value.
* @returns The cache object.
**/
export default function periodicallyUpdatingCache<T>(
ttl: number,
staleTtl: number,
logger: Logger | undefined,
fn: () => Promise<T | undefined>,
): Cache<T> {
let cachedValue: T | undefined;
let lastUpdate: number | undefined;
let timeoutId: NodeJS.Timeout | undefined;
let refreshPromise: Promise<void> | undefined;
const update = async () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
try {
const newValue = await fn();
if (newValue === undefined) {
return;
}
logger?.info("refreshed flag definitions");
cachedValue = newValue;
lastUpdate = Date.now();
logger?.debug("updated cached value", cachedValue);
} catch (e) {
logger?.error("failed to update cached value", e);
} finally {
refreshPromise = undefined;
timeoutId = setTimeout(update, ttl).unref();
}
};
const get = () => {
if (lastUpdate !== undefined) {
const age = Date.now() - lastUpdate!;
if (age > staleTtl) {
logger?.warn("cached value is stale", { age, cachedValue });
}
}
return cachedValue;
};
const refresh = async () => {
if (!refreshPromise) {
refreshPromise = update();
}
await refreshPromise;
return get();
};
const waitRefresh = async () => {
// no-op
};
const destroy = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
refreshPromise = undefined;
};
return {
get,
refresh,
waitRefresh,
destroy,
};
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/example/feedback/Feedback.jsx:
--------------------------------------------------------------------------------
```javascript
export const FeedbackForm = () => {
function handleSubmit(e) {
e.preventDefault();
const formData = Object.fromEntries(new FormData(e.target).entries());
const feedbackPayload = {
featureId: "EXAMPLE_FLAG",
userId: "EXAMPLE_USER",
companyId: "EXAMPLE_COMPANY",
score: formData.score ? Number(formData.score) : null,
comment: formData.comment ? formData.comment : null,
};
// Using the Reflag SDK
new ReflagClient({
publishableKey: "EXAMPLE_PUBLISHABLE_KEY",
}).feedback(feedbackPayload);
/*
// Using the Reflag API
fetch("https://front.reflag.com/feedback", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer EXAMPLE_PUBLISHABLE_KEY",
},
body: JSON.stringify(feedbackPayload),
});
*/
}
return (
<form action="#" onSubmit={handleSubmit}>
<h2>How satisfied are you with our ExampleFlag?</h2>
<fieldset>
<legend>Satisfaction</legend>
<div>
<label>
<input type="radio" name="score" value="1" />
<span>Very unsatsified</span>
</label>
</div>
<div>
<label>
<input type="radio" name="score" value="2" />
<span>Unsatisfied</span>
</label>
</div>
<div>
<label>
<input type="radio" name="score" value="3" />
<span>Neutral</span>
</label>
</div>
<div>
<label>
<input type="radio" name="score" value="4" />
<span>Satisfied</span>
</label>
</div>
<div>
<label>
<input type="radio" name="score" value="5" />
<span>Very satsified</span>
</label>
</div>
</fieldset>
<div>
<label>
<div>Comment</div>
<textarea name="comment" placeholder="Write a comment..."></textarea>
</label>
</div>
<button type="submit">Send feedback</button>
</form>
);
};
```
--------------------------------------------------------------------------------
/packages/browser-sdk/test/rateLimiter.test.ts:
--------------------------------------------------------------------------------
```typescript
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import RateLimiter from "../src/rateLimiter";
import { testLogger } from "./testLogger";
describe("rateLimit", () => {
beforeAll(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
});
beforeEach(() => {
vi.advanceTimersByTime(600000); // Advance time by 10 minutes
});
afterAll(() => {
vi.useRealTimers();
});
it("should call the key generator", () => {
const callback = vi.fn();
const limiter = new RateLimiter(1, testLogger);
for (let i = 0; i < 5; i++) {
limiter.rateLimited(`${i}`, callback);
}
expect(callback).toHaveBeenCalledTimes(5);
});
it("should not call the callback when the limit is exceeded", () => {
const callback = vi.fn();
const limiter = new RateLimiter(5, testLogger);
for (let i = 0; i < 10; i++) {
limiter.rateLimited("key", callback);
}
expect(callback).toHaveBeenCalledTimes(5);
});
it("should reset the limit after a minute", () => {
const callback = vi.fn();
const limited = new RateLimiter(1, testLogger);
for (let i = 0; i < 12; i++) {
limited.rateLimited("key", () => callback(i));
vi.advanceTimersByTime(6000); // Advance time by 6 seconds
}
expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenCalledWith(0);
expect(callback).toHaveBeenCalledWith(11); // first one goes through after 1min
});
it("should measure events separately by key", () => {
const callback = vi.fn();
const limited = new RateLimiter(5, testLogger);
for (let i = 0; i < 10; i++) {
limited.rateLimited("key1", callback);
limited.rateLimited("key2", callback);
}
expect(callback).toHaveBeenCalledTimes(10);
});
it("should return the value of the callback always", () => {
const callback = vi.fn();
const limited = new RateLimiter(5, testLogger);
for (let i = 0; i < 5; i++) {
callback.mockReturnValue(i);
const res = limited.rateLimited("key", callback);
expect(res).toBe(i);
}
});
});
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/httpClient.ts:
--------------------------------------------------------------------------------
```typescript
import { API_BASE_URL, SDK_VERSION, SDK_VERSION_HEADER_NAME } from "./config";
export interface HttpClientOptions {
baseUrl?: string;
sdkVersion?: string;
credentials?: RequestCredentials;
}
export class HttpClient {
private readonly baseUrl: string;
private readonly sdkVersion: string;
private readonly fetchOptions: RequestInit;
constructor(
public publishableKey: string,
opts: HttpClientOptions = {},
) {
this.baseUrl = opts.baseUrl ?? API_BASE_URL;
// Ensure baseUrl ends with a trailing slash so subsequent
// path concatenation works as expected
if (!this.baseUrl.endsWith("/")) {
this.baseUrl += "/";
}
this.sdkVersion = opts.sdkVersion ?? SDK_VERSION;
this.fetchOptions = { credentials: opts.credentials };
}
getUrl(path: string): URL {
// see tests for examples
if (path.startsWith("/")) {
path = path.slice(1);
}
return new URL(path, this.baseUrl);
}
async get({
path,
params,
timeoutMs,
}: {
path: string;
params?: URLSearchParams;
timeoutMs?: number;
}): ReturnType<typeof fetch> {
if (!params) {
params = new URLSearchParams();
}
params.set(SDK_VERSION_HEADER_NAME, this.sdkVersion);
params.set("publishableKey", this.publishableKey);
const url = this.getUrl(path);
url.search = params.toString();
if (timeoutMs === undefined) {
return fetch(url, this.fetchOptions);
}
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
const res = await fetch(url, {
...this.fetchOptions,
signal: controller.signal,
});
clearTimeout(id);
return res;
}
async post({
path,
body,
}: {
host?: string;
path: string;
body: any;
}): ReturnType<typeof fetch> {
return fetch(this.getUrl(path), {
...this.fetchOptions,
method: "POST",
headers: {
"Content-Type": "application/json",
[SDK_VERSION_HEADER_NAME]: this.sdkVersion,
Authorization: `Bearer ${this.publishableKey}`,
},
body: JSON.stringify(body),
});
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/Dialog.css:
--------------------------------------------------------------------------------
```css
/* Animations */
@keyframes scale {
from {
transform: scale(0.95);
}
to {
transform: scale(1);
}
}
@keyframes floatUp {
from {
transform: translateY(15%);
}
to {
transform: translateY(0%);
}
}
@keyframes floatDown {
from {
transform: translateY(-15%);
}
to {
transform: translateY(0%);
}
}
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Modal */
.dialog.modal {
margin: auto;
margin-top: 4rem;
&[open] {
animation: /* easeOutQuint */
scale 100ms cubic-bezier(0.22, 1, 0.36, 1),
fade 100ms cubic-bezier(0.22, 1, 0.36, 1);
&::backdrop {
animation: fade 150ms cubic-bezier(0.22, 1, 0.36, 1);
}
}
}
/* Anchored */
.dialog.anchored {
position: absolute;
margin: 0;
&[open] {
animation: /* easeOutQuint */
scale 100ms cubic-bezier(0.22, 1, 0.36, 1),
fade 100ms cubic-bezier(0.22, 1, 0.36, 1);
}
&.top-start {
transform-origin: bottom left;
}
&.top-end {
transform-origin: bottom right;
}
&.right-start {
transform-origin: left top;
}
&.right-end {
transform-origin: left bottom;
}
&.bottom-start {
transform-origin: top left;
}
&.bottom-end {
transform-origin: top right;
}
&.left-start {
transform-origin: right top;
}
&.left-end {
transform-origin: right bottom;
}
}
/* Unanchored */
.dialog[open].unanchored {
&.unanchored-bottom-left,
&.unanchored-bottom-right {
animation: /* easeOutQuint */
floatUp 300ms cubic-bezier(0.22, 1, 0.36, 1),
fade 300ms cubic-bezier(0.22, 1, 0.36, 1);
}
&.unanchored-top-left,
&.unanchored-top-right {
animation: /* easeOutQuint */
floatDown 300ms cubic-bezier(0.22, 1, 0.36, 1),
fade 300ms cubic-bezier(0.22, 1, 0.36, 1);
}
}
.dialog .arrow {
position: absolute;
width: 8px;
height: 8px;
transform: rotate(45deg);
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1px;
border-bottom: 1px solid var(--border-color);
padding: 3px 12px;
}
.dialog-content {
position: relative;
padding: 7px 12px;
}
```
--------------------------------------------------------------------------------
/packages/cli/services/flags.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { authRequest } from "../utils/auth.js";
import { booleanish, EnvironmentQuerySchema } from "../utils/schemas.js";
import { PaginatedResponse } from "../utils/types.js";
export type Stage = {
id: string;
name: string;
order: number;
};
export type RemoteConfigVariant = {
key?: string;
payload?: any;
};
export type RemoteConfig = {
variants: [
{
variant: RemoteConfigVariant;
},
];
};
export type FlagName = {
id: string;
name: string;
key: string;
};
export type Flag = FlagName & {
description: string | null;
remoteConfigs: RemoteConfig[];
stage: Stage | null;
};
export type FlagsResponse = PaginatedResponse<Flag>;
export const FlagsQuerySchema = EnvironmentQuerySchema.extend({
sortBy: z.string().default("key").describe("Field to sort features by"),
sortOrder: z
.enum(["asc", "desc"])
.default("asc")
.describe("Sort direction (ascending or descending)"),
includeRemoteConfigs: booleanish
.default(false)
.describe("Include remote configuration data"),
}).strict();
export const FlagCreateSchema = z
.object({
name: z
.string()
.min(1, "Flag name is required")
.describe("Name of the flag"),
key: z
.string()
.min(1, "Flag key is required")
.describe("Unique identifier key for the flag"),
description: z
.string()
.optional()
.describe("Optional description of the flag"),
})
.strict();
export type FlagsQuery = z.input<typeof FlagsQuerySchema>;
export type FlagCreate = z.input<typeof FlagCreateSchema>;
export async function listFlags(appId: string, query: FlagsQuery) {
return authRequest<FlagsResponse>(`/apps/${appId}/features`, {
params: FlagsQuerySchema.parse(query),
});
}
type CreateFlagResponse = {
feature: FlagName & {
description: string | null;
};
};
export async function createFlag(appId: string, featureData: FlagCreate) {
return authRequest<CreateFlagResponse>(`/apps/${appId}/features`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
source: "event",
...FlagCreateSchema.parse(featureData),
}),
}).then(({ feature }) => feature);
}
```
--------------------------------------------------------------------------------
/packages/cli/services/mcp.ts:
--------------------------------------------------------------------------------
```typescript
import { resolvePath } from "../utils/file.js";
export const SupportedEditors = [
"cursor",
"vscode",
"claude",
"windsurf",
] as const;
export type SupportedEditor = (typeof SupportedEditors)[number];
type ConfigPaths = {
name: string;
global:
| {
mac: string;
windows: string;
linux?: string;
}
| string;
local?: string;
};
export const ConfigPaths: Record<SupportedEditor, ConfigPaths> = {
cursor: {
name: "Cursor",
global: "~/.cursor/mcp.json",
local: ".cursor/mcp.json",
},
vscode: {
name: "Visual Studio Code",
global: {
mac: "~/Library/Application Support/Code/User/settings.json",
linux: "~/.config/Code/User/settings.json",
windows: "@/Code/User/settings.json",
},
local: ".vscode/mcp.json",
},
claude: {
name: "Claude Desktop",
global: {
mac: "~/Library/Application Support/Claude/claude_desktop_config.json",
windows: "@/Claude/claude_desktop_config.json",
},
},
windsurf: {
name: "Windsurf",
global: "~/.codeium/windsurf/mcp_config.json",
},
};
export function resolveConfigPath(editor: SupportedEditor, local = false) {
const editorConfig = ConfigPaths[editor];
const paths = local ? editorConfig.local : editorConfig.global;
if (!paths) return undefined;
if (typeof paths === "string") {
return resolvePath(paths);
}
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
switch (process.platform) {
case "darwin":
return resolvePath(paths.mac);
case "win32":
return resolvePath(paths.windows);
case "linux":
return paths.linux ? resolvePath(paths.linux) : undefined;
default:
return undefined;
}
}
export function getServersConfig(
editorConfig: any,
selectedEditor: SupportedEditor,
configPathType: "global" | "local",
) {
if (selectedEditor === "vscode") {
if (configPathType === "global") {
editorConfig.mcp = editorConfig.mcp || {};
editorConfig.mcp.servers = editorConfig.mcp.servers || {};
return editorConfig.mcp.servers;
} else {
editorConfig.servers = editorConfig.servers || {};
return editorConfig.servers;
}
}
editorConfig.mcpServers = editorConfig.mcpServers || {};
return editorConfig.mcpServers;
}
```
--------------------------------------------------------------------------------
/packages/vue-sdk/dev/plain/App.vue:
--------------------------------------------------------------------------------
```vue
<script setup lang="ts">
import { computed, ref } from "vue";
import { ReflagBootstrappedProvider, ReflagProvider } from "../../src";
import Events from "./components/Events.vue";
import FlagsList from "./components/FlagsList.vue";
import MissingKeyMessage from "./components/MissingKeyMessage.vue";
import RequestFeedback from "./components/RequestFeedback.vue";
import Section from "./components/Section.vue";
import StartHuddlesButton from "./components/StartHuddlesButton.vue";
import Track from "./components/Track.vue";
// Initial context
const initialUser = { id: "demo-user", email: "[email protected]" };
const initialCompany = { id: "demo-company", name: "Demo Company" };
const initialOther = { test: "test" };
const context = ref({
user: initialUser,
company: initialCompany,
other: initialOther,
});
const publishableKey = import.meta.env.VITE_PUBLISHABLE_KEY || "";
const apiBaseUrl = import.meta.env.VITE_REFLAG_API_BASE_URL;
// Check for bootstrapped query parameter
const isBootstrapped = computed(() => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get("bootstrapped") !== null;
});
</script>
<template>
<div v-if="!publishableKey">
<MissingKeyMessage />
</div>
<!-- Bootstrapped Provider -->
<ReflagBootstrappedProvider
v-else-if="isBootstrapped"
:publishable-key="publishableKey"
:flags="{
context,
flags: {
huddles: {
key: 'huddles',
isEnabled: true,
},
},
}"
:api-base-url="apiBaseUrl"
>
<template #loading>......loading......</template>
<h1>Vue SDK (Bootstrapped)</h1>
<StartHuddlesButton />
<Track />
<RequestFeedback />
<Section title="Set User ID">
<input v-model="context.user.id" />
</Section>
<Events />
<FlagsList />
</ReflagBootstrappedProvider>
<!-- Regular Provider -->
<ReflagProvider
v-else
:publishable-key="publishableKey"
:context="context"
:api-base-url="apiBaseUrl"
>
<template #loading>......loading......</template>
<h1>Vue SDK</h1>
<StartHuddlesButton />
<Track />
<RequestFeedback />
<Section title="Set User ID">
<input v-model="context.user.id" />
</Section>
<Events />
<FlagsList />
</ReflagProvider>
</template>
```
--------------------------------------------------------------------------------
/packages/cli/utils/errors.ts:
--------------------------------------------------------------------------------
```typescript
import { ExitPromptError } from "@inquirer/core";
import { ErrorObject } from "ajv";
import chalk from "chalk";
export class MissingAppIdError extends Error {
constructor() {
super(
"App ID is required. Please provide it with --appId or in the config file. Use `reflag apps list` to see available apps.",
);
this.name = "MissingAppIdError";
}
}
export class MissingEnvIdError extends Error {
constructor() {
super("Environment ID is required.");
this.name = "MissingEnvIdError";
}
}
export class ConfigValidationError extends Error {
constructor(errors?: ErrorObject[] | null) {
const messages = errors
?.map((e) => {
const path = e.instancePath || "config";
const value = e.params?.allowedValues
? `: ${e.params.allowedValues.join(", ")}`
: "";
return `${path}: ${e.message}${value}`;
})
.join("\n");
super(messages);
this.name = "ConfigValidationError";
}
}
type ResponseErrorData = {
error?: {
message?: string;
code?: string;
};
validationErrors?: { path: string[]; message: string }[];
};
export class ResponseError extends Error {
public readonly data: ResponseErrorData;
constructor(response: ResponseErrorData) {
super(response.error?.message ?? response.error?.code);
this.data = response;
this.name = "ResponseError";
}
}
export function handleError(error: unknown, tag: string): never {
tag = chalk.bold(`\n[${tag}] error:`);
if (error instanceof ExitPromptError) {
process.exit(0);
} else if (error instanceof ResponseError) {
console.error(
chalk.red(tag, error.data.error?.message ?? error.data.error?.code),
);
if (error.data.validationErrors) {
console.table(
error.data.validationErrors.map(
({ path, message }: { path: string[]; message: string }) => ({
path: path.join("."),
error: message,
}),
),
);
}
} else if (error instanceof Error) {
console.error(chalk.red(tag, error.message));
if (error.cause) {
console.error(error.cause);
}
} else if (typeof error === "string") {
console.error(chalk.red(tag, error));
} else {
console.error(chalk.red(tag ?? "An unknown error occurred:", error));
}
process.exit(1);
}
```
--------------------------------------------------------------------------------
/packages/cli/utils/options.ts:
--------------------------------------------------------------------------------
```typescript
import { Argument, Option } from "commander";
import { CONFIG_FILE_NAME } from "./constants.js";
// Define supported editors directly here or import from a central place if needed elsewhere
const SUPPORTED_EDITORS = ["cursor", "vscode"] as const; // Add more later: "claude", "cline", "windsurf"
export const debugOption = new Option("--debug", "Enable debug mode.");
export const baseUrlOption = new Option(
"--base-url [url]",
`Reflag service URL (useful if behind a proxy). Falls back to baseUrl value in ${CONFIG_FILE_NAME}.`,
);
export const apiUrlOption = new Option(
"--api-url [url]",
`Reflag API URL (useful if behind a proxy). Falls back to apiUrl value in ${CONFIG_FILE_NAME} or baseUrl with /api appended.`,
);
export const apiKeyOption = new Option(
"--api-key [key]",
`Reflag API key. Can be used in CI/CD pipelines where logging in is not possible.`,
);
export const appIdOption = new Option(
"-a, --appId [appId]",
`Reflag App ID. Falls back to appId value in ${CONFIG_FILE_NAME}.`,
);
export const overwriteOption = new Option(
"--overwrite",
"Force initialization and overwrite existing configuration.",
);
export const typesOutOption = new Option(
"-o, --out [path]",
`Single output path for generated flag types. Falls back to typesOutput value in ${CONFIG_FILE_NAME}.`,
);
export const typesFormatOption = new Option(
"-f, --format [format]",
"Single output format for generated flag types",
).choices(["react", "node"]);
export const flagNameArgument = new Argument(
"[name]",
"Flag's name. If not provided, you'll be prompted to enter one.",
);
export const flagKeyOption = new Option(
"-k, --key [flag key]",
"Flag key. If not provided, a key is generated from the flag's name.",
);
export const editorOption = new Option(
"-e, --editor [editor]",
"Specify the editor to configure for MCP.",
).choices(SUPPORTED_EDITORS);
export const configScopeOption = new Option(
"-s, --scope [scope]",
"Specify whether to use local or global configuration.",
).choices(["local", "global"]);
export const rulesFormatOption = new Option(
"-f, --format [format]",
"Format to copy rules in",
)
.choices(["cursor", "copilot"])
.default("cursor");
export const yesOption = new Option(
"-y, --yes",
"Skip confirmation prompts and overwrite existing files without asking.",
);
```
--------------------------------------------------------------------------------
/packages/browser-sdk/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>Reflag Browser SDK</title>
</head>
<body style="background-color: white">
<div id="app"></div>
<span id="loading">Loading...</span>
<script>
const urlParams = new URLSearchParams(window.location.search);
const publishableKey = urlParams.get("publishableKey");
const flagKey = urlParams.get("flagKey") ?? "huddles";
const isBootstrapped = urlParams.get("bootstrapped") === "true";
</script>
<style>
body {
font-family: sans-serif;
}
#start-huddle {
border: 1px solid black;
padding: 10px;
}
</style>
<div id="start-huddle" style="display: none">
<button
onClick="reflag.requestFeedback({flagKey, position: {type: 'POPOVER', anchor: event.currentTarget}})"
>
Give feedback!
</button>
<button onClick="reflag.track(flagKey)">Start huddle</button>
</div>
<script type="module">
import { ReflagClient } from "./src/index.ts";
window.reflag = new ReflagClient({
publishableKey,
user: { id: "42" },
company: { id: "1" },
toolbar: {
show: true,
position: {
placement: "bottom-right",
},
},
bootstrappedFlags: isBootstrapped
? {
[flagKey]: {
key: flagKey,
isEnabled: true,
},
}
: undefined,
});
function setVisibility(isVisible) {
const startHuddleElem = document.getElementById("start-huddle");
if (startHuddleElem)
startHuddleElem.style.display = isVisible ? "block" : "none";
}
reflag.initialize().then(() => {
console.log("Reflag initialized");
document.getElementById("loading").style.display = "none";
if (isBootstrapped) {
const flag = reflag.getFlag(flagKey);
setVisibility(flag.isEnabled);
}
});
reflag.on("check", (check) =>
console.log(`Check event for ${check.key}`),
);
reflag.on("flagsUpdated", (flags) => {
console.log("Flags updated");
const flag = reflag.getFlag(flagKey);
setVisibility(flag.isEnabled);
});
</script>
</body>
</html>
```
--------------------------------------------------------------------------------
/packages/browser-sdk/example/feedback/feedback.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Reflag feedback collection</title>
<script src="https://cdn.jsdelivr.net/npm/@reflag/browser-sdk@latest"></script>
<style>
fieldset label {
display: flex;
align-items: center;
padding: 5px;
gap: 5px;
}
label input {
margin: 0;
}
</style>
</head>
<body>
<script>
function handleSubmit(e) {
e.preventDefault();
const formData = Object.fromEntries(new FormData(e.target).entries());
const feedbackPayload = {
featureId: "EXAMPLE_FLAG",
userId: "EXAMPLE_USER",
companyId: "EXAMPLE_COMPANY",
score: formData.score ? Number(formData.score) : null,
comment: formData.comment ? formData.comment : null,
};
// Using the Reflag SDK
new ReflagClient({
publishableKey: "EXAMPLE_PUBLISHABLE_KEY",
}).feedback(feedbackPayload);
/*
// Using the Reflag API
fetch("https://front.reflag.com/feedback", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer EXAMPLE_PUBLISHABLE_KEY",
},
body: JSON.stringify(feedbackPayload),
});
*/
}
</script>
<form action="#" onsubmit="handleSubmit(event)">
<h2>How satisfied are you with our ExampleFlag?</h2>
<fieldset>
<legend>Satisfaction</legend>
<div>
<label>
<input type="radio" name="score" value="1" />
<span>Very unsatsified</span>
</label>
</div>
<div>
<label>
<input type="radio" name="score" value="2" />
<span>Unsatisfied</span>
</label>
</div>
<div>
<label>
<input type="radio" name="score" value="3" />
<span>Neutral</span>
</label>
</div>
<div>
<label>
<input type="radio" name="score" value="4" />
<span>Satisfied</span>
</label>
</div>
<div>
<label>
<input type="radio" name="score" value="5" />
<span>Very satsified</span>
</label>
</div>
</fieldset>
<div>
<label>
<div>Comment</div>
<textarea name="comment" placeholder="Write a comment..."></textarea>
</label>
</div>
<button type="submit">Send feedback</button>
</form>
</body>
</html>
```
--------------------------------------------------------------------------------
/packages/node-sdk/src/batch-buffer.ts:
--------------------------------------------------------------------------------
```typescript
import { BATCH_INTERVAL_MS, BATCH_MAX_SIZE } from "./config";
import { BatchBufferOptions, Logger } from "./types";
import { isObject, ok } from "./utils";
/**
* A buffer that accumulates items and flushes them in batches.
* @typeparam T - The type of items to buffer.
*/
export default class BatchBuffer<T> {
private buffer: T[] = [];
private flushHandler: (items: T[]) => Promise<void>;
private logger?: Logger;
private maxSize: number;
private intervalMs: number;
private timer: NodeJS.Timeout | null = null;
/**
* Creates a new `BatchBuffer` instance.
* @param options - The options to configure the buffer.
* @throws If the options are invalid.
*/
constructor(options: BatchBufferOptions<T>) {
ok(isObject(options), "options must be an object");
ok(
typeof options.flushHandler === "function",
"flushHandler must be a function",
);
ok(isObject(options.logger) || !options.logger, "logger must be an object");
ok(
(typeof options.maxSize === "number" && options.maxSize > 0) ||
typeof options.maxSize !== "number",
"maxSize must be greater than 0",
);
ok(
(typeof options.intervalMs === "number" && options.intervalMs >= 0) ||
typeof options.intervalMs !== "number",
"intervalMs must be greater than or equal to 0",
);
this.flushHandler = options.flushHandler;
this.logger = options.logger;
this.maxSize = options.maxSize ?? BATCH_MAX_SIZE;
this.intervalMs = options.intervalMs ?? BATCH_INTERVAL_MS;
}
/**
* Adds an item to the buffer.
*
* @param item - The item to add.
*/
public async add(item: T) {
this.buffer.push(item);
if (this.buffer.length >= this.maxSize) {
await this.flush();
} else if (!this.timer && this.intervalMs > 0) {
this.timer = setTimeout(() => this.flush(), this.intervalMs).unref();
}
}
public async flush(): Promise<void> {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.buffer.length === 0) {
this.logger?.debug("buffer is empty. nothing to flush");
return;
}
const flushingBuffer = this.buffer;
this.buffer = [];
try {
await this.flushHandler(flushingBuffer);
this.logger?.info("flushed buffered items", {
count: flushingBuffer.length,
});
} catch (error) {
this.logger?.error("flush of buffered items failed; discarding items", {
error,
count: flushingBuffer.length,
});
}
}
/**
* Destroys the buffer, clearing any pending timer and discarding buffered items.
*/
public destroy(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.buffer = [];
}
}
```
--------------------------------------------------------------------------------
/packages/browser-sdk/test/init.test.ts:
--------------------------------------------------------------------------------
```typescript
import { DefaultBodyType, http, StrictRequest } from "msw";
import { beforeEach, describe, expect, test, vi, vitest } from "vitest";
import { ReflagClient } from "../src";
import { HttpClient } from "../src/httpClient";
import { getFlags } from "./mocks/handlers";
import { server } from "./mocks/server";
const KEY = "123";
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
describe("init", () => {
test("will accept setup with key and debug logger", async () => {
const reflagInstance = new ReflagClient({
publishableKey: KEY,
user: { id: 42 },
company: { id: 42 },
logger,
});
const spyInit = vi.spyOn(reflagInstance, "initialize");
await reflagInstance.initialize();
expect(spyInit).toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalled();
});
test("will accept setup with custom host", async () => {
let usedSpecialHost = false;
server.use(
http.get(
"https://example.com/features/evaluated",
({ request }: { request: StrictRequest<DefaultBodyType> }) => {
usedSpecialHost = true;
return getFlags({ request });
},
),
);
const reflagInstance = new ReflagClient({
publishableKey: KEY,
user: { id: "foo" },
apiBaseUrl: "https://example.com",
});
await reflagInstance.initialize();
expect(usedSpecialHost).toBe(true);
});
test("automatically does user/company tracking", async () => {
const user = vitest.spyOn(ReflagClient.prototype as any, "user");
const company = vitest.spyOn(ReflagClient.prototype as any, "company");
const reflagInstance = new ReflagClient({
publishableKey: KEY,
user: { id: "foo" },
company: { id: "bar" },
});
await reflagInstance.initialize();
expect(user).toHaveBeenCalled();
expect(company).toHaveBeenCalled();
});
test("can disable tracking and auto. feedback surveys", async () => {
const post = vitest.spyOn(HttpClient.prototype as any, "post");
const reflagInstance = new ReflagClient({
publishableKey: KEY,
user: { id: "foo" },
apiBaseUrl: "https://example.com",
enableTracking: false,
feedback: {
enableAutoFeedback: false,
},
});
await reflagInstance.initialize();
await reflagInstance.track("test");
expect(post).not.toHaveBeenCalled();
});
test("passes credentials correctly to httpClient", async () => {
const credentials = "include";
const reflagInstance = new ReflagClient({
publishableKey: KEY,
user: { id: "foo" },
credentials,
});
await reflagInstance.initialize();
expect(reflagInstance["httpClient"]["fetchOptions"].credentials).toBe(
credentials,
);
});
});
```