diff --git a/.changeset/browser-framework-collection-values.md b/.changeset/browser-framework-collection-values.md new file mode 100644 index 000000000..362fc65c7 --- /dev/null +++ b/.changeset/browser-framework-collection-values.md @@ -0,0 +1,17 @@ +--- +"@reflag/browser-sdk": major +"@reflag/react-sdk": major +"@reflag/vue-sdk": major +--- + +Adopt protocol-v2 evaluated flags and direct JSON values across Browser, React and Vue. + +Bare singular reads return Boolean with an implicit false default. Positional JSON defaults infer widened direct-value types without generated declarations, enforce top-level categories, preserve accepted falsy values, and return independent copies. Null explicitly accepts arbitrary JSON; nested schema validation remains the application's responsibility. + +Browser `getFlag()` and `getFlags()` register root checks immediately with genuine selected-key/source/snapshot identity. Delivery stays buffered and identity-aware rate-limited. Diagnostic access and bootstrap serialization do not register checks; defaults, mismatches, overrides and static fallback values do not invent selections. V2 cache/override storage is isolated from v1, and complete evaluated snapshots are validated before acceptance. This requires compatible `/flags/evaluated` and canonical-check ingestion on the server. + +React `useFlag()` returns a direct value without suspending. Use the separate `useFlagSuspense()` hook to wait for initialization; missing flags then use the default. Vue `useFlag()` returns a ref to the direct value. Loading, application usage tracking and feedback remain separate hooks/composables. The existing opt-in Suspense controls remain specific to `useOptInFlags`. + +Upgrade bootstrap producers/consumers together: only protocol-v2 envelopes from Node SDK v2 are supported; legacy bootstrap wrappers and the Browser `bootstrappedFlags` option are rejected. Move wrapper `.track()`/`.requestFeedback()` calls to ordinary client/framework methods. Undefined clears a value override; null is a JSON override. + +Opt-in metadata arrives with ordinary v2 evaluated responses. Bootstrapped clients fetch missing metadata on demand from the same v2 endpoint without replacing server-derived values. Mutations confirm versioned evaluation and membership in one refresh. The unchanged OpenFeature Browser provider and React Native wrapper remain pinned to published v1 SDK dependencies. CLI direct-value generation remains a separate migration. diff --git a/.changeset/browser-framework-registered-defaults.md b/.changeset/browser-framework-registered-defaults.md new file mode 100644 index 000000000..32ed1719e --- /dev/null +++ b/.changeset/browser-framework-registered-defaults.md @@ -0,0 +1,9 @@ +--- +"@reflag/browser-sdk": patch +"@reflag/react-sdk": patch +"@reflag/vue-sdk": patch +--- + +Accept named-interface defaults in Browser reads and check non-null React/Vue defaults against registered flag value schemas, including React Suspense reads. Preserve Boolean bare reads, explicit-null arbitrary JSON, readonly-default support, default-inferred widening, and runtime JSON validation/copying. + +Keep Vue declaration imports pointed at the public Vue types so published refs retain their value types rather than degrading to any. diff --git a/.changeset/multivariate-evaluation.md b/.changeset/multivariate-evaluation.md new file mode 100644 index 000000000..b65d2150c --- /dev/null +++ b/.changeset/multivariate-evaluation.md @@ -0,0 +1,17 @@ +--- +"@reflag/flag-evaluation": major +--- + +Introduce a v2-only compiled-variant evaluator: + +- `newEvaluator({ flags, segments })` prepares a complete collection and returns `evaluateAll(context)` and `evaluateFlag(context, flagKey)` methods. The batch result contains shared normalized context and per-flag results without repeated context. Single-flag evaluation visits only the requested flag and its dependencies. Unresolved batch entries do not prevent other flags from being evaluated. Remove the v1 value-rule APIs and the one-shot `evaluateFlag` wrapper. +- Preserve flag and segment reference nodes instead of inlining their filters. Flag references explicitly match variant keys; segments remain reusable predicates. Resolve references recursively without dependency-result caching or memoization. +- Preserve diagnostics from unsuccessful referenced rules without suppressing a valid matching selection. Invalid nonmatches remain fail-closed through negation. Propagated diagnostics identify their source flag or segment. +- Reject duplicate keys, missing references, unknown referenced variant keys and cycles at preparation, including mixed flag/segment cycles. Evaluating an unknown flag key throws. +- Preserve arbitrary variant value types through `CompiledFlag` and `EvaluationResult` (default `any`). `resolved: true | false` distinguishes a selected value, including `undefined`, from failure. Successful results require the variant key and matched rule ID. +- Keep membership Sets and prepared percentage bounds private. `ANY_OF` / `NOT_ANY_OF` use cached Set lookups, including under groups and negations; percentage distributions use precomputed bounds and a simple scan. +- Evaluate rules sequentially with explicit fallthrough and source-version/rule/allocation diagnostics. +- Return structured `INVALID_COMPARISON` errors for invalid numeric/date comparisons instead of logging context values. Invalid conditions cannot match through negation. +- Retain the original hash function and threshold rollout filter for boolean cohort compatibility. The inclusive maximum distribution bucket selects the last nonzero allocation. + +The runtime exports are `newEvaluator`, `flattenContext`, and `hashInt`. Removed APIs include `evaluateFlagRules`, `newFlagEvaluator`, `evaluateFlag`, the scalar `evaluate` helper, and the legacy `flattenJSON` / `unflattenJSON` helpers. diff --git a/.changeset/node-sdk-collection-values.md b/.changeset/node-sdk-collection-values.md new file mode 100644 index 000000000..bcb78e94a --- /dev/null +++ b/.changeset/node-sdk-collection-values.md @@ -0,0 +1,18 @@ +--- +"@reflag/node-sdk": major +"@reflag/openfeature-node-provider": patch +--- + +Adopt evaluator v2 and the complete `GET /flags` collection protocol in the Node SDK. + +- `getFlag(context, key)` and bound `getFlag(key)` are direct Boolean reads with an implicit `false` default. Positional defaults infer the value type, widening literals, without generated declarations. Non-null defaults require the same top-level JSON category; missing/mismatched values use the default without authored-selection metadata. An explicit `null` default accepts arbitrary JSON, including authored null. Nested payload schemas are not validated. There is no flag wrapper, `.isEnabled`, `.config`, or flag `.track()` method. Batch `getFlags()` keeps raw JSON/`undefined` values. Ordinary client `track()` APIs remain separate. +- Singular remote reads share the default contract. Root `getFlagRemote(key, defaultValue, userId?, companyId?, context?)` puts the default second, moving old positional IDs one place right; pass `undefined` for a Boolean read with IDs. Bound `getFlagRemote(key, defaultValue?)` needs no explicit IDs. +- Reads immediately register checks for requested roots. Checks carry the selected variant key as `evalResult`, `sourceVersionId`, and the snapshot `flagStateVersion`. Boolean values send the keys `"true"` / `"false"`. Equal-valued variants and dependency-only version changes retain their distinct identities. Delivery remains buffered and rate-limited. +- Prepare complete flags/segments snapshots before atomically accepting them. Evaluate references recursively without dependency-result caching. +- Persist full graphs and snapshot versions in version-2 fallback snapshots. V1 snapshots are not converted. Built-in storage uses separate versioned keys/files so V1 and V2 clients cannot overwrite one another's snapshots. Local overrides and value-only fallbacks do not fabricate authored variant selections. +- Bootstrap state contains v2 selection results and context/version metadata, without registering browser checks. Existing v1 browser bootstrap consumers require a separate migration; this is not a backwards-compatible bootstrap shape. +- Preserve sync modes, ordinary tracking, bound contexts, and post-evaluation override layering. Update the Express example to consume direct values. + +The existing OpenFeature provider and v1 React bootstrap demo remain explicitly pinned to published Node SDK v1 through an npm alias. This prevents automatic workspace dependency upgrades from silently breaking their wrapper/bootstrap contracts; migrating those consumers to multivariate values is separate work. + +This release requires the collection-capable evaluator and compatible `/flags` and variant-key check ingestion. It does not migrate legacy remote config into multivariate flag values. diff --git a/.changeset/node-sdk-registered-defaults.md b/.changeset/node-sdk-registered-defaults.md new file mode 100644 index 000000000..4800434f7 --- /dev/null +++ b/.changeset/node-sdk-registered-defaults.md @@ -0,0 +1,5 @@ +--- +"@reflag/node-sdk": patch +--- + +Check non-null singular-read defaults against the registered flag value type for local, remote, and bound getters. Accept named-interface and readonly defaults, while preserving Boolean bare reads, explicit-null arbitrary JSON, default-inferred widening, and runtime JSON validation/copying. diff --git a/docs.sh b/docs.sh index 9b57fa72b..8cbf72df0 100755 --- a/docs.sh +++ b/docs.sh @@ -24,7 +24,7 @@ do echo "Processing $file..." # Fix anchor links in globals.md files - if [[ "$file" == *"globals.md" ]]; then + if [ "${file##*/}" = "globals.md" ]; then sed -r "$SEDCOMMAND" "$file" > "$file.fixed" rm "$file" mv "$file.fixed" "$file" diff --git a/packages/browser-sdk/README.md b/packages/browser-sdk/README.md index bc7364a98..202f66aba 100644 --- a/packages/browser-sdk/README.md +++ b/packages/browser-sdk/README.md @@ -1,582 +1,148 @@ -# Reflag Browser SDK +# Reflag Browser SDK v2 -Basic client for [Reflag.com](https://reflag.com). If you're using React, you'll be better off with the Reflag React SDK. +Client-side feature flags and JSON values for [Reflag](https://reflag.com). -Reflag supports flag toggling, tracking flag usage, [collecting feedback](#qualitative-feedback-on-beta-flags) on flags, and [remotely configuring flags](#remote-config). +> This is a breaking v2 prerelease. It requires the v2 evaluated-flags/check protocol on the server. Do not switch existing v1 applications or bootstrap consumers without coordinating that upgrade. Nothing in this branch publishes or deploys the SDK/server. -## Install - -First find your `publishableKey` under [environment settings](https://app.reflag.com/env-current/settings/app-environments) in Reflag. - -The package can be imported or used directly in a HTML script tag: - -A. Import module: - -```typescript -import { ReflagClient } from "@reflag/browser-sdk"; - -const user = { - id: 42, - role: "manager", -}; - -const company = { - id: 99, - plan: "enterprise", -}; - -const reflagClient = new ReflagClient({ publishableKey, user, company }); - -await reflagClient.initialize(); - -const { - isEnabled, - config: { payload: question }, - track, - requestFeedback, -} = reflagClient.getFlag("huddle"); - -if (isEnabled) { - // Show flag. When retrieving `isEnabled` the client automatically - // sends a "check" event for the "huddle" flag which is shown in the - // Reflag UI. - - // On usage, call `track` to let Reflag know that a user interacted with the flag - track(); - - // The `payload` is a user-supplied JSON in Reflag that is dynamically picked - // out depending on the user/company. - const question = payload?.question ?? "Tell us what you think of Huddles"; - - // Use `requestFeedback` to create "Send feedback" buttons easily for specific - // flags. This is not related to `track` and you can call them individually. - requestFeedback({ title: question }); -} - -// `track` just calls `reflagClient.track()` to send an event using the same flag key -// You can also use `track` on the client directly to send any custom event. -reflagClient.track("huddle"); - -// similarly, `requestFeedback` just calls `reflagClient.requestFeedback({flagKey: })` -// which you can also call directly: -reflagClient.requestFeedback({ flagKey: "huddle" }); -``` - -B. Script tag (client-side directly in html) - -See [example/browser.html](https://github.com/reflagcom/javascript/tree/main/packages/browser-sdk/example/browser.html) for a working example: - -```html - - -Loading... - -``` - -### Init options - -Supply these to the constructor call: - -```typescript -type Configuration = { - logger: console; // by default only logs warn/error, by passing `console` you'll log everything - apiBaseUrl?: "https://front.reflag.com"; - credentials?: "include" | "same-origin" | "omit"; // forwarded to fetch requests; "include" also enables credentials for the default EventSource transport - feedback?: undefined; // See FEEDBACK.md - enableTracking?: true; // set to `false` to stop sending track events and user/company updates to Reflag servers. Useful when you're impersonating a user - enableLiveFlagUpdates?: false; // Set to `true` to keep flags up to date over SSE (browser SDK default: false) - eventSourceFactory?: (url: string) => { - // Advanced: provide a custom EventSource-compatible transport - addEventListener: (type: string, cb: (event: any) => void) => void; - close: () => void; - }; - fallbackFlags?: - | string[] - | Record; // Enable these flags if unable to contact reflag.com. Can be a list of flag keys or a record with configuration values - timeoutMs?: number; // Timeout for fetching flags (default: 5000ms) - staleWhileRevalidate?: boolean; // Revalidate in the background when cached flags turn stale to avoid latency in the UI (default: false) - staleTimeMs?: number; // at initialization time flags are loaded from the cache unless they have gone stale. Defaults to 0 which means the cache is disabled. Increase this in the case of a non-SPA - expireTimeMs?: number; // In case we're unable to fetch flags from Reflag, cached/stale flags will be used instead until they expire after `expireTimeMs`. Default is 30 days - offline?: boolean; // Use the SDK in offline mode. Offline mode is useful during testing and local development -}; -``` - -## Migrating from Bucket SDK - -If you have been using the Bucket SDKs, the following list will help you migrate to Reflag SDK: - -- `Bucket*` classes, and types have been renamed to `Reflag*` (e.g. `BucketClient` is now `ReflagClient`) -- `Feature*` classes, and types have been renamed to `Feature*` (e.g. `Feature` is now `Flag`, `RawFeatures` is now `RawFlags`) -- All methods that contained `feature` in the name have been renamed to use the `flag` terminology (e.g. `getFeature` is `getFlag`) -- The `fallbackFeatures` property in client constructor and configuration files has been renamed to `fallbackFlags` -- `featureKey` has been renamed to `flagKey` in all methods that accepts that argument -- The new cookies that are stored in the client's browser are now `reflag-*` prefixed instead og `bucket-*` -- The `featuresUpdated` hook has been renamed to `flagsUpdated` -- The `checkIsEnabled` and `checkConfig` hooks have been removed, use `check` from now on - -To ease in transition to Reflag SDK, some of the old methods have been preserved as aliases to the new methods: - -- `getFeature` method is an alias for `getFlag` -- `getFeatures` method is an alias for `getFlags` -- `featuresUpdated` hook is an alias for `flagsUpdated` - -If you are running with strict Content Security Policies active on your website, you will need change them as follows: - -- `connect-src https://front.bucket.co` to `connect-src https://front.reflag.com` - -Finally, if you have customized the look & feel of the Feedback component, update `--bucket-feedback-*` CSS classes to `--reflag-feedback-*` - -## Flag toggles - -Reflag determines which flags are active for a given user/company. The user/company is given in the ReflagClient constructor. - -If you supply `user` or `company` objects, they must include at least the `id` property otherwise they will be ignored in their entirety. -In addition to the `id`, you must also supply anything additional that you want to be able to evaluate flag targeting rules against. - -Attributes cannot be nested (multiple levels) and must be either strings, integers or booleans. -Some attributes are special and used in Reflag UI: - -- `name` -- display name for `user`/`company`, -- `email` -- is accepted for `user`s and will be highlighted in the Reflag UI if available, -- `avatar` -- can be provided for both `user` and `company` and should be an URL to an image. +## Get started ```ts -const reflagClient = new ReflagClient({ - publishableKey, - user: { - id: "user_123", - name: "John Doe", - email: "john@acme.com" - avatar: "https://example.com/images/udsy6363" - }, - company: { - id: "company_123", - name: "Acme, Inc", - avatar: "https://example.com/images/31232ds" - }, -}); -``` - -To retrieve flags along with their targeting information, use `getFlag(key: string)`: - -```ts -const huddle = reflagClient.getFlag("huddle"); -// { -// isEnabled: true, -// config: { key: "zoom", payload: { ... } }, -// track: () => Promise -// requestFeedback: (options: RequestFeedbackData) => void -// } -``` - -You can use `getFlags()` to retrieve all enabled flags currently. - -```ts -const flags = reflagClient.getFlags(); -// { -// huddle: { -// isEnabled: true, -// targetingVersion: 42, -// config: ... -// } -// } -``` - -`getFlags()` is meant to be more low-level than `getFlag()` and it typically used -by down-stream clients, like the React SDK. - -Note that accessing `isEnabled` on the object returned by `getFlags` does not automatically -generate a `check` event, contrary to the `isEnabled` property on the object returned by `getFlag`. - -## End-user opt-in - -If a flag has end-user opt-in enabled in Reflag, you can list the opt-in options for the current context and set or cancel opt-in for the current user or company. - -```ts -const optInFlags = reflagClient.getOptInFlags(); -const isLoadingOptInFlags = reflagClient.getIsLoadingOptInFlags(); -// [{ key, name, description, isEnabled, userOptedIn, companyOptedIn, isOptedIn }] - -await reflagClient.setOptIn("huddle", { optedIn: true }); -await reflagClient.setOptIn("huddle", { optedIn: false }); - -await reflagClient.setOptIn("huddle", { - optedIn: true, - scope: "company", -}); -``` - -By default, `setOptIn()` changes the opt-in for the current user, so the current context must include a `user.id`. To manage the current company's opt-in instead, pass `scope: "company"`; the context must then include a `company.id`. - -User and company opt-ins are managed independently. Setting `optedIn` to `false` removes the opt-in only for the selected scope. For example, cancelling a user's opt-in does not change the company's opt-in for the same flag. - -`setOptIn` returns a promise so you can wait for the new membership state to be synchronized. It resolves after the latest flag state has been applied locally, the requested membership change has been confirmed, and `flagsUpdated` listeners have been notified. - -The `description` comes from the dedicated SDK-facing opt-in description configured in Reflag. - -For a bootstrapped client, the first `getOptInFlags()` or `getIsLoadingOptInFlags()` call starts one flags refresh. The list call returns the currently available list synchronously, and the loading getter returns `true` until the refresh succeeds or fails. Normal initialization already exposes loading through the client's state. - -Listen for `optInFlagsLoadingUpdated` to update UI when this loading state changes. `flagsUpdated` is emitted when a successful refresh updates the list. - -## Remote config - -Remote config is a dynamic and flexible approach to configuring flag behavior outside of your app – without needing to re-deploy it. - -Similar to `isEnabled`, each flag has a `config` property. This configuration is managed from within Reflag. -It is managed similar to the way access to flags is managed, but instead of the binary `isEnabled` you can have -multiple configuration values which are given to different user/companies. - -```ts -const flags = reflagClient.getFlags(); -// { -// huddle: { -// isEnabled: true, -// targetingVersion: 42, -// config: { -// key: "gpt-3.5", -// payload: { maxTokens: 10000, model: "gpt-3.5-beta1" } -// } -// } -// } -``` - -`key` is mandatory for a config, but if a flag has no config or no config value was matched against the context, the `key` will be `undefined`. Make sure to check against this case when trying to use the configuration in your application. `payload` is an optional JSON value for arbitrary configuration needs. - -Just as `isEnabled`, accessing `config` on the object returned by `getFlags` does not automatically -generate a `check` event, contrary to the `config` property on the object returned by `getFlag`. - -## Server-side rendering and bootstrapping - -For server-side rendered applications, you can render immediately with pre-fetched flag data by bootstrapping the client. - -### Init options bootstrapped - -```typescript -type Configuration = { - logger: console; // by default only logs warn/error, by passing `console` you'll log everything - apiBaseUrl?: "https://front.reflag.com"; - credentials?: "include" | "same-origin" | "omit"; // forwarded to fetch requests; "include" also enables credentials for the default EventSource transport - feedback?: undefined; // See FEEDBACK.md - enableTracking?: true; // set to `false` to stop sending track events and user/company updates to Reflag servers. Useful when you're impersonating a user - offline?: boolean; // Use the SDK in offline mode. Offline mode is useful during testing and local development - bootstrappedState?: { - context: ReflagContext; - flags: FetchedFlags; - flagStateVersion?: number; - }; // Pre-fetched evaluated state from server-side (see Server-side rendering section) - bootstrappedFlags?: FetchedFlags; // Deprecated: use `bootstrappedState` instead -}; -``` - -### Using `bootstrappedState` - -Use the Node SDK's `getFlagsForBootstrap()` method to pre-fetch evaluated state server-side, then pass the returned object directly to the browser client: - -```typescript -// Server-side: Get bootstrapped state using Node SDK -import { ReflagClient as ReflagNodeClient } from "@reflag/node-sdk"; - -const serverClient = new ReflagNodeClient({ secretKey: "your-secret-key" }); -await serverClient.initialize(); - -const bootstrappedState = serverClient.getFlagsForBootstrap({ - user: { id: "user123", name: "John Doe", email: "john@acme.com" }, - company: { id: "company456", name: "Acme Inc", plan: "enterprise" }, -}); - -// Pass the bootstrapped state to the client using your framework's preferred method -app.get("/", (req, res) => { - res.set("Content-Type", "text/html"); - res.send( - Buffer.from( - ` -
`, - ), - ); -}); - -// Client-side: Initialize with pre-fetched evaluated state import { ReflagClient } from "@reflag/browser-sdk"; -const reflagClient = new ReflagClient({ +const reflag = new ReflagClient({ publishableKey: "your-publishable-key", - bootstrappedState, // Contains context, flags, and optional flagStateVersion + user: { id: "alice", role: "manager" }, + company: { id: "acme", plan: "enterprise" }, }); +await reflag.initialize(); -await reflagClient.initialize(); -const { isEnabled } = reflagClient.getFlag("huddle"); -``` - -The `bootstrappedState` object contains: - -- `context`: the evaluation context used on the server -- `flags`: the evaluated raw flags -- `flagStateVersion`: an optional version used to avoid redundant live-update refreshes immediately after bootstrapping - -If you want live flag updates to continue working after bootstrapping, use a recent `@reflag/node-sdk` so `getFlagsForBootstrap()` includes `flagStateVersion`. +const enabled = reflag.getFlag("huddles"); // boolean, implicit false +const settings = reflag.getFlag("assistant-settings", { model: "quick" }); +// settings: { model: string } -If a bootstrapped application requests opt-in flags, the browser SDK performs one flags refresh. Applications that do not request opt-in data do not make this request. - -If you previously used `bootstrappedFlags`, migrate like this: - -```typescript -// Before -const { flags } = serverClient.getFlagsForBootstrap(context); -const client = new ReflagClient({ - publishableKey, - user: context.user, - company: context.company, - other: context.other, - bootstrappedFlags: flags, -}); - -// After -const bootstrappedState = serverClient.getFlagsForBootstrap(context); -const client = new ReflagClient({ - publishableKey, - bootstrappedState, -}); -``` - -> [!NOTE] -> After bootstrapping, any live flag updates are fetched directly by the browser SDK from Reflag using the browser-visible context. If your bootstrapped snapshot depends on server-only or secret context that is not available in the browser, later live refreshes may differ. In that case, keep `enableLiveFlagUpdates` disabled. - -This eliminates loading states and removes the initial render's dependency on the flags API. - -## Context management - -### Updating user/company/other context - -Attributes given for the user/company/other context in the ReflagClient constructor can be updated for use in flag targeting evaluation with the `updateUser()`, `updateCompany()` and `updateOtherContext()` methods. -They return a promise which resolves once the flags have been re-evaluated follow the update of the attributes. - -> [!NOTE] > `user`/`company` attributes are also stored remotely on the Reflag servers and will automatically be used to evaluate flag targeting if the page is refreshed. - -### setContext() - -The `setContext()` method allows you to replace the entire context (user, company, and other attributes) at once. This method is useful when you need to completely change the context, such as when a user logs in or switches between different accounts. - -```ts -await reflagClient.setContext({ - user: { - id: "new-user-123", - name: "Jane Doe", - email: "jane@example.com", - role: "admin", - }, - company: { - id: "company-456", - name: "New Company Inc", - plan: "enterprise", - }, - other: { - feature: "beta", - locale: "en-US", - }, -}); +if (enabled) { + // Application usage tracking is separate from reading the flag. + await reflag.track("Started huddle"); +} ``` -The method will: - -- Replace the entire context with the new values -- Re-evaluate all flags based on the new context -- Update the user and company information on Reflag servers -- Return a promise that resolves once the flags have been re-evaluated +Use only a **publishable** key in a browser. Secret keys belong on your server. -### getContext() - -The `getContext()` method returns the current context being used for flag evaluation. This is useful for debugging or when you need to inspect the current user, company, and other attributes. +## Direct-value reads ```ts -const currentContext = reflagClient.getContext(); -console.log(currentContext); -// { -// user: { id: "user-123", name: "John Doe", email: "john@example.com" }, -// company: { id: "company-456", name: "Acme Inc", plan: "enterprise" }, -// other: { locale: "en-US", feature: "beta" } -// } +reflag.getFlag("release"); // boolean; missing/mismatched -> false +reflag.getFlag("model", "quick"); // string +reflag.getFlag("limit", 10); // number +reflag.getFlag("settings", { model: "quick" }); +reflag.getFlag("nullable-settings", null); // FlagValue (arbitrary JSON) ``` -The returned context object contains: +- Defaults are positional JSON values; generation is not required for useful types. Primitive literals widen, and readonly defaults are accepted. +- A non-null default accepts only the same **top-level JSON category**: Boolean, number, string, array or object. A mismatch warns without logging the payload and returns the default. A bare read can never return a truthy object. +- Explicit `null` accepts any JSON value, including authored null. Missing values return null. +- Falsy selected values (`false`, `0`, `""`, null) are preserved when accepted by the requested category. +- Object fields and array elements are **not schema-validated**. Validate untrusted/nested payloads in your application. +- Values and defaults are returned as independent mutable copies. There are no reserved JSON property names. -- `user`: Current user attributes (if any) -- `company`: Current company attributes (if any) -- `other`: Additional context attributes not related to user or company +`getFlags()` returns a direct-value record (entries may be undefined) and registers checks for its returned roots. `getFlagResults()` returns owned evaluated diagnostics **without checks**; it is intended for tooling, not ordinary application reads. `getFlagsForBootstrap()` serializes protocol-v2 evaluated state without checks. -## Toolbar +## Checks and delivery -The Reflag Toolbar is great for toggling flags on/off for yourself to ensure that everything works both when a flag is on and when it's off. +Calling a getter registers a requested-root check immediately, even if its return value is ignored. There are no `.isEnabled`, `.config`, `.value` or flag `.track()` wrappers. Delivery remains buffered through `/bulk` and identity-aware rate-limited. -Toolbar screenshot +A selected check carries the authored selected key as `evalResult`, its `sourceVersionId` and the available snapshot `flagStateVersion`. Payload equality is not identity: equal-valued selections or segment-only snapshot changes remain distinguishable on subsequent reads. Defaults, category mismatches, local overrides and static fallback values do not fabricate authored selections. Diagnostics and bootstrap serialization generate no checks. The server evaluates dependencies; fetching their results does not itself register reads. -The toolbar will automatically appear on `localhost`. However, it can also be incredibly useful in production. -You have full control over when it appears through the `toolbar` configuration option passed to the `ReflagClient`. +`enableTracking: false` disables checks and user/company/usage tracking. `offline: true` skips HTTP and tracking. `await reflag.stop()` flushes buffered events and closes the client's background transports. -You can pass a simple boolean to force the toolbar to appear/disappear: +## Context, refresh and cache -```typescript -const client = new ReflagClient({ - // show the toolbar even in production if the user is an internal/admin user - toolbar: user?.isInternal, - ... +```ts +await reflag.updateUser({ role: "admin" }); +await reflag.updateCompany({ plan: "enterprise" }); +await reflag.updateOtherContext({ region: "eu" }); +await reflag.setContext({ + user: { id: "bob" }, + company: { id: "other-company" }, }); +await reflag.refresh(); ``` -You can also configure the position of the toolbar on the screen: - -```typescript -const client = new ReflagClient({ - toolbar: { - show: true; - position: { - placement: "bottom-left", - offset: {x: "1rem", y: "1rem"} - } - } - ... -}) -``` - -See [the reference](https://docs.reflag.com/supported-languages/browser-sdk/globals#toolbaroptions) for details. - -## Qualitative feedback on beta flags - -Reflag can collect qualitative feedback from your users in the form of a [Customer Satisfaction Score](https://en.wikipedia.org/wiki/Customer_satisfaction) and a comment. +`setContext()` replaces the context; update methods shallow-merge their section. User/company sections require IDs, including valid numeric zero. Context supports nested JSON and arrays. A context change clears the old context's values while fetching; stale/in-flight responses cannot replace a newer context or snapshot. -### Automated feedback collection +Values come from `GET /flags/evaluated`, not legacy `/features/evaluated`. Server evaluation retains the complete internal graph and authorized public roots. The Browser SDK does not cache dependency evaluations of its own. -The Reflag Browser SDK comes with automated feedback collection mode enabled by default, which lets the Reflag service ask your users for feedback for relevant flags just after they've used them. +Constructor options include: -> [!NOTE] -> To get started with automatic feedback collection, make sure you've set `user` in the `ReflagClient` constructor. +| Option | Behavior | +| ------------------------------- | --------------------------------------------------------------------------- | +| `apiBaseUrl`, `appBaseUrl` | API origin and toolbar/feedback links | +| `credentials` | Fetch credentials mode; `include` also enables default SSE credentials | +| `timeoutMs` | Flag request timeout; default 5,000 ms | +| `staleTimeMs` | Fresh-cache duration; default 0 | +| `staleWhileRevalidate` | Serve stale cached state while revalidating; default false | +| `expireTimeMs` | Cache expiry; default 30 days | +| `storage` | Optional async storage adapter | +| `fallbackFlags` | Value record or array of enabled Boolean keys, without authored provenance | +| `enableLiveFlagUpdates` | Refresh after SSE publications; default false in Browser, true in React/Vue | +| `eventSourceFactory` | Optional EventSource-compatible transport | +| `trackingQueue` | Buffered-delivery `flushDelayMs` and `maxSize` | +| `logger`, `toolbar`, `feedback` | Logging, Boolean override toolbar and feedback options | -Automated feedback surveys work even if you're not using the SDK to send events to Reflag. -It works because the Reflag Browser SDK maintains a live connection to Reflag's servers and can automatically show a feedback prompt whenever the Reflag servers determines that an event should trigger a prompt - regardless of how this event is sent to Reflag. +Evaluated cache and override storage use isolated v2 keys. V1 cache entries are never interpreted as v2 values. Malformed refreshes leave known-good state intact. Valid saved v2 selections retain their real provenance on restore. There is no automatic periodic HTTP fallback after an SSE failure; manual refresh/context updates remain available. -You can find all the options to make changes to the default behavior in the [Reflag feedback documentation](./FEEDBACK.md). - -### Reflag feedback UI - -Reflag can assist you with collecting your user's feedback by offering a pre-built UI, allowing you to get started with minimal code and effort. - -[Read the Reflag feedback UI documentation](./FEEDBACK.md) - -### Reflag feedback SDK - -Feedback can be submitted to Reflag using the SDK: +## Overrides and toolbar ```ts -reflagClient.feedback({ - flagKey: "my-flag-key", // String (required), copy from Flag feedback tab - score: 5, // Number: 1-5 (optional) - comment: "Absolutely stellar work!", // String (optional) -}); +reflag.setFlagOverride("release", true); +reflag.setFlagOverride("settings", { model: "local" }); +reflag.setFlagOverride("nullable-settings", null); // a real null override +reflag.setFlagOverride("release", undefined); // clear the override ``` -### Reflag feedback API - -If you are not using the Reflag Browser SDK, you can still submit feedback using the HTTP API. - -See details in [Feedback HTTP API](https://docs.reflag.com/api/http-api#post-feedback) +Overrides apply after server evaluation and do not rewrite authored dependency results. The built-in toolbar appears on localhost by default and offers Boolean overrides only. It uses diagnostic access, so opening it does not register application checks. -## Tracking flag usage +## Server bootstrap -The `track` function lets you send events to Reflag to denote flag usage. -By default Reflag expects event names to align with the flag keys, but -you can customize it as you wish. +Generate the full payload with **Node SDK v2** on your server: ```ts -reflagClient.track("huddle", { voiceHuddle: true }); +const bootstrappedState = serverClient.getFlagsForBootstrap(context); +// Send via your framework's safe JSON/hydration mechanism. ``` -## Event listeners - -Event listeners allow for capturing various events occurring in the `ReflagClient`. This is useful to build integrations with other system or for various debugging purposes. The available events are: - -- `check`: Your code used `isEnabled` or `config` for a flag -- `flagsUpdated`: Flags were updated. Either because they were loaded as part of initialization or because the user/company updated -- `optInFlagsLoadingUpdated`: The opt-in flag loading state changed -- `user`: User information updated (similar to the `identify` call used in tracking terminology) -- `company`: Company information updated (sometimes to the `group` call used in tracking terminology) -- `track`: Track event occurred. - -Use the `on()` method to add an event listener to respond to certain events. See the API reference for details on each hook. - ```ts -import { ReflagClient, CheckEvent, RawFlags } from "@reflag/browser-sdk"; - -const client = new ReflagClient({ - // options -}); - -// or add the hooks after construction: -const unsub = client.on("check", (check: CheckEvent) => - console.log(`Check event ${check}`), -); -// use the returned function to unsubscribe, or call `off()` with the same arguments again -unsub(); +const reflag = new ReflagClient({ publishableKey, bootstrappedState }); +await reflag.initialize(); +const enabled = reflag.getFlag("release"); ``` -## Zero PII - -The Reflag Browser SDK doesn't collect any metadata and HTTP IP addresses are _not_ being stored. +The payload includes `protocolVersion: 2`, context, evaluated diagnostics/selections and an optional snapshot version. V1 payloads and the old `bootstrappedFlags` option are rejected. Preserve the entire envelope; do not pass just its `flags` property. A version is required for bootstrapped live updates. Never embed secrets or server-only context in public bootstrap data. Later refreshes use browser-visible context and can differ from server-only evaluation; disable live updates when that is inappropriate. -For tracking individual users, we recommend using something like database ID as userId, as it's unique and doesn't include any PII (personal identifiable information). If, however, you're using e.g. email address as userId, but prefer not to send any PII to Reflag, you can hash the sensitive data before sending it to Reflag: +## End-user opt-in ```ts -import reflag from "@reflag/browser-sdk"; -import { sha256 } from "crypto-hash"; - -reflag.user(await sha256("john_doe")); +const options = reflag.getOptInFlags(); +const loading = reflag.getIsLoadingOptInFlags(); +await reflag.setOptIn("huddles", { optedIn: true }); +await reflag.setOptIn("huddles", { optedIn: false, scope: "company" }); ``` -## Use of cookies - -The Reflag Browser SDK uses a couple of cookies to support automated feedback surveys. These cookies are not used for tracking purposes and thus should not need to appear in cookie consent forms. - -The two cookies are: - -- `reflag-prompt-${userId}`: store the last automated feedback prompt message ID received to avoid repeating surveys -- `reflag-token-${userId}`: caching a token used to connect to Reflag's live messaging infrastructure that is used to deliver automated feedback surveys in real time. - -## TypeScript - -Types are bundled together with the library and exposed automatically when importing through a package manager. - -## Content Security Policy (CSP) +The list contains presentation/membership metadata (`key`, `name`, `description`, `isEnabled`, `userOptedIn`, `companyOptedIn`, `isOptedIn`); this is not the ordinary flag getter API. User/company grants remain independent. The selected scope requires its ID. Successful mutations wait for versioned evaluation and membership confirmation. -If you are running with strict Content Security Policies active on your website, you will need to enable these directives in order to use the SDK: +Metadata arrives with ordinary `/flags/evaluated` responses and is cached with the evaluated snapshot. Bootstrapped clients whose snapshot lacks metadata fetch it on demand from the same v2 endpoint, without replacing server-derived values or provenance with browser-context evaluations. Loading settles on success or failure, including an empty list. Opt-in mutations confirm values and membership together in one versioned refresh. `optInFlagsLoadingUpdated` and `flagsUpdated` support UI subscriptions. -| Directive | Values | Reason | -| ----------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| connect-src | [https://front.reflag.com](https://front.reflag.com) | API requests plus Server-Sent Events for live flag updates and automated feedback surveys. | -| style-src | 'unsafe-inline' | The feedback UI is styled with inline styles. Not having this directive results unstyled HTML elements. | +## Tracking, feedback and events -If you are including the Reflag tracking SDK with a ` - - - -``` - -If using Nuxt, wrap `` in ``. `` only renders client-side currently. - -### 2. Use `useFlag get flag status - -```vue - - - -``` - -See [useFlag()](#useflag) for a full example - -## Setting context - -Reflag determines which flags are active for a given `user`, `company`, or `other` context. -You can pass these to the `ReflagProvider` using the `context` prop. - -### Using the `context` prop - -```vue - - - -``` - -### Legacy individual props (deprecated) - -For backward compatibility, you can still use individual props, but these are deprecated and will be removed in the next major version: - -```vue - - - -``` - -> [!Important] -> The `user`, `company`, and `otherContext` props are deprecated. Use the `context` prop instead, which provides the same functionality in a more structured way. - -### Context requirements - -If you supply `user` or `company` objects, they must include at least the `id` property otherwise they will be ignored in their entirety. -In addition to the `id`, you must also supply anything additional that you want to be able to evaluate flag targeting rules against. -Attributes which are not properties of the `user` or `company` can be supplied using the `other` property. - -Attributes cannot be nested (multiple levels) and must be either strings, numbers or booleans. -A number of special attributes exist: - -- `name` -- display name for `user`/`company`, -- `email` -- the email of the user, -- `avatar` -- the URL for `user`/`company` avatar image. - -To retrieve flags along with their targeting information, use `useFlag(key: string)` hook (described in a section below). - -Note that accessing `isEnabled` on the object returned by `useFlag()` automatically -generates a `check` event. - -## Remote config - -Remote config is a dynamic and flexible approach to configuring flag behavior outside of your app – without needing to re-deploy it. - -Similar to `isEnabled`, each flag accessed using the `useFlag()` hook, has a `config` property. This configuration is managed from within Reflag. It is managed similar to the way access to flags is managed, but instead of the -binary `isEnabled` you can have multiple configuration values which are given to different user/companies. - -### Get started with Remote config - -```ts -const { - isEnabled, - config: { key, payload }, -} = useFlag("huddles"); - -// isEnabled: true, -// key: "gpt-3.5", -// payload: { maxTokens: 10000, model: "gpt-3.5-beta1" } -``` - -`key` is mandatory for a config, but if a flag has no config or no config value was matched against the context, the `key` will be `undefined`. Make sure to check against this case when trying to use the configuration in your application. `payload` is an optional JSON value for arbitrary configuration needs. - -Note that, similar to `isEnabled`, accessing `config` on the object returned by `useFlag()` automatically -generates a `check` event. - -## `` component - -The `` initializes the Reflag SDK, fetches flags and starts listening for automated feedback survey events. The component can be configured using a number of props: - -- `publishableKey` is used to connect the provider to an _environment_ on Reflag. Find your `publishableKey` under [environment settings](https://app.reflag.com/env-current/settings/app-environments) in Reflag, -- `context`: An object containing `user`, `company`, and `other` properties that make up the evaluation context used to determine if a flag is enabled or not. `company` and `user` contexts are automatically transmitted to Reflag servers so the Reflag app can show you which companies have access to which flags etc. -- `company`, `user` and `otherContext` (deprecated): Individual props for context. These are deprecated in favor of the `context` prop and will be removed in the next major version. - - > [!Note] - > If you specify `company` and/or `user` they must have at least the `id` property, otherwise they will be ignored in their entirety. You should also supply anything additional you want to be able to evaluate flag targeting against, - -- `timeoutMs`: Timeout in milliseconds when fetching flags from the server, -- `staleWhileRevalidate`: If set to `true`, stale flags will be returned while refetching flags in the background, -- `expireTimeMs`: If set, flags will be cached between page loads for this duration (in milliseconds), -- `staleTimeMs`: Maximum time (in milliseconds) that stale flags will be returned if `staleWhileRevalidate` is true and new flags cannot be fetched. - -- `enableTracking`: Set to `false` to stop sending tracking events and user/company updates to Reflag. Useful when you're impersonating a user (defaults to `true`), -- `apiBaseUrl`: Optional base URL for the Reflag API. This also controls the SSE origin used for live flag updates and automated feedback, -- `credentials`: Optional fetch credentials mode. Set to `"include"` when proxying through your backend and authenticating with cookies; this also enables credentials for live-update SSE connections. -- `appBaseUrl`: Optional base URL for the Reflag application. Use this to override the default app URL, - -- `debug`: Set to `true` to enable debug logging to the console. If both `logger` and `debug` are provided, `logger` takes precedence, -- `logger`: Optional custom logger implementation (`debug`, `info`, `warn`, `error`) used by the underlying client, -- `toolbar`: Optional [configuration](https://docs.reflag.com/supported-languages/browser-sdk/globals#toolbaroptions) for the Reflag toolbar, -- `feedback`: Optional configuration for feedback collection - -### Loading states - -ReflagProvider lets you define a template to be shown while ReflagProvider is initializing: - -```vue ``` -If you want more control over loading screens, `useIsLoading()` returns a `Ref` which you can use to customize the loading experience. - -## `` component - -The `` component is a specialized version of `ReflagProvider` designed for server-side rendering and preloaded flag scenarios. It uses pre-fetched flags for the initial render, resulting in faster initial page loads and better SSR compatibility. +Provider options forward Browser settings, including cache/storage, fallback values, offline/tracking, feedback, toolbar, credentials and API origins. `logger` takes precedence over `debug`. Live updates default to true; disable with `:enable-live-flag-updates="false"`. For an ordinary client-only Nuxt provider, wrap it in ``. -### Usage +## `useFlag(key, defaultValue?)` ```vue - - -``` - -### Getting bootstrapped flags - -You'll typically generate the `bootstrappedFlags` object on your server using the Node.js SDK or by fetching from the Reflag API. Pass the full object returned by `getFlagsForBootstrap()` directly to ``. It contains: - -- `context`: the evaluation context used on the server -- `flags`: the evaluated raw flags -- `flagStateVersion`: an optional version used to avoid redundant live-update refreshes immediately after bootstrapping - -If you want live flag updates to continue working after bootstrapping, use a recent `@reflag/node-sdk` so `getFlagsForBootstrap()` includes `flagStateVersion`. - -With `ReflagBootstrappedProvider`, `useOptInFlags()` triggers one flags refresh and reports `isLoading: true` until it settles. No refresh occurs unless the composable is used. - -Here's an example using the Node.js SDK: - -```js -// server.js (Node.js/SSR) -import { ReflagClient } from "@reflag/node-sdk"; - -const client = new ReflagClient({ - secretKey: "your-secret-key", // Use secret key on server -}); -await client.initialize(); +import { useFlag, useIsLoading, useTrack } from "@reflag/vue-sdk"; -// Fetch flags for specific context -const context = { - user: { id: "user123", name: "John Doe", email: "john@acme.com" }, - company: { id: "company456", name: "Acme Inc", plan: "enterprise" }, -}; - -const bootstrappedFlags = client.getFlagsForBootstrap(context); - -// Pass to your Vue app -``` - -### ReflagBootstrappedProvider Props - -`ReflagBootstrappedProvider` accepts all the same props as `ReflagProvider` except: - -- `flags`: The pre-fetched bootstrapped state object containing `context`, evaluated `flags`, and an optional `flagStateVersion` -- All other props available in `ReflagProvider` are supported except `context`, `user`, `company`, and `otherContext` (which are extracted from `flags.context`) - -If the `flags` prop is not provided or is undefined, the provider will not initialize the client and will render in a non-loading state. - -> [!NOTE] -> The on-demand browser refresh and any later live flag updates use the browser-visible context. If your bootstrapped snapshot depends on server-only or secret context that is not available in the browser, refreshed flags may differ. In that case, keep `enableLiveFlagUpdates` disabled. - -## `` component - -The `` is a lower-level component that accepts a pre-initialized `ReflagClient` instance. This is useful for advanced use cases where you need full control over client initialization or want to share a client instance across multiple parts of your application. - -### ReflagClientProvider Usage - -```vue - ``` -### ReflagClientProvider Props - -The `ReflagClientProvider` accepts the following props: +Vue unwraps refs in templates; use `.value` in script. This is a **Vue ref**, not a flag wrapper. There are no `.isEnabled`, `.config`, flag `.track()` or flag loading fields. -- `client`: A pre-initialized `ReflagClient` instance +```ts +useFlag("release"); // ShallowRef +useFlag("model", "quick"); // ShallowRef +useFlag("limit", 10); // ShallowRef +useFlag("settings", { model: "quick" }); // ShallowRef<{ model: string }> +useFlag("nullable-settings", null); // ShallowRef +``` -### Slots +Defaults are positional JSON; literals widen without generated types. Non-null defaults require the same top-level category (Boolean/number/string/array/object), otherwise the default is returned with a payload-safe warning. Null explicitly accepts arbitrary JSON, including authored null; missing returns null. Valid false/zero/empty strings are preserved. Nested payload schemas are not validated by the SDK. Returned values/defaults are independent copies; readonly defaults are accepted. -- `loading`: Optional slot to show while the client is initializing (same as `ReflagProvider`) +The ref updates when evaluated flags change. A composable read registers the requested root through the Browser SDK, even if its result is ignored; refresh-driven rereads are identity-aware rate-limited and delivered in batches. Diagnostics/bootstrap inspection register none. Checks carry genuine selected key/source/snapshot identity, never payloads or invented selections for defaults/overrides. -> [!Note] -> Most applications should use `ReflagProvider` or `ReflagBootstrappedProvider` instead of `ReflagClientProvider`. Only use this component when you need the advanced control it provides. +## Loading, context and client access -## Hooks +- `useIsLoading()` returns a Boolean ref for provider loading; the `#loading` slot and `initialLoading` control initial presentation. +- `useUpdateUser()`, `useUpdateCompany()` and `useUpdateOtherContext()` return async shallow-update functions. +- `useClient()` exposes the Browser client, including `setContext(context)`, `refresh()` and `setFlagOverride(key, value)`. Undefined clears an override; null is a JSON override. +- `getFlags()` returns direct values and registers returned-root checks. Use `getFlagResults()` for tooling without checks. +- `useOnEvent(type, handler, client?)` subscribes on mount and unsubscribes on unmount. An external client may be supplied explicitly. -### `useFlag()` +A context change clears old values while fetching. Late responses for previous contexts or lower snapshot versions cannot replace accepted state. V2 cache/override storage is isolated from v1. Browser fetching uses `/flags/evaluated` and never re-evaluates/caches dependencies locally. -Returns the state of a given flag for the current context. The composable provides access to flags and their configurations. +## Bootstrap -`useFlag()` returns an object with this shape: +On the server, use Node SDK v2 and your framework's safe serialization mechanism: ```ts -{ - isEnabled: boolean, // is the flag enabled - track: () => void, // send a track event when the flag is used - requestFeedback: (...) => void // open up a feedback dialog - config: {key: string, payload: any}, // remote configuration for this flag - isLoading: boolean // if you want to manage loading state at the flag level -} +const flags = serverClient.getFlagsForBootstrap(context); ``` -Example: - ```vue - - - + + + ``` -See the reference docs for details. - -### `useOptInFlags()` and `useSetOptIn()` +Pass the entire envelope (`protocolVersion: 2`, context, evaluated flags and optional snapshot version). Legacy v1 bootstrap is rejected. A snapshot version is required for live updates. Never expose secret keys/server-only attributes in bootstrap data; later refreshes use browser-visible context, so disable live updates when they must not replace server-only evaluations. -Use these composables to build an end-user opt-in UI for flags where opt-in is enabled in Reflag. +`ReflagClientProvider` accepts an externally initialized Browser v2 client. Its lifecycle belongs to the application. -```vue - - - -``` - -By default, `useSetOptIn()` changes the opt-in for the current user, so the current context must include a `user.id`. To manage the current company's opt-in instead, pass `scope: "company"`; the context must then include a `company.id`. - -User and company opt-ins are managed independently. Setting `optedIn` to `false` removes the opt-in only for the selected scope. For example, cancelling a user's opt-in does not change the company's opt-in for the same flag. - -`setOptIn` returns a promise so you can wait for the new membership state to be synchronized. It resolves after the latest flag state has been applied, the requested membership change has been confirmed, and components using `useOptInFlags()` have been notified. Vue schedules the resulting render normally, so it may not yet be committed when the promise resolves. - -`useOptInFlags()` returns `{ flags, isLoading }`, where both values are computed refs. With `ReflagBootstrappedProvider`, the composable fetches opt-in metadata on first use and reports `isLoading: true` until the flags refresh succeeds or fails. - -With a regular `ReflagProvider`, opt-in metadata arrives as part of the normal initial flags request, so `useOptInFlags().isLoading` remains `false`. Use the general `useIsLoading()` composable or the provider's loading slot for that initial loading state. - -### `useTrack()` - -`useTrack()` returns a function which lets you send custom events to Reflag. It takes a string argument with the event name and optionally an object with properties to attach the event. - -Using `track` returned from `useFlag()` calls this track function with the flag key as the event name. - -```vue - - - -``` - -### `useRequestFeedback()` - -Returns a function that lets you open up a dialog to ask for feedback on a specific feature. This is useful for collecting targeted feedback about specific features. - -See [Automated Feedback Surveys](https://docs.reflag.com/product-handbook/live-satisfaction) for how to do this automatically, without code. - -When using the `useRequestFeedback` you must pass the flag key to `requestFeedback`. -The example below shows how to use `position` to ensure the popover appears next to the "Give feedback!" button. - -```vue - - - -``` - -See the [Feedback Documentation](https://github.com/reflagcom/javascript/blob/main/packages/browser-sdk/FEEDBACK.md#manual-feedback-collection) for more information on `requestFeedback` options. - -### `useSendFeedback()` - -Returns a function that lets you send feedback to Reflag. This is useful if you've manually collected feedback through your own UI and want to send it to Reflag. - -```vue - - - -``` - -### `useUpdateUser()`, `useUpdateCompany()` and `useUpdateOtherContext()` - -These composables return functions that let you update the attributes for the currently set user, company, or other context. Updates to user/company are stored remotely and affect flag targeting, while "other" context updates only affect the current session. - -```vue - - - -``` - -Note: To change the `user.id` or `company.id`, you need to update the props passed to `ReflagProvider` instead of using these composables. - -### `useClient()` - -Returns the `ReflagClient` used by the `ReflagProvider`. The client offers more functionality that -is not directly accessible through the other composables. - -```vue - - - +await sendFeedback({ flagKey: "huddles", score: 5 }); ``` -### `useIsLoading()` - -Returns a `Ref` to indicate if Reflag has finished loading. -Initially, the value will be `true` if no bootstrap flags have been provided and the client has not be initialized. +Opt-in metadata arrives with ordinary `/flags/evaluated` responses. Bootstrapped providers fetch missing metadata on demand from the same v2 endpoint without replacing server-derived values or provenance; loading settles on success or failure. User/company grants remain independent; successful mutations confirm versioned evaluation and membership in a single refresh. The metadata list retains presentation/membership fields such as `isEnabled`; ordinary reads remain direct values. -```vue - +`enableTracking: false` disables checks and ordinary tracking. Offline mode skips HTTP/tracking. See [Browser feedback documentation](../browser-sdk/FEEDBACK.md) for prompting/UI options. - -``` +## Optional key registration and migration -### `useOnEvent()` - -Vue composable for listening to Reflag client events. This composable automatically handles mounting and unmounting of event listeners. - -Available events include: - -- `flagsUpdated`: Triggered when flags are updated -- `track`: Triggered when tracking events are sent -- `feedback`: Triggered when feedback is sent - -```vue - - - -``` - -You can also provide a specific client instance if needed: - -```vue - - - +```ts +import "@reflag/vue-sdk"; +declare module "@reflag/vue-sdk" { + interface Flags { + huddles: boolean; + "huddle-settings": { maxParticipants: number }; + } +} ``` -## Content Security Policy (CSP) - -See [CSP](https://github.com/reflagcom/javascript/blob/main/packages/browser-sdk/README.md#content-security-policy-csp) for info on using Reflag React SDK with CSP - -## License - -MIT License +Registration constrains keys and checks non-null defaults against the registered value schema, including required and nested properties. Named-interface and readonly defaults are supported. Explicit `null` remains an arbitrary-JSON request; bare/`undefined` reads remain Boolean refs. Positional defaults still determine widened direct-value types, and generated declarations do not add nested runtime validation. The v2 CLI generates these schemas from current production-environment variants with `--format vue`; regenerate old wrapper declarations when migrating. -Copyright (c) 2025 Bucket ApS +Replace `const { isEnabled } = useFlag(key)` with `const enabled = useFlag(key)` (then `enabled.value` in script). Replace remote-config wrappers with separately authored multivariate flags and positional defaults, and move usage/feedback into their separate composables. Bootstrap both ends with v2. This branch does not publish SDKs or deploy the required server changes. diff --git a/packages/vue-sdk/dev/plain/App.vue b/packages/vue-sdk/dev/plain/App.vue index 77aa4e19b..1c799cc35 100644 --- a/packages/vue-sdk/dev/plain/App.vue +++ b/packages/vue-sdk/dev/plain/App.vue @@ -42,11 +42,14 @@ const isBootstrapped = computed(() => { v-else-if="isBootstrapped" :publishable-key="publishableKey" :flags="{ + protocolVersion: 2, context, flags: { huddles: { - key: 'huddles', - isEnabled: true, + value: true, + resolved: false, + ruleResults: [], + errors: [], }, }, }" diff --git a/packages/vue-sdk/dev/plain/components/FlagsList.vue b/packages/vue-sdk/dev/plain/components/FlagsList.vue index 6912ac6fe..0d6c770f2 100644 --- a/packages/vue-sdk/dev/plain/components/FlagsList.vue +++ b/packages/vue-sdk/dev/plain/components/FlagsList.vue @@ -1,117 +1,40 @@ - - + + diff --git a/packages/vue-sdk/dev/plain/components/StartHuddlesButton.vue b/packages/vue-sdk/dev/plain/components/StartHuddlesButton.vue index e26860c0e..350959836 100644 --- a/packages/vue-sdk/dev/plain/components/StartHuddlesButton.vue +++ b/packages/vue-sdk/dev/plain/components/StartHuddlesButton.vue @@ -1,35 +1,33 @@ + diff --git a/packages/vue-sdk/package.json b/packages/vue-sdk/package.json index 0e10b56f7..978f06abb 100644 --- a/packages/vue-sdk/package.json +++ b/packages/vue-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@reflag/vue-sdk", - "version": "1.5.3", + "version": "2.0.0-next.0", "license": "MIT", "repository": { "type": "git", @@ -25,7 +25,7 @@ "dev": "vite", "build": "tsc --project tsconfig.build.json && vite build", "test": "yarn test:types && vitest run", - "test:types": "tsc --project tsconfig.type-tests.json", + "test:types": "tsc --project tsconfig.type-tests.json && tsc --project tsconfig.type-tests-defaults.json && tsc --project tsconfig.type-tests-registered.json", "test:watch": "vitest", "test:ci": "yarn test --reporter=default --reporter=junit --outputFile=junit.xml", "coverage": "yarn test --coverage", @@ -38,7 +38,7 @@ "fmt:fix": "oxfmt --write ." }, "dependencies": { - "@reflag/browser-sdk": "1.7.0" + "@reflag/browser-sdk": "workspace:^" }, "devDependencies": { "@reflag/tsconfig": "^0.0.2", diff --git a/packages/vue-sdk/src/ReflagBootstrappedProvider.vue b/packages/vue-sdk/src/ReflagBootstrappedProvider.vue index 63216a4fb..af2169204 100644 --- a/packages/vue-sdk/src/ReflagBootstrappedProvider.vue +++ b/packages/vue-sdk/src/ReflagBootstrappedProvider.vue @@ -23,13 +23,14 @@ const client = useReflagClient({ logger, }); -const isLoading = ref( - client.getState() !== "initialized" ? initialLoading : false, -); +let hasInitialized = client.getState() === "initialized"; +const isLoading = ref(hasInitialized ? false : initialLoading); useOnEvent( "stateUpdated", (state) => { - isLoading.value = state === "initializing"; + if (state === "initialized") hasInitialized = true; + isLoading.value = + state === "initializing" && (hasInitialized || initialLoading); }, client, ); diff --git a/packages/vue-sdk/src/hooks.ts b/packages/vue-sdk/src/hooks.ts index 27fc70078..9b3f8fbee 100644 --- a/packages/vue-sdk/src/hooks.ts +++ b/packages/vue-sdk/src/hooks.ts @@ -5,6 +5,8 @@ import { onMounted, onUnmounted, ref, + shallowRef, + type ShallowRef, } from "vue"; import { @@ -20,8 +22,11 @@ import { FlagKey, OptInFlag, ProviderContextType, - RequestFlagFeedbackOptions, - TypedFlags, + Flags, + FlagDefaultFor, + FlagDefaultValue, + FlagValue, + FlagValueFromDefault, UseOptInFlagsResult, } from "./types"; import { SDK_VERSION } from "./version"; @@ -67,60 +72,32 @@ export function useReflagClient( return reflagClients.get(publishableKey)!; } -/** - * Vue composable for getting the state of a given flag for the current context. - * - * This composable returns an object with the state of the flag for the current context. - * - * @param key - The key of the flag to get the state of. - * @returns An object with the state of the flag. - * - * @example - * ```ts - * import { useFlag } from '@reflag/vue-sdk'; - * - * const { isEnabled, config, track, requestFeedback } = useFlag("huddles"); - * - * function StartHuddlesButton() { - * const { isEnabled, config: { payload }, track } = useFlag("huddles"); - * if (isEnabled) { - * return ; - * } - * ``` - */ -export function useFlag(key: TKey): TypedFlags[TKey] { +/** Reactive direct values: Boolean/false by default, or infer a positional JSON default. */ +export function useFlag( + key: FlagKey, + defaultValue?: undefined, +): ShallowRef; +export function useFlag< + Key extends FlagKey, + Default extends FlagDefaultFor, +>(key: Key, defaultValue: Default): ShallowRef>; +export function useFlag( + key: keyof Flags extends never ? string : never, + defaultValue: Default, +): ShallowRef>; +export function useFlag( + key: FlagKey, + defaultValue: FlagDefaultValue = false, +): ShallowRef { const client = useClient(); - const isLoading = useIsLoading(); - - const track = () => client.track(key); - const requestFeedback = (opts: RequestFlagFeedbackOptions) => - client.requestFeedback({ ...opts, flagKey: key }); - - const flag = ref( - client.getFlag(key) || { - isEnabled: false, - config: { key: undefined, payload: undefined }, - }, - ); - + const flag = shallowRef(client.getFlag(key, defaultValue)); const updateFlag = () => { - flag.value = client.getFlag(key); + flag.value = client.getFlag(key, defaultValue); }; - - onMounted(() => { - updateFlag(); - }); - - useOnEvent("flagsUpdated", updateFlag); - - return { - key, - isLoading, - isEnabled: computed(() => flag.value.isEnabled), - config: computed(() => flag.value.config), - track, - requestFeedback, - } as TypedFlags[TKey]; + useOnEvent("flagsUpdated", updateFlag, client); + // Catch updates between setup and listener registration. + onMounted(updateFlag); + return flag; } /** @@ -133,15 +110,15 @@ export function useFlag(key: TKey): TypedFlags[TKey] { */ export function useOptInFlags(): UseOptInFlagsResult { const client = useClient(); - const isBootstrapped = client.getConfig().bootstrapped; + const isLoading = useIsLoading(); const optInFlags = ref(client.getOptInFlags() as OptInFlag[]); const isOptInFlagsLoading = ref(client.getIsLoadingOptInFlags()); const updateOptInFlags = () => { optInFlags.value = client.getOptInFlags() as OptInFlag[]; }; - const updateIsLoading = (isLoading = client.getIsLoadingOptInFlags()) => { - isOptInFlagsLoading.value = isLoading; + const updateIsLoading = (loading = client.getIsLoadingOptInFlags()) => { + isOptInFlagsLoading.value = loading; }; useOnEvent("flagsUpdated", updateOptInFlags, client); @@ -154,7 +131,7 @@ export function useOptInFlags(): UseOptInFlagsResult { return { flags: computed(() => optInFlags.value), - isLoading: computed(() => isBootstrapped && isOptInFlagsLoading.value), + isLoading: computed(() => isLoading.value || isOptInFlagsLoading.value), }; } diff --git a/packages/vue-sdk/src/index.ts b/packages/vue-sdk/src/index.ts index 85b10c946..1cfa56847 100644 --- a/packages/vue-sdk/src/index.ts +++ b/packages/vue-sdk/src/index.ts @@ -20,11 +20,11 @@ export { } from "./hooks"; export type { BootstrappedFlags, - EmptyFlagRemoteConfig, - Flag, + FlagValue, + FlagDefaultValue, + FlagValueFromDefault, FlagKey, Flags, - FlagType, OptInFlag, ReflagBaseProps, ReflagBootstrappedProps, diff --git a/packages/vue-sdk/src/types.ts b/packages/vue-sdk/src/types.ts index b0430aa49..c5663011d 100644 --- a/packages/vue-sdk/src/types.ts +++ b/packages/vue-sdk/src/types.ts @@ -6,51 +6,31 @@ import type { InitOptions, Logger, OptInFlag as BrowserOptInFlag, - RawFlags, + FlagValue, + FlagDefaultFor, + FlagDefaultValue, + FlagValueFromDefault, ReflagClient, ReflagContext, RequestFeedbackData, UserContext, } from "@reflag/browser-sdk"; -export type EmptyFlagRemoteConfig = { key: undefined; payload: undefined }; - -export type FlagType = { - config?: { - payload: any; - }; +export type { + FlagValue, + FlagDefaultFor, + FlagDefaultValue, + FlagValueFromDefault, }; -export type FlagRemoteConfig = - | { - key: string; - payload: any; - } - | EmptyFlagRemoteConfig; - -export interface Flag< - TConfig extends FlagType["config"] = EmptyFlagRemoteConfig, -> { - key: string; - isEnabled: Ref; - isLoading: Ref; - config: Ref<({ key: string } & TConfig) | EmptyFlagRemoteConfig>; - track(): Promise | undefined; - requestFeedback: (opts: RequestFlagFeedbackOptions) => void; -} - // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface Flags {} - export type TypedFlags = keyof Flags extends never - ? Record - : { - [TypedFlagKey in keyof Flags]: Flags[TypedFlagKey] extends FlagType - ? Flag - : Flag; - }; - -export type FlagKey = keyof TypedFlags; + ? Record + : { [Key in keyof Flags]: Flags[Key] | undefined }; +export type FlagKey = keyof Flags extends never + ? string + : Extract; export type OptInFlag = Omit & { key: FlagKey; @@ -66,9 +46,7 @@ export interface ProviderContextType { isLoading: Ref; } -export type BootstrappedFlags = BrowserBootstrappedState & { - flags: RawFlags; -}; +export type BootstrappedFlags = BrowserBootstrappedState; export type RequestFlagFeedbackOptions = Omit< RequestFeedbackData, diff --git a/packages/vue-sdk/test/opt-in-types.ts b/packages/vue-sdk/test/opt-in-types.ts index a5c0e05f1..690b95e02 100644 --- a/packages/vue-sdk/test/opt-in-types.ts +++ b/packages/vue-sdk/test/opt-in-types.ts @@ -1,8 +1,9 @@ -import { useOptInFlags, useSetOptIn } from "@reflag/vue-sdk"; +import { useFlag, useOptInFlags, useSetOptIn } from "@reflag/vue-sdk"; declare module "@reflag/vue-sdk" { interface Flags { - "generated-opt-in-flag": {}; + "generated-opt-in-flag": boolean; + legacy: { isEnabled: boolean; config: { payload: string } }; } } @@ -19,5 +20,10 @@ export function GeneratedOptInTypes() { // @ts-expect-error generated flag types still reject unknown literal keys void setOptIn("unknown-flag", { optedIn: true }); - return loading; + const enabled: boolean = useFlag("generated-opt-in-flag").value; + // @ts-expect-error old registered wrappers cannot change the Boolean contract + void useFlag("legacy").value.isEnabled; + // @ts-expect-error registration still constrains singular keys + useFlag("unknown-flag"); + return loading || enabled; } diff --git a/packages/vue-sdk/test/usage.test.ts b/packages/vue-sdk/test/usage.test.ts index 66cf0c19c..5e14dd063 100644 --- a/packages/vue-sdk/test/usage.test.ts +++ b/packages/vue-sdk/test/usage.test.ts @@ -18,14 +18,8 @@ import { beforeAll(() => { vi.spyOn(ReflagClient.prototype, "initialize").mockResolvedValue(undefined); vi.spyOn(ReflagClient.prototype, "stop").mockResolvedValue(undefined); - vi.spyOn(ReflagClient.prototype, "getFlag").mockReturnValue({ - isEnabled: true, - config: { key: "default", payload: { message: "Hello" } }, - track: vi.fn().mockResolvedValue(undefined), - requestFeedback: vi.fn(), - setIsEnabledOverride: vi.fn(), - isEnabledOverride: null, - }); + vi.spyOn(ReflagClient.prototype, "getFlag").mockReturnValue(true); + vi.spyOn(ReflagClient.prototype, "getState").mockReturnValue("initialized"); vi.spyOn(ReflagClient.prototype, "getFlags").mockReturnValue({}); vi.spyOn(ReflagClient.prototype, "getOptInFlags").mockReturnValue([]); vi.spyOn(ReflagClient.prototype, "getIsLoadingOptInFlags").mockReturnValue( @@ -42,6 +36,7 @@ beforeAll(() => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(ReflagClient.prototype.getFlag).mockReturnValue(true); vi.mocked(ReflagClient.prototype.getOptInFlags).mockReturnValue([]); vi.mocked(ReflagClient.prototype.getIsLoadingOptInFlags).mockReturnValue( false, @@ -60,6 +55,64 @@ function getProvider() { } describe("ReflagProvider", () => { + test("useFlag returns a direct reactive value, forwards positional defaults and unsubscribes", async () => { + let update: (() => void) | undefined; + const unsubscribe = vi.fn(); + vi.mocked(ReflagClient.prototype.on).mockImplementation( + (event, handler) => { + if (event === "flagsUpdated") update = handler as () => void; + return unsubscribe; + }, + ); + vi.mocked(ReflagClient.prototype.getFlag) + .mockReturnValueOnce(false) + .mockReturnValue(false); + const Child = defineComponent({ + setup() { + return { enabled: useFlag("release") }; + }, + template: "{{ enabled }}", + }); + const wrapper = mount(ReflagProvider, { + props: { publishableKey: "reactive-direct-value" }, + slots: { default: () => h(Child) }, + }); + await nextTick(); + expect(wrapper.text()).toBe("false"); + expect(ReflagClient.prototype.getFlag).toHaveBeenCalledWith( + "release", + false, + ); + vi.mocked(ReflagClient.prototype.getFlag).mockReturnValue(true); + update?.(); + await nextTick(); + expect(wrapper.text()).toBe("true"); + wrapper.unmount(); + expect(unsubscribe).toHaveBeenCalled(); + }); + + test("useFlag forwards JSON defaults without wrappers or truthiness fallback", async () => { + vi.mocked(ReflagClient.prototype.getFlag).mockReturnValue(null); + const fallback = { model: "default" }; + const Child = defineComponent({ + setup() { + return { settings: useFlag("settings", fallback) }; + }, + template: "{{ settings === null ? 'off' : settings.model }}", + }); + const wrapper = mount(ReflagProvider, { + props: { publishableKey: "reactive-json-default" }, + slots: { default: () => h(Child) }, + }); + await nextTick(); + expect(ReflagClient.prototype.getFlag).toHaveBeenCalledWith( + "settings", + fallback, + ); + expect(wrapper.text()).toBe("off"); + wrapper.unmount(); + }); + test("provides the client", async () => { const Child = defineComponent({ setup() { @@ -156,7 +209,7 @@ describe("ReflagProvider", () => { await nextTick(); expect(wrapper.findComponent(Child).vm.flags).toEqual(optInFlags); - expect(wrapper.findComponent(Child).vm.isLoading).toBe(false); + expect(wrapper.findComponent(Child).vm.isLoading).toBe(true); expect(ReflagClient.prototype.on).toHaveBeenCalledWith( "flagsUpdated", expect.any(Function), @@ -195,12 +248,16 @@ describe("ReflagProvider", () => { props: { publishableKey: "key-loading-opt-in-flags", flags: { + protocolVersion: 2, context: { user: { id: "user-1" } }, flags: { optInFlag: { - key: "optInFlag", - isEnabled: false, - targetingVersion: 1, + value: false, + resolved: true, + variantKey: "false", + sourceVersionId: "version-1", + ruleResults: [], + errors: [], }, }, }, @@ -348,15 +405,19 @@ describe("ReflagProvider", () => { describe("ReflagBootstrappedProvider", () => { const bootstrappedFlags = { + protocolVersion: 2 as const, context: { user: { id: "test-user" }, company: { id: "test-company" }, }, flags: { "test-flag": { - key: "test-flag", - isEnabled: true, - config: { key: "default", payload: { message: "Hello" } }, + value: true, + resolved: true, + variantKey: "true", + sourceVersionId: "version-1", + ruleResults: [], + errors: [], }, }, flagStateVersion: 1, @@ -382,7 +443,7 @@ describe("ReflagBootstrappedProvider", () => { await nextTick(); expect(wrapper.findComponent(Child).vm.client).toBeDefined(); - expect(wrapper.findComponent(Child).vm.flag.isEnabled.value).toBe(true); + expect(wrapper.findComponent(Child).vm.flag).toBe(true); }); test("enables live flag updates by default for bootstrapped providers", async () => { diff --git a/packages/vue-sdk/tsconfig.type-tests-defaults.json b/packages/vue-sdk/tsconfig.type-tests-defaults.json new file mode 100644 index 000000000..d8f91f7fe --- /dev/null +++ b/packages/vue-sdk/tsconfig.type-tests-defaults.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.type-tests.json", + "compilerOptions": { "noUnusedLocals": false, "noUnusedParameters": false }, + "include": ["src", "type-tests/defaults.ts"] +} diff --git a/packages/vue-sdk/tsconfig.type-tests-registered.json b/packages/vue-sdk/tsconfig.type-tests-registered.json new file mode 100644 index 000000000..9114cfb03 --- /dev/null +++ b/packages/vue-sdk/tsconfig.type-tests-registered.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.type-tests.json", + "compilerOptions": { "noUnusedLocals": false, "noUnusedParameters": false }, + "include": ["src", "type-tests/registered-defaults.ts"] +} diff --git a/packages/vue-sdk/type-tests/defaults.ts b/packages/vue-sdk/type-tests/defaults.ts new file mode 100644 index 000000000..eed8b6cb4 --- /dev/null +++ b/packages/vue-sdk/type-tests/defaults.ts @@ -0,0 +1,31 @@ +import type { ShallowRef } from "vue"; + +import { FlagValue, useFlag } from "@reflag/vue-sdk"; + +export function DefaultContracts() { + const enabled: ShallowRef = useFlag("release"); + const settings = useFlag("settings", { + model: "quick", + limits: [1], + } as const); + settings.value.model = "quality"; + settings.value.limits.push(2); + const _model: ShallowRef = useFlag("model", "quick"); + const _arbitrary: ShallowRef = useFlag("nullable", null); + // @ts-expect-error legacy wrappers are no longer returned + const { isEnabled: _isEnabled } = useFlag("release"); + // @ts-expect-error Boolean refs do not contain legacy wrapper fields + void enabled.value.isEnabled; + interface Settings { + model: string; + limits: number[]; + } + const fallback: Settings = { model: "quick", limits: [1] }; + const _interface: ShallowRef = useFlag("settings", fallback); + const _explicit: ShallowRef = useFlag( + "settings", + fallback, + ); + // @ts-expect-error literal defaults widen + const _literal: ShallowRef<"quick"> = useFlag("model", "quick"); +} diff --git a/packages/vue-sdk/type-tests/registered-defaults.ts b/packages/vue-sdk/type-tests/registered-defaults.ts new file mode 100644 index 000000000..aee065507 --- /dev/null +++ b/packages/vue-sdk/type-tests/registered-defaults.ts @@ -0,0 +1,65 @@ +import type { ShallowRef } from "vue"; + +import { FlagValue, useFlag } from "@reflag/vue-sdk"; + +interface Prompt { + tone: string; +} +interface Settings { + model: "quick" | "quality"; + prompt: Prompt; + methods: string[]; +} + +declare module "@reflag/vue-sdk" { + interface Flags { + enabled: boolean; + settings: Settings; + days: number; + arbitrary: FlagValue; + unknownArray: FlagValue[]; + methods: string[]; + model: "quick" | "quality"; + nullable: Settings | null; + } +} + +export function RegisteredContracts() { + const fallback: Settings = { + model: "quick", + prompt: { tone: "concise" }, + methods: ["standard"], + }; + const _settings = useFlag("settings", fallback); + const _arbitraryShape = useFlag("arbitrary", fallback); + const _unknownArray = useFlag("unknownArray", [fallback]); + const mutable = useFlag("settings", { + model: "quick", + prompt: { tone: "concise" }, + methods: ["standard"], + } as const); + mutable.value.model = "quality"; + mutable.value.prompt.tone = "detailed"; + mutable.value.methods.push("express"); + const _bare: ShallowRef = useFlag("settings"); + const _model: ShallowRef = useFlag("model", "quick"); + const _array: ShallowRef = useFlag("methods", [ + "standard", + ] as const); + const _arbitrary: ShallowRef = useFlag("settings", null); + const _nullable: ShallowRef = useFlag("nullable", null); + // @ts-expect-error Registered keys reject unknown names. + useFlag("unknown", false); + // @ts-expect-error A numeric flag elsewhere in the registry must not permit this mismatch. + useFlag("settings", 123); + // @ts-expect-error Required fields must be present. + useFlag("settings", { model: "quick" }); + // @ts-expect-error Nested property types must match. + useFlag("settings", { model: "quick", prompt: { tone: 3 }, methods: [] }); + // @ts-expect-error Registered literal unions constrain defaults. + useFlag("model", "unknown"); + // @ts-expect-error Array elements must match. + useFlag("methods", [123]); + // @ts-expect-error An explicit generic must not bypass the key's registered schema. + useFlag("settings", 123); +} diff --git a/packages/vue-sdk/vite.config.mjs b/packages/vue-sdk/vite.config.mjs index 7d25d020c..47378ece9 100644 --- a/packages/vue-sdk/vite.config.mjs +++ b/packages/vue-sdk/vite.config.mjs @@ -19,7 +19,7 @@ export default defineConfig({ }, plugins: [ vue(), - dts({ insertTypesEntry: true, exclude: ["dev"] }), + dts({ insertTypesEntry: true, exclude: ["dev"], aliasesExclude: ["vue"] }), preserveDirectives(), ], build: { diff --git a/yarn.lock b/yarn.lock index dbffbb08f..7ab448763 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6888,7 +6888,19 @@ __metadata: languageName: node linkType: hard -"@reflag/browser-sdk@npm:1.7.0, @reflag/browser-sdk@workspace:packages/browser-sdk": +"@reflag/browser-sdk@npm:1.7.0": + version: 1.7.0 + resolution: "@reflag/browser-sdk@npm:1.7.0" + dependencies: + "@floating-ui/dom": "npm:^1.6.8" + fast-equals: "npm:^5.2.2" + js-cookie: "npm:^3.0.5" + preact: "npm:^10.22.1" + checksum: 10c0/c83adf86c666b027e57cdd88d2086ecb962e2ef208cf9e8dde17129e74e1bacc49d8fe0ee773982e175b7d3a63306f80554051273d1311480a6589ab9971eed0 + languageName: node + linkType: hard + +"@reflag/browser-sdk@workspace:^, @reflag/browser-sdk@workspace:packages/browser-sdk": version: 0.0.0-use.local resolution: "@reflag/browser-sdk@workspace:packages/browser-sdk" dependencies: @@ -6948,10 +6960,20 @@ __metadata: languageName: unknown linkType: soft -"@reflag/flag-evaluation@npm:1.1.1, @reflag/flag-evaluation@workspace:packages/flag-evaluation": +"@reflag/flag-evaluation-v1@npm:@reflag/flag-evaluation@1.1.1, @reflag/flag-evaluation@npm:1.1.1": + version: 1.1.1 + resolution: "@reflag/flag-evaluation@npm:1.1.1" + dependencies: + js-sha256: "npm:0.11.0" + checksum: 10c0/fe6388d3d75f67ef9dd14d302925be368aabde3066019729627faa1fd25b9914f1ae908f64618a3d3217a8cad4f7c065d5ea3587156d8e4b05c1dde396d0efee + languageName: node + linkType: hard + +"@reflag/flag-evaluation@workspace:*, @reflag/flag-evaluation@workspace:packages/flag-evaluation": version: 0.0.0-use.local resolution: "@reflag/flag-evaluation@workspace:packages/flag-evaluation" dependencies: + "@reflag/flag-evaluation-v1": "npm:@reflag/flag-evaluation@1.1.1" "@reflag/tsconfig": "npm:^0.0.2" "@types/node": "npm:^22.12.0" js-sha256: "npm:0.11.0" @@ -6990,7 +7012,19 @@ __metadata: languageName: unknown linkType: soft -"@reflag/node-sdk@npm:1.8.0, @reflag/node-sdk@workspace:*, @reflag/node-sdk@workspace:^, @reflag/node-sdk@workspace:packages/node-sdk": +"@reflag/node-sdk-v1@npm:@reflag/node-sdk@1.8.0": + version: 1.8.0 + resolution: "@reflag/node-sdk@npm:1.8.0" + dependencies: + "@aws-sdk/client-s3": "npm:^3.888.0" + "@googleapis/storage": "npm:21.2.0" + "@redis/client": "npm:^5.11.0" + "@reflag/flag-evaluation": "npm:1.1.1" + checksum: 10c0/319fcbecbe57d0ff3545bd072b959c648a5db9fc033ef58d45bd6c7add0d486d68e45ce6ccda6088cbe33178e7cadad04759ca3d4d2ed9a8d33ea8a9432ad6de + languageName: node + linkType: hard + +"@reflag/node-sdk@workspace:*, @reflag/node-sdk@workspace:^, @reflag/node-sdk@workspace:packages/node-sdk": version: 0.0.0-use.local resolution: "@reflag/node-sdk@workspace:packages/node-sdk" dependencies: @@ -6998,7 +7032,7 @@ __metadata: "@babel/core": "npm:~7.29.6" "@googleapis/storage": "npm:21.2.0" "@redis/client": "npm:^5.11.0" - "@reflag/flag-evaluation": "npm:1.1.1" + "@reflag/flag-evaluation": "workspace:*" "@reflag/tsconfig": "npm:~0.0.2" "@types/node": "npm:^22.12.0" "@vitest/coverage-v8": "npm:~1.6.0" @@ -7038,7 +7072,7 @@ __metadata: "@babel/core": "npm:~7.29.6" "@openfeature/core": "npm:^1.5.0" "@openfeature/server-sdk": "npm:>=1.16.1" - "@reflag/node-sdk": "npm:1.8.0" + "@reflag/node-sdk-v1": "npm:@reflag/node-sdk@1.8.0" "@reflag/tsconfig": "npm:~0.0.2" "@types/node": "npm:^22.12.0" flush-promises: "npm:~1.0.2" @@ -7068,11 +7102,26 @@ __metadata: languageName: unknown linkType: soft -"@reflag/react-sdk@npm:1.7.0, @reflag/react-sdk@workspace:^, @reflag/react-sdk@workspace:packages/react-sdk": +"@reflag/react-sdk@npm:1.7.0": + version: 1.7.0 + resolution: "@reflag/react-sdk@npm:1.7.0" + dependencies: + "@reflag/browser-sdk": "npm:1.7.0" + peerDependencies: + react: "*" + react-dom: "*" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 10c0/7b07e2f9bc5d5ef9f8972694d3b0402bfbd434401f028729097ff127ca8b12f7189f5432e8b576aebcec771128033cb7586a717dfebd9012888e8d057b2c4263 + languageName: node + linkType: hard + +"@reflag/react-sdk@workspace:^, @reflag/react-sdk@workspace:packages/react-sdk": version: 0.0.0-use.local resolution: "@reflag/react-sdk@workspace:packages/react-sdk" dependencies: - "@reflag/browser-sdk": "npm:1.7.0" + "@reflag/browser-sdk": "workspace:^" "@reflag/tsconfig": "npm:^0.0.2" "@testing-library/react": "npm:^15.0.7" "@types/jsdom": "npm:^21.1.6" @@ -7121,7 +7170,7 @@ __metadata: version: 0.0.0-use.local resolution: "@reflag/vue-sdk@workspace:packages/vue-sdk" dependencies: - "@reflag/browser-sdk": "npm:1.7.0" + "@reflag/browser-sdk": "workspace:^" "@reflag/tsconfig": "npm:^0.0.2" "@types/jsdom": "npm:^21.1.6" "@types/node": "npm:^22.12.0"