https://featurevisor.com/docs Documentation Documentation for Featurevisor src/app/docs/page.md https://featurevisor.com/docs/advanced/custom-parsers Custom Parsers Learn how to define your own custom parsers for Featurevisor going beyond just YAML and JSON files. src/app/docs/advanced/custom-parsers/page.md require('toml').parse(content), }, } ``` You can find a example project using TOML [here](https://github.com/featurevisor/featurevisor/tree/main/examples/example-toml).]]> https://featurevisor.com/docs/alternatives Alternatives Alternative solutions src/app/docs/alternatives/page.md https://featurevisor.com/docs/attributes Attributes Learn how to create attributes in Featurevisor src/app/docs/attributes/page.md https://featurevisor.com/docs/bucketing Bucketing Learn how Featurevisor bucketing works src/app/docs/bucketing/page.md https://featurevisor.com/docs/building-datafiles Building datafiles Build your Featurevisor project datafiles src/app/docs/building-datafiles/page.md https://featurevisor.com/docs/catalog Catalog Explore and publish a static catalog for your Featurevisor project src/app/docs/catalog/page.md ` | all Catalog commands | Override `catalogDirectoryPath` for this invocation | | `--port=` or `-p ` | `catalog` and `catalog serve` | Serve on another port instead of `3000` | | `--hash-router` | `catalog` and `catalog export` | Generate routes suitable for hosts without an HTML fallback | | `--no-assets` | `catalog` and `catalog export` | Generate project data without copying the Catalog UI assets | Use `--no-assets` only when your workflow manages the compiled Catalog interface separately.]]> https://featurevisor.com/docs/cli Command Line Interface (CLI) Usage Command Line Interface (CLI) Usage of Featurevisor src/app/docs/cli/page.md ` one or more times to build only those targets. Without it, all targets are built. In a project with [sets](/docs/sets/), you can build a single set by passing `--set`: ``` $ npx featurevisor build --set=storefront ``` Learn more in [Building datafiles](/docs/building-datafiles). ## Testing Test your features and segments: ``` $ npx featurevisor test ``` Pass `--target=` one or more times to build only those target datafiles and run untargeted assertions plus assertions for the selected targets. Segment tests are not filtered. In a project with [sets](/docs/sets/), you can test a single set by passing `--set`: ``` $ npx featurevisor test --set=storefront ``` Learn more in [Testing](/docs/testing). ## Promote between sets In a project with [sets](/docs/sets/), you can preview and apply [promotions](/docs/promotions/) from one set to another. Preview what would be copied: ``` $ npx featurevisor promote --from=dev --to=staging ``` Apply the promotion to write destination files: ``` $ npx featurevisor promote --from=dev --to=staging --apply ``` Learn more in [Promotions](/docs/promotions). ## Generate static catalog Build the catalog: ``` $ npx featurevisor catalog export ``` Serve the built catalog (defaults to port 3000): ``` $ npx featurevisor catalog serve ``` Serve it in a specific port: ``` $ npx featurevisor catalog serve -p 3000 ``` Learn more in [Catalog](/docs/catalog/). ## Generate code Generate TypeScript code from feature definitions: ``` $ npx featurevisor generate-code --language typescript --out-dir ./src ``` See output in `./src` directory. Pass repeatable `--tag=` or `--target=` options to generate the union of features needed by several tags or targets. Learn more in [code generation](/docs/code-generation) page. ## Find duplicate segments It is possible to end up with multiple segments having same conditions in larger projects. This is not a problem per se, but we should be aware of it. We can find these duplicates early on by running: ``` $ npx featurevisor find-duplicate-segments ``` If we want to know the names of authors who worked on the duplicate segments, we can pass `--authors`: ``` $ npx featurevisor find-duplicate-segments --authors ``` ## Find usage Learn where/if certain segments and attributes are used in. For each of the `find-usage` commands below, you can optionally pass `--authors` to find who worked on the affected entities. ### Segment usage ``` $ npx featurevisor find-usage --segment=my_segment ``` ### Attribute usage ``` $ npx featurevisor find-usage --attribute=my_attribute ``` ### Unused segments ``` $ npx featurevisor find-usage --unusedSegments ``` ### Unused attributes ``` $ npx featurevisor find-usage --unusedAttributes ``` ### Feature usage ``` $ npx featurevisor find-usage --feature=my_feature ``` ## Benchmarking You can measure how fast or slow your SDK evaluations are for particular features. The `--n` option is used to specify the number of iterations to run the benchmark for. Pass `--target=` one or more times to benchmark the same evaluation independently against each selected target datafile. Without it, the complete in-memory datafile is used. The output includes both the total loop duration and individual evaluation timings: - `Total duration`: how long all benchmark iterations took together - `Minimum duration`: fastest individual evaluation - `Average duration`: average individual evaluation - `Maximum duration`: slowest individual evaluation Use the individual evaluation timings when comparing SDK performance. The maximum value can include runtime pauses like garbage collection or process scheduling. ### Feature To benchmark evaluating a feature itself if it is enabled or disabled via SDK's `.isEnabled()` method against provided [context](/docs/sdks/javascript/#context): ``` $ npx featurevisor benchmark \ --environment=production \ --feature=my_feature \ --context='{"userId": "123"}' \ --n=1000 ``` ### Variation To benchmark evaluating a feature's variation via SDKs's `.getVariation()` method: ``` $ npx featurevisor benchmark \ --environment=production \ --feature=my_feature \ --variation \ --context='{"userId": "123"}' \ --n=1000 ``` ### Variable To benchmark evaluating a feature's variable via SDKs's `.getVariable()` method: ``` $ npx featurevisor benchmark \ --environment=production \ --feature=my_feature \ --variable=my_variable_key \ --context='{"userId": "123"}' \ --n=1000 ``` ## Configuration To view the project [configuration](/docs/configuration): ``` $ npx featurevisor config ``` Printing configuration as JSON: ``` $ npx featurevisor config --json --pretty ``` ## Evaluate To learn why certain values (like feature and its variation or variables) are evaluated as they are against provided [context](/docs/sdks/javascript/#context): ``` $ npx featurevisor evaluate \ --environment=production \ --feature=my_feature \ --context='{"userId": "123", "country": "nl"}' ``` This will show you full [evaluation details](/docs/sdks/javascript/#evaluation-details) helping you debug better in case of any confusion. Pass `--target=` one or more times to evaluate independently against the exact datafiles for those targets. With repeated targets and `--json`, the result is an array containing each target and its evaluations. A single target keeps the regular evaluation object shape. It is similar to [diagnostics](/docs/sdks/javascript/#diagnostics) in SDKs with `debug` level. But here instead, we are doing it at CLI directly in our Featurevisor project without having to involve our application(s). If you wish to print the evaluation details in plain JSON, you can pass `--json` at the end: ``` $ npx featurevisor evaluate \ --environment=production \ --feature=my_feature \ --context='{"userId": "123", "country": "nl"}' \ --json \ --pretty ``` The `--pretty` flag is optional. To print further logs in a more verbose way, you can pass `--verbose`: ``` $ npx featurevisor evaluate \ --environment=production \ --feature=my_feature \ --context='{"userId": "123", "country": "nl"}' \ --verbose ``` ## List ### List datafiles To list generated datafiles in the configured `datafiles/` directory: ``` $ npx featurevisor list --datafiles ``` The result is a three-column table of paths relative to `datafiles/`, their uncompressed sizes, and their gzip-compressed sizes, excluding the `REVISION` file and hidden files. Size values are right-aligned and always show two decimal places; one-character `B` suffixes are padded to align with `kB` and `mB`. There is an empty line between directories. Directories beginning with `dev` appear first and those beginning with `prod` appear last. Sizes use colored `B`, `kB`, and `mB` suffixes in terminal output. With `--json`, each item has `path`, byte `size`, and byte `gzipSize` fields in the same order. | Option | Description | | ---------- | ------------- | | `--json` | print as JSON | | `--pretty` | pretty JSON | ### List features To list all features in the project: ``` $ npx featurevisor list --features ``` Advanced search options: | Option | Description | | ------------------------------ | -------------------------------------------------- | | `--archived=` | by [archived](/docs/features/#archiving) status | | `--description=` | by description pattern | | `--disabledIn=` | disabled in an [environment](/docs/environments) | | `--enabledIn=` | enabled in an [environment](/docs/environments) | | `--json` | print as JSON | | `--keyPattern=` | by key pattern | | `--tag=` | by [tag](/docs/tags/) | | `--target=` | selected by one or more repeatable targets | | `--variable=` | containing specific variable key | | `--variation=` | containing specific variation key | | `--with-tests` | with [test specs](/docs/testing) | | `--with-variables` | with variables | | `--with-variations` | with [variations](/docs/features/#variations) | | `--without-tests` | without any test specs | | `--without-variables` | without any [variables](/docs/features/#variables) | | `--without-variations` | without any variations | ### List segments To list all segments in the project: ``` $ npx featurevisor list --segments ``` Advanced search options: | Option | Description | | ---------------------------- | ----------------------------------------------- | | `--archived=` | by [archived](/docs/segments/#archiving) status | | `--description=` | by description pattern | | `--json` | print as JSON | | `--keyPattern=` | by key pattern | | `--pretty` | pretty JSON | | `--with-tests` | with [test specs](/docs/testing) | | `--without-tests` | without any test specs | ### List attributes To list all attributes in the project: ``` $ npx featurevisor list --attributes ``` Advanced search options: | Option | Description | | ---------------------------- | ------------------------------------------------- | | `--archived=` | by [archived](/docs/attributes/#archiving) status | | `--description=` | by description pattern | | `--json` | print as JSON | | `--keyPattern=` | by key pattern | | `--pretty` | pretty JSON | ### List tests To list all tests specs in the project: ``` $ npx featurevisor list --tests ``` Advanced search options: | Option | Description | | ------------------------------ | ------------------------------------------------- | | `--applyMatrix` | apply matrix for assertions | | `--assertionPattern=` | by assertion's description pattern | | `--json` | print as JSON | | `--keyPattern=` | by key pattern of feature or segment being tested | | `--pretty` | pretty JSON | ## Assess distribution To check if the gradual rollout of a feature and the weight distribution of its variations (if any exists) are going to work as expected in a real world application with real traffic against provided [context](/docs/sdks/javascript/#context), we can imitate that by running: ``` $ npx featurevisor assess-distribution \ --environment=production \ --feature=my_feature \ --context='{"country": "nl"}' \ --populateUuid=userId \ --n=1000 ``` The `--n` option controls the number of iterations to run, and the `--populateUuid` option is used to simulate different users in each iteration in this particular case. Further details about all the options: - `--environment`: the environment name - `--feature`: the feature key - `--context`: the common [context](/docs/sdks/javascript/#context) object in stringified form - `--populateUuid`: attribute key that should be populated with a new UUID, and merged with provided context. - You can pass multiple attributes in your command: `--populateUuid=userId --populateUuid=deviceId` - `--n`: the number of iterations to run the assessment for - The higher the number, the more accurate the distribution will be - `--target`: assess the exact target datafile; repeat it to assess several targets independently - `--verbose`: print the merged context for better debugging Everything is happening locally in memory without modifying any content anywhere. This command exists only to add to our confidence if questions arise about how effective traffic distribution in Featurevisor is. ## Info Shows count of various entities in the project: ``` $ npx featurevisor info ``` Pass `--target=` one or more times to show counts and datafile sizes for each selected target in each environment. Without it, `info` shows the project-wide entity counts. ## Version Get the current version number of Featurevisor CLI, and its relevant packages: ``` $ npx featurevisor --version ``` Or do: ``` $ npx featurevisor -v ```]]> https://featurevisor.com/docs/code-generation Code Generation Generate code from your defined features in Featurevisor. src/app/docs/code-generation/page.md `: generate code for features with the given tag - `--target=`: generate code for features selected by the target - `--react`: also generate typed React hooks in `react.ts` Both `--tag` and `--target` can be repeated. Repeated values and a combination of both options form a union, which is useful when one generated package serves several datafiles: ```{% title="Command" %} $ npx featurevisor generate-code \ --language typescript \ --out-dir ./src \ --tag=shared \ --target=web \ --target=mobile ``` A target uses the same `tag` or `tags`, `includeFeatures`, and `excludeFeatures` selection as its datafile build. Its build-time `context` does not remove feature types because code generation is not tied to one environment or one specialized datafile result. ## Publishing the generated code We are free to use the generated code in any way we want. We can choose to either: - copy/paste the code in our applications, or - publish the generated code as a private npm package and use it in multiple applications, like as `@yourorg/features` package The publishing part can be done in the same [deployment](/docs/deployment) process right after deploying our generated [datafiles](/docs/building-datafiles). ## Consuming the generated code Assuming we published the generated code as a private npm package `@yourorg/features`, we can consume it in our applications as follows. Initialize Featurevisor [SDK](/docs/sdks/javascript) as usual, and make our newly created package aware of the SDK instance: ```js {% path="your-app/index.js" %} import { createFeaturevisor } from '@featurevisor/sdk' import { setInstance } from '@yourorg/features' const f = createFeaturevisor({ // ... }) setInstance(f) ``` Afterwards, we can import a common set of functions which are already aware of which feature keys we are allowed to use including their variable keys. ## Importing functions Similar to [JavaScript SDK](/docs/sdks/javascript)'s methods `isEnabled`, `getVariation`, and `getVariable`, we can import these functions with the same names: ```js import { isEnabled, getVariation, getVariable } from '@yourorg/features' const featureIsEnabled = isEnabled('featureKey') const featureVariation = getVariation('featureKey') const featureVariable = getVariable('featureKey', 'variableKey') ``` We can optionally pass additional [context](/docs/sdks/javascript/#context) as the last argument: ```js const context = { userId: '123', } const featureIsEnabled = isEnabled('featureKey', context) const featureVariation = getVariation('featureKey', context) const featureVariable = getVariable('featureKey', 'variableKey', context) ``` Everything here is typed as per our defined [features](/docs/features). If we pass a wrong feature key, or a variable key that does not belong to the same feature, we will get a TypeScript error. ## Importing React hooks If we passed `--react` in CLI, we can import React hooks with the same names as the original package [`@featurevisor/react`](/docs/react): ```js import { useFlag, useVariation, useVariable } from '@yourorg/features' const isEnabled = useFlag('featureKey') const variation = useVariation('featureKey') const variable = useVariable('featureKey', 'variableKey') ``` Passing any wrong feature key or variable key combination will result in a TypeScript error. We can optionally pass additional [context](/docs/sdks/javascript/#context) as the last argument: ```js const context = { userId: '123', } const isEnabled = useFlag('featureKey', context) const variation = useVariation('featureKey', context) const variable = useVariable('featureKey', 'variableKey', context) ``` ## Suggestions for package publishing You are advised to publish the generated code as a private npm package, with support for ES Modules (ESM). When published as ES Modules, it will enable tree-shaking in your applications, thus reducing the bundle size.]]> https://featurevisor.com/docs/concepts Concepts Understand the core concepts behind Featurevisor src/app/docs/concepts/page.md https://featurevisor.com/docs/concepts/cloud-native-architecture Cloud Native Architecture Learn what being Cloud Native means and how it applies to Featurevisor src/app/docs/concepts/cloud-native-architecture/page.md https://featurevisor.com/docs/concepts/gitops What is GitOps? Learn what GitOps mean and how it applies to Featurevisor src/app/docs/concepts/gitops/page.md https://featurevisor.com/docs/concepts/infrastructure-as-code Infrastructure as Code (IaC) Learn what Infrastructure as Code (IaC) means and how it applies to Featurevisor. src/app/docs/concepts/infrastructure-as-code/page.md https://featurevisor.com/docs/configuration Configuration Configure your Featurevisor project src/app/docs/configuration/page.md /targets`. ### `setsDirectoryPath` Path to the directory containing your [sets](/docs/sets/). Defaults to `/sets`. ### `attributesDirectoryPath` Path to the directory containing your [attributes](/docs/attributes/). Defaults to `/attributes`. ### `segmentsDirectoryPath` Path to the directory containing your [segments](/docs/segments/). Defaults to `/segments`. ### `featuresDirectoryPath` Path to the directory containing your [features](/docs/features/). Defaults to `/features`. ### `groupsDirectoryPath` Path to the directory containing your [groups](/docs/groups/). Defaults to `/groups`. ### `schemasDirectoryPath` Path to the directory containing your reusable [schemas](/docs/schemas/). Defaults to `/schemas`. ### `testsDirectoryPath` Path to the directory containing your [tests](/docs/testing/). Defaults to `/tests`. ### `datafilesDirectoryPath` Path to the directory for your generated [datafiles](/docs/building-datafiles/). Defaults to `/datafiles`. ### `datafileNamePattern` Pattern used to name generated datafiles, where `%s` is replaced by the [target](/docs/targets/) key. Defaults to `featurevisor-%s.json`. ### `revisionFileName` Defaults to `REVISION`. Name of the file that will be used to store the [revision](/docs/building-datafiles/) number of your project. ### `stateDirectoryPath` Path to the directory containing your state. Defaults to `/.featurevisor`. Read more in [State files](/docs/state-files). ### `catalogDirectoryPath` Path to the directory where the generated [catalog](/docs/catalog/) is written. Defaults to `/catalog`. ### `defaultBucketBy` Default value for the `bucketBy` property in your project. Defaults to `userId`. ### `prettyState` Set to `true` or `false` to enable or disable pretty-printing of state files. Defaults to `true`. ### `prettyDatafile` Set to `true` or `false` to enable or disable pretty-printing of datafiles. Defaults to `false`. ### `stringify` By default, Featurevisor will stringify conditions and segments in generated datafiles so that they are parsed only when needed by the SDKs. This optimization technique works well when datafiles are too large in client-side devices (think browsers) and you are only dealing with one user in the runtime. This kind of optimization though can bring opposite results if you are using the SDKs in server-side (think Node.js) serving many different users. To disable this stringification, you can set it to `false`. ### `parser` By default, Featurevisor expects YAML for all definitions. You can change this to JSON by setting `parser: "json"`. See [custom parsers](/docs/advanced/custom-parsers) for more information. ### `enforceCatchAllRule` When set to `true`, linting will make sure all features have a catch-all rule with `segment: "*"` as the last rule in all environments. ### `maxVariableStringLength` Maximum length of a string variable in features. Defaults to no limit. ### `maxVariableArrayStringifiedLength` Maximum length of a stringified array variable in features. Defaults to no limit. ### `maxVariableObjectStringifiedLength` Maximum length of a stringified object variable in features. Defaults to no limit. ### `maxVariableJSONStringifiedLength` Maximum length of a JSON stringified variable in features. Defaults to no limit.]]> https://featurevisor.com/docs/contributing Contributing Learn how to contribute to Featurevisor src/app/docs/contributing/page.md https://featurevisor.com/docs/datasource Datasource & Adapters Go beyond file system as source of your configuration with Featurevisor src/app/docs/datasource/page.md https://featurevisor.com/docs/deployment Deployment Deploy your Featurevisor project datafiles src/app/docs/deployment/page.md https://featurevisor.com/docs/environments Environments Customize your Featurevisor project with multiple environments src/app/docs/environments/page.md /`: ``` sets/ ├── dev/ │ ├── attributes/ │ ├── segments/ │ ├── features/ │ ├── targets/ │ └── tests/ ├── staging/ │ └── ... └── production/ └── ... ``` The same feature key can live in every lane with different rules. Because there are no `environments`, rules are authored directly without an environment key: ```yml {% path="sets/dev/features/checkoutFlow.yml" %} description: New checkout flow in dev tags: - all bucketBy: userId # fully rolled out in dev rules: - key: everyone segments: '*' percentage: 100 ``` ```yml {% path="sets/production/features/checkoutFlow.yml" %} description: New checkout flow in production tags: - all bucketBy: userId # still off in production rules: - key: everyone segments: '*' percentage: 0 ``` ### Building datafiles [Building](/docs/building-datafiles/) writes datafiles under a directory per lane: ```{% title="Output" %} datafiles/dev/featurevisor-all.json datafiles/staging/featurevisor-all.json datafiles/production/featurevisor-all.json ``` ### Promoting between lanes When a feature is ready to move to the next lane, [promote](/docs/promotions/) it: ```{% title="Command" %} $ npx featurevisor promote --from=dev --to=staging --apply ``` The `promotionFlows` configuration above makes sure promotions only move forward, so promoting straight from `dev` to `production` is rejected. Read more in [Sets](/docs/sets/) and [Promotions](/docs/promotions/). ## No environments Projects have no environments by default. You can omit `environments` from your configuration: ```js {% path="featurevisor.config.js" %} module.exports = { tags: [ 'web', 'mobile' ], } ``` This will allow us to define our rollout rules directly without needing any environment specific keys: ```yml {% path="features/my_feature.yml" %} description: My feature tags: - web bucketBy: userId # rules without needing environment specific keys rules: - key: everyone segments: '*' percentage: 100 ``` The [datafiles](/docs/building-datafiles) will be built without any environment: ``` $ tree datafiles . ├── featurevisor-web.json ├── featurevisor-mobile.json ```]]> https://featurevisor.com/docs/examples Examples Example projects with Featurevisor src/app/docs/examples/page.md https://featurevisor.com/docs/faq Frequently Asked Questions Frequently asked questions about Featurevisor src/app/docs/faq/page.md https://featurevisor.com/docs/feature-management Feature Management Learn what feature management is all about and how to use it to roll out new features safely. src/app/docs/feature-management/page.md https://featurevisor.com/docs/features Features Learn how to create feature flags in Featurevisor src/app/docs/features/page.md https://featurevisor.com/docs/flags Flags Learn about feature flags in Featurevisor src/app/docs/flags/page.md https://featurevisor.com/docs/frameworks/astro Astro Learn how to integrate Featurevisor in Astro applications for evaluating feature flags src/app/docs/frameworks/astro/page.md res.json()) instance = createFeaturevisor({ datafile, }) return instance } ``` Now that we have the SDK instance in place, we can use it anywhere in our application. {% callout type="note" title="Featurevisor's build & deployment" %} To understand how the datafiles are generated and deployed, please refer to these guides: - [Building datafiles](/docs/building-datafiles) - [Deployment](/docs/deployment) {% /callout %} We created a very simple instance of the SDK, but we can also configure it further for fetching latest datafile without restarting our server: - periodically (see [updating datafile](/docs/sdks/javascript/#updating-datafile)) - as soon as they happen (see [websockets guide](/docs/integrations/partykit)) ## Accessing SDK in components From any component file, we can import the `getInstance` function and use it to access the SDK instance: ```js // src/pages/index.astro --- import { getInstance } from '../featurevisor.mjs'; const f = await getInstance(); const featureKey = "my_feature"; const context = { userId: "123", country: "nl" }; const isEnabled = f.isEnabled(featureKey, context); ---

Feature {featureKey} is {isEnabled ? 'enabled' : 'disabled' }.

``` With just a few lines of code, we can now evaluate feature flags in our Astro components. ## Regular client-side usage If your use case is not server-side rendering or at build time, you can use the SDK instance directly in your client-side code: - [JavaScript SDK](/docs/sdks) - [React SDK](/docs/react) - [Vue.js SDK](/docs/vue) ## Bucketing guidelines If you are using Featurevisor for gradual rollouts or A/B testing, you should make sure that the [bucketing](/docs/bucketing) is consistent when rendering your components. Usually bucketing is done by passing the User's ID when the user is already known, or a randomly generated UUID for the device if the user has not logged in yet. When evaluating using the SDK instance, we would be passing these values as `context` object: ```ts const context = { userId: '123', deviceId: '', } const isEnabled = f.isEnabled(featureKey, context) ``` If the evaluation of features are done in the server, you should make sure that the User's ID is passed to the server as well. If that's not an option, you are recommended to use a single value consistently. See documentation about `bucketBy` property in feature definitions for further explanation [here](/docs/features/#bucketing). ## Working repository You can find a fully functional example of this integration on GitHub: [https://github.com/featurevisor/featurevisor-example-astro](https://github.com/featurevisor/featurevisor-example-astro).]]>
https://featurevisor.com/docs/frameworks/express Express.js Learn how to integrate Featurevisor in Express.js applications for evaluating feature flags src/app/docs/frameworks/express/page.md { res.send('Hello World!') }) app.listen(PORT, () => { console.log(`Example app listening on port ${PORT}`) }) ``` We can start the server with this command: ``` $ node index.js Example app listening on port 3000 ``` ## Featurevisor integration We install the Featurevisor SDK now: ``` $ npm install --save @featurevisor/sdk ``` We can now create an instance of the SDK and use it in our application: ```js // index.js const express = require('express') const { createFeaturevisor } = require('@featurevisor/sdk') const PORT = 3000 const DATAFILE_URL = 'https://cdn.yoursite.com/datafile.json' const app = express() const f = createFeaturevisor({}) app.get('/', (req, res) => { const featureKey = 'myFeature' const context = { userId: 'user-123' } const isEnabled = f.isEnabled(featureKey, context) if (isEnabled) { res.send('Hello World!') } else { res.send('Not enabled yet!') } }) fetch(DATAFILE_URL) .then((response) => response.json()) .then((datafile) => { f.setDatafile(datafile, true) // we start the server only after the datafile is loaded app.listen(PORT, () => { console.log(`Example app listening on port ${PORT}`) }) }) ``` ## Middleware If is very unlikely that we will have all our routes defined in the same `index.js` file, making it difficult for us to use the same Featurevisor SDK instance in all of them. To solve this problem, we can create a custom middleware that will set the Featurevisor SDK instance to the `req` object, so that we can use the same instance in all our routes throughout the lifecycle of this application. ```js // index.js // ... const f = createFeaturevisor({}) app.use((req, res, next) => { req.f = f next() }) // ... ``` Now from anywhere in our application (either in `index.js` or some other module), we can access the Featurevisor SDK instance via `req.f`: ```js app.get('/my-route', (req, res) => { const { f } = req const featureKey = 'myFeature' const context = { userId: 'user-123' } const isEnabled = f.isEnabled(featureKey, context) if (isEnabled) { res.send('Hello World!') } else { res.send('Not enabled yet!') } }) ``` ## TypeScript usage If you are using TypeScript, you can extend the `Request` interface to add the `f` property for Featurevisor SDK's instance. Create a new `custom.d.ts` file and make sure to add it in `tsconfig.json`'s `files` section: ```ts import type { Featurevisor } from '@featurevisor/sdk' declare namespace Express { export interface Request { f: Featurevisor } } ``` ## Refreshing datafile Because a server instance is meant to run for a long time, we might want to refresh the datafile periodically so that latest datafile is always used without needing to restart the server. See more documentation in [JavaScript SDK page](/docs/sdks/javascript/#interval-based-update). ## Child instances If you are in need of request specific context isolation, you may want to look into spawning child instances from the primary Featurevisor SDK [here](/docs/sdks/javascript/#child-instance). ## Working repository You can find a fully functional example of this integration on GitHub: [https://github.com/featurevisor/featurevisor-example-expressjs](https://github.com/featurevisor/featurevisor-example-expressjs).]]> https://featurevisor.com/docs/frameworks/fastify Fastify Learn how to integrate Featurevisor in Fastify applications for evaluating feature flags src/app/docs/frameworks/fastify/page.md { return 'Hello World!' }) fastify.listen(PORT, () => { console.log(`Example app listening on port ${PORT}`) }) ``` We can start the server with this command: ``` $ node index.js Example app listening on port 3000 ``` ## Featurevisor integration We install the Featurevisor SDK first: ``` $ npm install --save @featurevisor/sdk ``` We can now create an instance of the SDK and use it in our application: ```js // Require the fastify framework and instantiate it const fastify = require('fastify')({ logger: true, }) // Featurevisor SDK const { createFeaturevisor } = require('@featurevisor/sdk') const DATAFILE_URL = 'https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-all.json' // replace with yoursite cdn const f = createFeaturevisor({}) // Declare a route fastify.get('/', async (request, reply) => { const featureKey = 'my_feature' const context = { userId: '123', country: 'nl' } const isEnabled = f.isEnabled(featureKey, context) if (isEnabled) { reply.send('Hello World!') } else { reply.send('Not enabled yet!') } }) // Run the server! const start = async () => { const datafile = await fetch(DATAFILE_URL).then((res) => res.json()) f.setDatafile(datafile, true) // we start the server only after the datafile is loaded fastify.listen({ port: 3000 }, function (err, address) { if (err) { fastify.log.error(err) process.exit(1) } fastify.log.info(`server listening on ${fastify.server.address().port}`) }) } start() ``` To keep the datafile fresh without restarting the server, fetch it again on an interval and call `f.setDatafile(datafile, true)` to replace the previously loaded full datafile. See more in the [JavaScript SDK page](/docs/sdks/javascript/#interval-based-update). ## Decorator It is very unlikely that we will have all our routes defined in the same index.js file, making it difficult for us to use the same Featurevisor SDK instance in all of them. To solve this problem, we can create a custom decorator that will set the Featurevisor SDK instance to the request object, so that we can use the same instance in all our routes throughout the lifecycle of this application. ```js // index.js // ... fastify.decorateRequest('f', f) // ... ``` Now from anywhere in our application (either in index.js or some other module), we can access the Featurevisor SDK instance via request.f: ```js fastify.get('/my-route', async (request, reply) => { const featureKey = 'my_feature' const context = { userId: '123', country: 'nl' } const isEnabled = request.f.isEnabled(featureKey, context) if (isEnabled) { reply.send('Hello World!') } else { reply.send('Not enabled yet!') } }) ``` ## TypeScript usage If you are using TypeScript, you can extend the `Request` interface to add the `f` property for Featurevisor SDK's instance. Create a new `custom.d.ts` file and make sure to add it in `tsconfig.json`'s `files` section: ```ts import type { Featurevisor } from '@featurevisor/sdk' import type { FastifyInstance } from 'fastify' declare module 'fastify' { interface FastifyInstance { f: Featurevisor } } ``` ## Working repository You can find a fully functional example of this integration on GitHub: [https://github.com/featurevisor/featurevisor-example-fastify](https://github.com/featurevisor/featurevisor-example-fastify).]]> https://featurevisor.com/docs/frameworks/nextjs Next.js Learn how to integrate Featurevisor in Next.js applications for evaluating feature flags src/app/docs/frameworks/nextjs/page.md ` | `my_feature` | | Variation | string | `:variation` | `my_feature:variation` | | Variable | mixed | `:` | `my_feature:my_variable` | ## Set up Featurevisor We will start by creating a new Featurevisor adapter using Flags SDK in the `src/featurevisor.ts` file: ```ts {% path="src/featurevisor.ts" %} import type { Adapter } from 'flags' import { createFeaturevisor } from '@featurevisor/sdk' import type { Featurevisor } from '@featurevisor/sdk' export interface FeaturevisorAdapterOptions { datafileUrl: string refreshInterval?: number f?: Featurevisor } export interface FeaturevisorEntitiesType { userId?: string // ...add more properties (attributes) for your context here } export function createFeaturevisorAdapter(options: FeaturevisorAdapterOptions) { const f = options.f || createFeaturevisor({}) let initialFetchCompleted = false // datafile fetcher function fetchAndSetDatafile() { console.log('[Featurevisor] Fetching datafile from:', options.datafileUrl) const result = fetch(options.datafileUrl) .then((response) => response.json()) .then((datafile) => { f.setDatafile(datafile, true) initialFetchCompleted = true }) .catch((error) => console.error('[Featurevisor] Error fetching datafile:', error) ) return result } // datafile refresher (periodic update) if (options.refreshInterval) { setInterval(async () => { await fetchAndSetDatafile() }, options.refreshInterval) } // adapter return function featurevisorAdapter< ValueType, EntitiesType extends FeaturevisorEntitiesType >(): Adapter { return { async decide({ key, entities, headers, cookies }): Promise { // ensure the datafile is fetched before making decisions if (!initialFetchCompleted) { await fetchAndSetDatafile() } const context = { userId: entities?.userId, } // mapping passed key to Featurevisor SDK methods: // // - '' => f.isEnabled(key, context) // - ':variation' => f.getVariation(key, context) // - ':' => f.getVariable(key, variableKey, context) const [featureKey, variableKey] = key.split(':') if (variableKey) { if (variableKey === 'variation') { // variation return f.getVariation(featureKey, context) as ValueType } else { // variable return f.getVariable(featureKey, variableKey, context) as ValueType } } // flag return f.isEnabled(featureKey, context) as ValueType }, } } } ``` ## Flags SDK integration Now we can create a new `src/flags.ts` file for our individual features and their evaluations: ```ts {% path="src/flags.ts" %} import { flag } from 'flags/next' import { createFeaturevisorAdapter } from './featurevisor' // set up adapter const featurevisorAdapter = createFeaturevisorAdapter({ // replace with your Featurevisor project datafile URL datafileUrl: 'https://cdn.yoursite.com/datafile.json', // if you want to periodically refresh the datafile refreshInterval: 5 * 60 * 1000, // every 5 minutes }) // feature specific flags export const myFeatureFlag = flag({ // '' as the feature key alone to get its flag (boolean) status key: 'my_feature', adapter: featurevisorAdapter(), }) export const myFeatureVariation = flag({ // ':variation' is to get the variation (string) of the feature key: 'my_feature:variation', adapter: featurevisorAdapter(), }) export const myFeatureVariable = flag({ // ':' is to get the variable value of the feature key: 'my_feature:variableKeyHere', adapter: featurevisorAdapter(), }) ``` ## App Router If you're using the App Router, you can call the flag function from a page, component, or middleware to evaluate the flag: ```ts {% path="src/app/page.tsx" %} import { myFeatureFlag } from '../flags' export default async function Page() { const myFeature = await myFeatureFlag() return
{myFeature ? 'Flag is on' : 'Flag is off'}
} ``` ## Pages Router If you're using the Pages Router, you can call the flag function inside getServerSideProps and pass the values to the page as props: ```ts {% path="src/pages/index.tsx" %} import type { InferGetServerSidePropsType, GetServerSideProps } from 'next' import { myFeatureFlag } from '../flags' export const getServerSideProps = (async ({ req }) => { const myFeature = await myFeatureFlag(req) return { props: { myFeature } } }) satisfies GetServerSideProps<{ myFeature: boolean }> export default function Page({ myFeature }: InferGetServerSidePropsType) { return
{myFeature ? 'Flag is on' : 'Flag is off'}
} ``` ## Working repository You can find a fully functional example of this integration on GitHub: [https://github.com/featurevisor/featurevisor-example-nextjs](https://github.com/featurevisor/featurevisor-example-nextjs).]]>
https://featurevisor.com/docs/frameworks/nuxt Nuxt Learn how to integrate Featurevisor in Nuxt applications for evaluating feature flags src/app/docs/frameworks/nuxt/page.md res.json()) instance = createFeaturevisor({ datafile, }) return instance } ``` To understand how the datafiles are generated and deployed, please refer to these guides: - [Building datafiles](/docs/building-datafiles) - [Deployment](/docs/deployment) Now that we have the SDK instance in place, we can use it anywhere in our application. ## Accessing SDK in Vue.js components From any Vue.js component file, we can import the `getInstance` function and use it to access the SDK instance: ```html ``` With just a few lines of code, we can now evaluate feature flags in our Vue.js components. ## Regular client-side usage If you are using Vue.js components in a regular client-side rendered application, you can refer to our separate [Vue.js SDK](/docs/vue) for Featurevisor. ## Bucketing guidelines If you are using Featurevisor for gradual rollouts or A/B testing, you should make sure that the [bucketing](/docs/bucketing) is consistent when rendering your components. Usually bucketing is done by passing the User's ID when the user is already known, or a randomly generated UUID for the device if the user has not logged in yet. When evaluating using the SDK instance, we would be passing these values as `context` object: ```ts const context = { userId: '123', deviceId: '', } const isEnabled = f.isEnabled(featureKey, context) ``` Since the evaluation of features are done in the server, you should make sure that the User's ID is passed to the server as well. If that's not an option, you are recommended to use a single value consistently. See documentation about `bucketBy` property in feature definitions for further explanation [here](/docs/features/#bucketing). ## Working repository You can find a fully functional example of this integration on GitHub: [https://github.com/featurevisor/featurevisor-example-nuxt](https://github.com/featurevisor/featurevisor-example-nuxt).]]> https://featurevisor.com/docs/glossary Glossary Glossary of terms that you will come across when adopting feature management principles with Featurevisor src/app/docs/glossary/page.md https://featurevisor.com/docs/groups Groups Learn how to create groups in Featurevisor src/app/docs/groups/page.md https://featurevisor.com/docs/integrations/cloudflare-pages Cloudflare Pages Learn how to upload Featurevisor datafiles to Cloudflare Pages src/app/docs/integrations/cloudflare-pages/page.md Secrets and variables > Actions` section: - `CLOUDFLARE_ACCOUNT_ID` - `CLOUDFLARE_API_TOKEN` ## Repository settings Make sure you have `Read and write permissions` enabled in your GitHub repository's `Settings > Actions > General > Workflow permissions` section. ## Workflows We will be covering two workflows for our set up with GitHub Actions. ### Checks This workflow will be triggered on every push to the repository targeting any non-master or non-main branches. This will help identify any issues with your Pull Requests early before you merge them to your main branch. ```yml {% path=".github/workflows/checks.yml" %} name: Checks on: push: branches-ignore: - main - master jobs: checks: name: Checks runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: npm ci - name: Lint run: npx featurevisor lint - name: Test specs run: npx featurevisor test - name: Build run: npx featurevisor build ``` ### Publish This workflow is intended to be run on every push to your main (or master) branch, and is supposed to handle uploading of your generated datafiles to Cloudflare Pages: ```yml {% path=".github/workflows/publish.yml" %} name: Publish on: push: branches: - main - master jobs: publish: name: Publish runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: npm ci - name: Lint run: npx featurevisor lint - name: Test specs run: npx featurevisor test - name: Build run: npx featurevisor build - name: Upload to Cloudflare Pages run: | echo "It works." > datafiles/index.html npx wrangler pages deploy datafiles --project-name="YOUR_CLOUDFLARE_PAGES_PROJECT_NAME" env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - name: Git configs run: | git config user.name "${{ github.actor }}" git config user.email "${{ github.actor }}@users.noreply.github.com" - name: Push back to origin run: | git add .featurevisor/* git commit -m "[skip ci] Revision $(cat .featurevisor/REVISION)" git push ``` After generating new [datafiles](/docs/building-datafiles/) and uploading them, the workflow will also take care of pushing the Featurevisor [state files](/docs/state-files) back to the repository, so that future builds will be built on top of latest state. Once uploaded, your datafiles will be accessible as: `https://.pages.dev//featurevisor-.json`. You may want to take it a step further by setting up custom domains (or subdomains) for your Cloudflare Pages project. Otherwise, you are good to go. Learn how to consume datafiles from URLs directly using [SDKs](/docs/sdks). ## Full example You can find a fully functional repository based on this guide here: [https://github.com/featurevisor/featurevisor-example-cloudflare](https://github.com/featurevisor/featurevisor-example-cloudflare). ## Sequential builds In case you are worried about simultaneous builds triggered by multiple Pull Requests merged in quick succession, you can learn about mitigating any unintended issues [here](/docs/integrations/github-actions/#sequential-builds).]]> https://featurevisor.com/docs/integrations/github-actions GitHub Actions (GHA) Learn how to set up CI/CD workflows with GitHub Actions for Featurevisor src/app/docs/integrations/github-actions/page.md Actions > General > Workflow permissions` section. ## Workflows We will be covering two workflows for our set up with GitHub Actions. ### Checks This workflow will be triggered on every push to the repository targeting any non-master or non-main branches. This will help identify any issues with your Pull Requests early before you merge them to your main branch. ```yml {% path=".github/workflows/checks.yml" %} name: Checks on: push: branches-ignore: - main jobs: checks: name: Checks runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: npm ci - name: Lint run: npx featurevisor lint - name: Test specs run: npx featurevisor test - name: Build run: npx featurevisor build ``` ### Publish This workflow is intended to be run on every push to your main (or master) branch, and is supposed to handle uploading of your generated datafiles as well: ```yml {% path=".github/workflows/publish.yml" %} name: Publish on: push: branches: - main jobs: ci: name: Publish runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: npm ci - name: Lint run: npx featurevisor lint - name: Test specs run: npx featurevisor test - name: Build run: npx featurevisor build - name: Upload datafiles run: echo "Uploading..." # Update "datafiles" directory content based on your CDN set up - name: Git configs run: | git config user.name "${{ github.actor }}" git config user.email "${{ github.actor }}@users.noreply.github.com" - name: Push back to origin run: | git add .featurevisor/* git commit -m "[skip ci] Revision $(cat .featurevisor/REVISION)" git push ``` After generating new [datafiles](/docs/building-datafiles/) and uploading them, the workflow will also take care of pushing the Featurevisor [state files](/docs/state-files) back to the repository, so that future builds will be built on top of latest state. If you want an example of an actual uploading step, see [Cloudflare Pages](/docs/integrations/cloudflare-pages/) integration guide. ## Sequential builds It is possible you might want to run the publish workflow sequentially for every merged Pull Requests, in case multiple Pull Requests are merged in quick succession. ### Queue You can consider using [softprops/turnstyle](https://github.com/softprops/turnstyle) GitHub Action to run publish workflow of all your merged Pull Requests sequentially. ### Branch protection rules Next to it, you can also make it stricter by requiring all Pull Request authors to have their branches up to date from latest main branch before merging: - [Managing suggestions to update pull request branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-suggestions-to-update-pull-request-branches) - [Create branch protection rule](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule#creating-a-branch-protection-rule) (see #7) - Require branches to be up to date before merging]]> https://featurevisor.com/docs/integrations/github-pages GitHub Pages Learn how to upload Featurevisor datafiles to GitHub Pages src/app/docs/integrations/github-pages/page.md It works." > datafiles/index.html - name: Setup Pages uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: 'datafiles' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 - name: Git configs run: | git config user.name "${{ github.actor }}" git config user.email "${{ github.actor }}@users.noreply.github.com" - name: Push back to origin run: | git add .featurevisor/* git commit -m "[skip ci] Revision $(cat .featurevisor/REVISION)" git push ``` After generating new [datafiles](/docs/building-datafiles/) and uploading them, the workflow will also take care of pushing the Featurevisor [state files](/docs/state-files) back to the repository, so that future builds will be built on top of latest state. Once uploaded, your datafiles will be accessible as: `https://.github.io///featurevisor-.json`. You may want to take it a step further by [setting up custom domains (or subdomains)](https://docs.github.com/articles/using-a-custom-domain-with-github-pages/) for your GitHub Pages project. Otherwise, you are good to go. Learn how to consume datafiles from URLs directly using [SDKs](/docs/sdks). ## Full example You can find a fully functional repository based on this guide here: [https://github.com/meirroth/featurevisor-example-github](https://github.com/meirroth/featurevisor-example-github). ## Sequential builds In case you are worried about simultaneous builds triggered by multiple Pull Requests merged in quick succession, you can learn about mitigating any unintended issues [here](/docs/integrations/github-actions/#sequential-builds).]]> https://featurevisor.com/docs/integrations/partykit WebSocket & PartyKit Learn how to integrate Featurevisor with PartyKit via WebSocket for realtime updates. src/app/docs/integrations/partykit/page.md " -d '{"type": "refresh"}' \ https://..partykit.dev/party/featurevisor ``` The `X-PartyKit-Secret` is there so that our server only accepts messages from our own CI/CD pipeline and not from anyone else. You are free to take any other approach to better manage your security. ## Listening to messages in our application Now that we have a PartyKit server that can receive messages from our CI/CD pipeline and also broadcast it to all connected applications, we (as one of those applications) can listen to those messages and trigger a new refresh of our SDK instance: ```js {% path="your-app/index.js" %} import { createFeaturevisor } from '@featurevisor/sdk' const DATAFILE_URL = 'https://cdn.yoursite.com/datafile.json' const WEBSOCKET_URL = 'wss://..partykit.dev/party/featurevisor' function fetchDatafile() { return fetch(DATAFILE_URL) .then((response) => response.json()) } const initialDatafileContent = await fetchDatafile() const f = createFeaturevisor({ datafile: initialDatafileContent, }) const socket = new WebSocket(WEBSOCKET_URL) socket.onmessage = (event) => { const message = JSON.parse(event.data) if (message.type === 'refresh') { const newDatafileContent = await fetchDatafile() f.setDatafile(newDatafileContent, true) } } ``` We just did that without even needing any new library, because WebSocket API is natively supported in modern browsers. {% callout type="note" title="WebSocket support in Node.js" %} If you are using Node.js, you can consider using the [ws](https://github.com/websockets/ws) package. {% /callout %} Wish just a few lines of code, we just made our application listen to messages from our PartyKit server and trigger a new refresh of our SDK instance as soon as there are new changes in our Featurevisor project, making every feature update a realtime update.]]> https://featurevisor.com/docs/linting Linting Lint your Featurevisor definition files src/app/docs/linting/page.md https://featurevisor.com/docs/linting-yamls Linting YAMLs Lint your Featurevisor YAML files src/app/docs/linting-yamls/page.md https://featurevisor.com/docs/llms LLM documentation Access compact and complete Featurevisor documentation feeds for AI tools src/app/docs/llms/page.md https://featurevisor.com/docs/migrations Migration guides Learn how to migrate to latest Featurevisor version src/app/docs/migrations/page.md https://featurevisor.com/docs/migrations/v2 Migrating from v1 to v2 Guide for migrating from Featurevisor v1 to v2 src/app/docs/migrations/v2/page.md .json` to `featurevisor-tag-.json` to help distinguish between Featurevisor datafiles and other datafiles that may be used in your project: {% row %} {% column %} ```{% title="Before" highlight="1,4,6" %} $ tree dist . ├── production │ └── datafile-tag-all.json └── staging └── datafile-tag-all.json 2 directories, 2 files ``` {% /column %} {% column %} ```js {% title="After" highlight="1,4,6" %} $ tree datafiles . ├── production │ └── featurevisor-tag-all.json └── staging └── featurevisor-tag-all.json 2 directories, 2 files ``` {% /column %} {% /row %} If you wish to maintain the old naming convention, you can update your project configuration: ```js {% path="featurevisor.config.js" highlight="4-5" %} module.exports = { // ... datafilesDirectoryPath: 'dist', datafileNamePattern: 'datafile-%s.json', } ``` --- ## JavaScript SDK usage ### Upgrade to latest SDK {% label="New" labelType="success" %} In your application repository: ```text {% title="Command" %} $ npm install --save @featurevisor/sdk@2 ``` ### Fetching datafile {% label="Breaking" labelType="error" %} This option has been removed from the SDK. You are now required to take care of fetching the datafile yourself and passing it to the SDK: {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="6-10" %} import { createInstance } from '@featurevisor/sdk' const DATAFILE_URL = '...' const f = createInstance({ datafileUrl: DATAFILE_URL, onReady: () => { console.log('SDK is ready') }, }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="9" %} import { createInstance } from '@featurevisor/sdk' const DATAFILE_URL = '...' const datafileContent = await fetch(DATAFILE_URL) .then((res) => res.json()) const f = createInstance({ datafile: datafileContent, }) ``` `onReady` callback is no longer needed, as the SDK is ready immediately after you pass the datafile. {% /column %} {% /row %} ### Refreshing datafile {% label="Breaking" labelType="error" %} This option has been removed from the SDK. You are now required to take care of fetching the datafile and then set to it existing SDK instance: {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="6-16,20,23-24" %} import { createInstance } from '@featurevisor/sdk' const DATAFILE_URL = '...' const f = createInstance({ datafileUrl: DATAFILE_URL, refreshInterval: 60, // every 60 seconds onRefresh: () => { console.log('Datafile refreshed') }, onUpdate: () => { console.log('New datafile revision detected') }, }) // manually refresh f.refresh() // stop/start refreshing f.stopRefreshing() f.startRefreshing() ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="12-19,26" %} import { createInstance } from '@featurevisor/sdk' const DATAFILE_URL = '...' const datafileContent = await fetch(DATAFILE_URL) .then((res) => res.json()) const f = createInstance({ datafile: datafileContent, }) const unsubscribe = f.on("datafile_set", ({ revision, // new revision previousRevision, revisionChanged, // true if revision has changed features, // list of all affected feature keys }) => { console.log('Datafile set') }); // custom interval setInterval(function () { const datafileContent = await fetch(DATAFILE_URL) .then((res) => res.json()) f.setDatafile(datafileContent) }, 60 * 1000); ``` `refreshInterval`, `onRefresh` and `onUpdate` options and `refresh` method are no longer supported. {% /column %} {% /row %} ### Getting variation {% label="Soft breaking" labelType="warning" %} When evaluating the variation of a feature that is disabled, the SDK used to return `undefined` in v1. This was challenging to handle in non-JavaScript SDKs, since there is no concept of `undefined` as a type there. Therefore, it has been changed to return `null` in v2. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="5" %} const f; // Featurevisor SDK instance const context = { userId: '123' } // could be either `string` or `undefined` const variation = f.getVariation( 'myFeature', context ) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="5" %} const f; // Featurevisor SDK instance const context = { userId: '123' } // now either `string` or `null` const variation = f.getVariation( 'myFeature', context ) ``` {% /column %} {% /row %} ### Getting variable {% label="Soft breaking" labelType="warning" %} Similar to above for getting variation, when evaluating a variable of a feature that is disabled, the SDK will now return `null` instead of `undefined`. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="5" %} const f; // Featurevisor SDK instance const context = { userId: '123' } // could be either value or `undefined` const variableValue = f.getVariable( 'myFeature', 'myVariableKey', context ) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="5" %} const f; // Featurevisor SDK instance const context = { userId: '123' } // now either value or `null` const variation = f.getVariable( 'myFeature', 'myVariableKey', context ) ``` {% /column %} {% /row %} This is applicable for type specific SDK methods as well for variables: - `getVariableString` - `getVariableBoolean` - `getVariableInteger` - `getVariableDouble` - `getVariableArray` - `getVariableObject` - `getVariableJSON` ### Activation {% label="Breaking" labelType="error" %} Experiment activations are not handled by the SDK any more. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="6-18,22" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ // ... onActivate: function ({ featureKey, variationValue, fullContext, captureContext, }) { // send to your analytics service here track('activation', { experiment: featureKey, variation: variationValue, userId: fullContext.userId, }) }, }) const context = { userId: '123' } f.activate('featureKey', context) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="6-13" %} import { createInstance } from '@featurevisor/sdk' const f; // Featurevisor SDK instance const context = { userId: '123' } const variation = f.getVariation("mFeature", context); // send to your analytics service here track('activation', { experiment: 'myFeature', variation: variation.value, userId: context.userId, }) ``` `activate` method and `onActivate` option are no longer supported. You can also make use of new [Hooks API](#hooks). {% /column %} {% /row %} ### Sticky features {% label="Breaking" labelType="error" %} {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="15,19" %} import { createInstance } from '@featurevisor/sdk' const stickyFeatures = { myFeatureKey: { enabled: true, variation: 'control', variables: { myVariableKey: 'myVariableValue', }, }, } // when creating instance const f = createInstance({ stickyFeatures: stickyFeatures, }) // replacing sticky features later f.setStickyFeatures(stickyFeatures) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="15,19" %} import { createInstance } from '@featurevisor/sdk' const stickyFeatures = { myFeatureKey: { enabled: true, variation: 'control', variables: { myVariableKey: 'myVariableValue', }, }, } // when creating instance const f = createInstance({ sticky: stickyFeatures, }) // replacing sticky features later f.setSticky(stickyFeatures, true) ``` Unless `true` is passed as the second argument, the sticky features will be merged with the existing ones. {% /column %} {% /row %} ### Initial features {% label="Breaking" labelType="error" %} Initial features used to be handy for setting some early values before the SDK fetched datafile and got ready. But since datafile fetching responsibility is now on you, the initial features are no longer needed. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="15" %} import { createInstance } from '@featurevisor/sdk' const initialFeatures = { myFeatureKey: { enabled: true, variation: 'control', variables: { myVariableKey: 'myVariableValue', }, }, } // when creating instance const f = createInstance({ initialFeatures: initialFeatures, }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="15,19,22-27" %} import { createInstance } from '@featurevisor/sdk' const initialFeatures = { myFeatureKey: { enabled: true, variation: 'control', variables: { myVariableKey: 'myVariableValue', }, }, } // you can pass them as sticky instead const f = createInstance({ sticky: initialFeatures, }) // fetch and set datafile after f.setDatafile(datafileContent) // remove sticky features after f.setSticky( {}, // replacing with empty object true ) ``` {% /column %} {% /row %} ### Setting context {% label="New" labelType="success" %} {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="11" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ // ... }) const isFeatureEnabled = f.isEnabled( 'myFeature', // pass context directly only { userId: '123' }, ) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="7,11-13,16-22,33" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ // ... // optional initial context context: { browser: 'chrome' }, }) // set more context later (append) f.setContext({ userId: '123', }) // replace currently set context entirely f.setContext( { userId: '123', browser: 'firefox', }, true, // replace ) // already set context will be used automatically const isFeatureEnabled = f.isEnabled('myFeature') // you can still pass context directly // for overriding specific attributes const isFeatureEnabled = f.isEnabled( 'myFeature', // still allows passing context directly { browser: 'edge' }, ) ``` {% /column %} {% /row %} ### Logging {% label="Breaking" labelType="error" %} Instead of passing all log [levels](/docs/sdks/javascript/#levels) individually, you can now pass a single level to the SDK. The set level will cover all the levels below it, so you can pass `debug` to cover all the levels together. #### Creating logger instance {% label="Breaking" labelType="error" %} {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="8-13" %} import { createInstance, createLogger } from '@featurevisor/sdk' const f = createInstance({ logger: createLogger({ levels: [ 'debug', 'info', 'warn', 'error', ], }) }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="8" %} import { createInstance, createLogger } from '@featurevisor/sdk' const f = createInstance({ logger: createLogger({ level: 'debug', }) }) ``` Setting `debug` will now cover all the levels together, instead of having to pass them all individually. {% /column %} {% /row %} #### Passing log level when creating SDK instance {% label="New" labelType="success" %} Alternatively, you can also pass the log level directly when creating the SDK instance: ```js {% path="your-app/index.js" highlight="4" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ logLevel: 'debug', }) ``` #### Setting log level after creating SDK instance {% label="Breaking" labelType="error" %} You can also change the log level after creating the SDK instance: {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="" %} f.setLogLevels([ 'error', 'warn', 'info', 'debug', ]) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="8" %} f.setLogLevel('debug') ``` {% /column %} {% /row %} Read more in [Logging](/docs/sdks/javascript/#logging) section. ### Hooks {% label="New" labelType="success" %} Hooks are a set of new APIs allowing you to intercept the evaluation process and customize it. A hook can be defined as follows: ```ts {% title="Defining a hook" path="your-app/index.ts" %} import { Hook } from "@featurevisor/sdk" const myCustomHook: Hook = { // only required property name: 'my-custom-hook', // rest of the properties below are all optional per hook // before evaluation before: function (options) { const { type, // `feature` | `variation` | `variable` featureKey, variableKey, // if type is `variable` context } options; // update context before evaluation options.context = { ...options.context, someAdditionalAttribute: 'value', } return options }, // after evaluation after: function (evaluation, options) { const { reason // `error` | `feature_not_found` | `variable_not_found` | ... } = evaluation if (reason === "error") { // log error return } }, // configure bucket key bucketKey: function (options) { const { featureKey, context, bucketBy, bucketKey, // default bucket key } = options; // return custom bucket key return bucketKey }, // configure bucket value (between 0 and 100,000) bucketValue: function (options) { const { featureKey, context, bucketKey, bucketValue, // default bucket value } = options; // return custom bucket value return bucketValue }, } ``` You can register the hook when creating SDK instance: ```js {% title="When creating instance" path="your-app/index.js" highlight="6" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ // ... hooks: [myCustomHook], }) ``` You can also register the hook after creating the SDK instance: ```js {% title="After creating instance" path="your-app/index.js" highlight="3,6" %} const f; // Featurevisor SDK instance const removeHook = f.addHook(myCustomHook) // remove the hook later removeHook() ``` ### Intercepting context {% label="Breaking" labelType="error" %} {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="6-12" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ // ... interceptContext: function (context) { // modify context before evaluation return { ...context, someAdditionalAttribute: 'value', } }, }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="6-19" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ // ... hooks: [ { name: 'intercept-context', before: function (options) { // modify context before evaluation options.context = { ...options.context, someAdditionalAttribute: 'value', } return options }, }, ], }) ``` {% /column %} {% /row %} ### Events {% label="Breaking" labelType="error" %} All the known events from v1 SDK have been removed in v2 SDK: - Readiness: see [fetching datafile](#fetching-datafile) - `onReady` option and method - `ready` event - Refreshing: see [refreshing datafile](#refreshing-datafile) - `refresh` event and method - `startRefreshing` method - `stopRefreshing` method - `onRefresh` option - `update` event - `onUpdate` option - Activation: see [activation](#activation) - `activate` event and method - `onActivate` option A new set of events has been introduced which are more generic. Because of these changes, reactivity is vastly improved allowing you to listen to the changes of specific features and react to them in a highly efficient way without having to reload or restart your application. #### datafile_set {% label="New" labelType="success" %} Will trigger when a datafile is set to the SDK instance: ```js {% path="your-app/index.js" highlight="3-10" %} const f; // Featurevisor SDK instance const unsubscribe = f.on("datafile_set", ({ revision, // new revision previousRevision, revisionChanged, // true if revision has changed features, // list of all affected feature keys }) => { console.log('Datafile set') }) unsubscribe(); ``` #### context_set {% label="New" labelType="success" %} Will trigger when context is set to the SDK instance: ```js {% path="your-app/index.js" highlight="3-7" %} const f; // Featurevisor SDK instance const unsubscribe = f.on("context_set", ({ replaced, // true if context was replaced context, // the new context }) => { console.log('Context set') }) unsubscribe(); ``` #### sticky_set {% label="New" labelType="success" %} Will trigger when sticky features are set to the SDK instance: ```js {% path="your-app/index.js" highlight="3-7" %} const f; // Featurevisor SDK instance const unsubscribe = f.on("sticky_set", ({ replaced, // true if sticky features got replaced features, // list of all affected feature keys }) => { console.log('Sticky features set') }) unsubscribe(); ``` ### Child instance {% label="New" labelType="success" %} It's one thing to deal with the same SDK instance when you are building a client-side application (think web or mobile app) where only one user is accessing the application. But when you are building a server-side application (think a REST API) serving many different users simultaneously, you may want to have different SDK instances with user or request specific context. Child instances make it very easy to achieve that now: ```js {% title="Primary instance" %} import { createInstance } from '@featurevisor/sdK' const f = createInstance({ datafile: datafileContent, }) // set common context for all f.setContext({ apiVersion: '5.0.0', }) ``` Afterwards, you can spawn child instances from it: ```js {% title="Child instance" highlight="3,9" %} // creating a child instance with its own context // (will get merged with parent context if available before evaluations) const childF = f.spawn({ userId: '234', country: 'nl', }) // evaluate via spawned child instance const isFeatureEnabled = childF.isEnabled('myFeature') ``` Similar to primary instance, you can also set context and sticky features in child instances: ```js {% title="Child instance: setting context" highlight="2,7" %} // override child context later if needed childF.setContext({ country: 'de', }) // when evaluating, you can still pass additional context const isFeatureEnabled = childF.isEnabled('myFeature', { browser: 'firefox', }) ``` Methods similar to primary instance are all available on child instances: - `isEnabled` - `getVariation` - `getVariable` - `getVariableBoolean` - `getVariableString` - `getVariableInteger` - `getVariableDouble` - `getVariableArray` - `getVariableObject` - `getVariableJSON` - `getAllEvaluations` - `setContext` - `setSticky` - `on` ### Get all evaluations {% label="New" labelType="success" %} You can get evaluation results of all your features currently loaded via datafile in the SDK instance: ```js {% path="your-app/index.js" %} const f; // Featurevisor SDK instance const allEvaluations = f.getAllEvaluations(context = {}) console.log(allEvaluations) // { // myFeature: { // enabled: true, // variation: "control", // variables: { // myVariableKey: "myVariableValue", // }, // }, // // anotherFeature: { // enabled: true, // variation: "treatment", // } // } ``` This can be very useful when you want to serialize all evaluations, and hand it off from backend to frontend for example. ### Configuring bucket key {% label="Breaking" labelType="error" %} {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="4-14" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ configureBucketKey: function (options) { const { featureKey, context, // default bucket key bucketKey, } = options return bucketKey }, }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="4-20" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ hooks: [ { name: 'my-custom-hook', bucketKey: function (options) { const { featureKey, context, bucketBy, // default bucket key bucketKey, } = options return bucketKey }, }, ], }) ``` {% /column %} {% /row %} ### Configuring bucket value {% label="Breaking" labelType="error" %} {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="4-14" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ configureBucketValue: function (options) { const { featureKey, context, // default bucket value bucketValue, } = options return bucketValue }, }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="4-20" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ hooks: [ { name: 'my-custom-hook', bucketValue: function (options) { const { featureKey, context, bucketKey // default bucket value bucketValue, } = options return bucketValue }, }, ], }) ``` {% /column %} {% /row %} Learn more in [JavaScript SDK](/docs/sdks/javascript/) page. ## React SDK usage All the hooks are now reactive. Meaning, your components will automatically re-render when: - a newew datafile is set - context is set or updated - sticky features are set or updated Learn more in [React SDK](/docs/react/) page. --- ## Testing features ### sticky {% label="New" labelType="success" %} Test specs of features can now also include sticky features, similar to SDK's API: ```yml {% path="tests/features/myFeature.spec.yml" highlight="9-14" %} feature: myFeature assertions: - description: My feature is enabled environment: production at: 100 context: country: nl sticky: myFeatureKey: enabled: true variation: control variables: myVariableKey: myVariableValue expectedToBeEnabled: true ``` ### expectedEvaluations {% label="New" labelType="success" %} You can go deep with testing feature evaluations, including their evaluation reasons for example: ```yml {% path="tests/features/myFeature.spec.yml" highlight="10-20" %} feature: myFeature assertions: - description: My feature is enabled environment: production at: 100 context: country: nl expectedToBeEnabled: true expectedEvaluations: flag: enabled: true reason: rule # see available rules in Evaluation type from SDK variation: variationValue: control reason: rule variables: myVariableKey: value: myVariableValue reason: rule ``` ### children {% label="New" labelType="success" %} Based on the new [child instance](#child-instance) API in SDK, you can also imitate testing against them via test specs: ```yml {% path="tests/features/myFeature.spec.yml" highlight="10-19" %} feature: myFeature assertions: - description: My feature is enabled environment: production at: 100 context: apiVersion: 5.0.0 children: - context: userId: '123' country: nl expectedToBeEnabled: true - context: userId: '456' country: de expectedToBeEnabled: false ``` Learn more in [Testing](/docs/testing/) page.]]> https://featurevisor.com/docs/migrations/v3 Migrating from v2 to v3 Guide for migrating from Featurevisor v2 to v3 src/app/docs/migrations/v3/page.md ` because one generated package represents one set's definitions. Target selection is also available on `build`, `test`, `evaluate`, `benchmark`, `assess-distribution`, `list --features`, and `info`. The option is repeatable where several targets can be processed. Each runtime command evaluates every selected target independently. A normal build writes every selected target datafile, while `build --json` and `build --print` accept only one target because they emit one datafile. ### Working with a single set {% label="New" labelType="success" %} Most commands accept a `--set` flag to scope them to one set: ```{% title="Command" %} $ npx featurevisor build --set=storefront $ npx featurevisor test --set=storefront ``` --- ## JavaScript SDK usage ### Upgrade to latest SDK {% label="New" labelType="success" %} In your application repository: ```text {% title="Command" %} $ npm install --save @featurevisor/sdk@3 ``` ### Creating an instance {% label="Breaking" labelType="error" %} `createInstance` has been renamed to `createFeaturevisor`. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="1,3" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ datafile: datafileContent, }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="1,3" %} import { createFeaturevisor } from '@featurevisor/sdk' const f = createFeaturevisor({ datafile: datafileContent, }) ``` {% /column %} {% /row %} ### Instance type renamed {% label="Breaking" labelType="error" %} The `FeaturevisorInstance` type has been renamed to `Featurevisor`. {% row %} {% column %} ```ts {% title="Before" path="your-app/index.ts" highlight="1,3" %} import type { FeaturevisorInstance } from '@featurevisor/sdk' let f: FeaturevisorInstance ``` {% /column %} {% column %} ```ts {% title="After" path="your-app/index.ts" highlight="1,3" %} import type { Featurevisor } from '@featurevisor/sdk' let f: Featurevisor ``` {% /column %} {% /row %} ### Options type renamed {% label="Breaking" labelType="error" %} The constructor options type has been renamed from `InstanceOptions` to `FeaturevisorOptions`. {% row %} {% column %} ```ts {% title="Before" path="your-app/featurevisor.ts" %} import type { InstanceOptions } from '@featurevisor/sdk' const options: InstanceOptions = { datafile: datafileContent, } ``` {% /column %} {% column %} ```ts {% title="After" path="your-app/featurevisor.ts" %} import type { FeaturevisorOptions } from '@featurevisor/sdk' const options: FeaturevisorOptions = { datafile: datafileContent, } ``` {% /column %} {% /row %} Learn more in [JavaScript SDK](/docs/sdks/javascript/) page. ### setDatafile merges by default {% label="Soft breaking" labelType="warning" %} In v2, calling `setDatafile` replaced the instance's datafile. In v3, it merges with the existing datafile by default. Incoming `features` and `segments` override matching keys, while existing ones that are missing from the incoming datafile are kept. To get the old replacing behavior, pass `true` as the second argument. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" %} // always replaced f.setDatafile(datafileContent) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="5" %} // merges by default f.setDatafile(datafileContent) // pass true to replace entirely f.setDatafile(datafileContent, true) ``` {% /column %} {% /row %} Merging makes it possible to load smaller datafiles on demand. Learn more in [Loading datafiles on demand](/docs/use-cases/on-demand-datafiles/). ### Sticky values belong to instances {% label="Breaking" labelType="error" %} The per-evaluation `sticky` override has been removed. Set sticky values on the main instance, or give a child instance its own sticky values when spawning it. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="3" %} const enabled = f.isEnabled('checkout', context, { sticky: stickyFeatures, }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="1,4" %} f.setSticky(stickyFeatures) const enabled = f.isEnabled('checkout', context) ``` {% /column %} {% /row %} For isolated state, use `f.spawn(context, { sticky: stickyFeatures })` and evaluate through the returned child instance. ### Diagnostics instead of logger {% label="Breaking" labelType="error" %} The `logger` option and the `createLogger` function have been removed. The SDK now reports diagnostics, which you control with `logLevel` and an optional `onDiagnostic` handler. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="1-4,7-9" %} import { createInstance, createLogger, } from '@featurevisor/sdk' const f = createInstance({ logger: createLogger({ level: 'debug', }), }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="1,4" %} import { createFeaturevisor } from '@featurevisor/sdk' const f = createFeaturevisor({ logLevel: 'debug', }) ``` {% /column %} {% /row %} You can pass your own handler if you do not want diagnostics printed to the console: ```js {% path="your-app/index.js" highlight="3-5" %} const f = createFeaturevisor({ logLevel: 'info', onDiagnostic: function (diagnostic) { // send to your observability system }, }) ``` The `setLogLevel` method still works for changing the level at runtime. Learn more in [Diagnostics](/docs/sdks/javascript/#diagnostics) section. ### Modules instead of hooks {% label="Breaking" labelType="error" %} The `hooks` API has been replaced by the [modules](/docs/sdks/javascript/#modules) API. The `Hook` type is now `FeaturevisorModule`, `hooks` is now `modules`, and `addHook` is now `addModule`. Your existing `before`, `after`, `bucketKey`, and `bucketValue` callbacks carry over unchanged. Modules also add an optional `setup` lifecycle and a `close` callback. {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="1,4" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ hooks: [myCustomHook], }) const removeHook = f.addHook(myCustomHook) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="1,4" %} import { createFeaturevisor } from '@featurevisor/sdk' const f = createFeaturevisor({ modules: [myCustomModule], }) const removeModule = f.addModule(myCustomModule) await removeModule?.() await f.removeModule('my-custom-module') ``` {% /column %} {% /row %} The old `interceptContext`, `configureBucketKey`, and `configureBucketValue` options, which were already replaced by hooks in v2, are now expressed as modules. Module cleanup can be asynchronous, so the function returned by `addModule` and `removeModule` should be awaited. Learn more in [Modules](/docs/sdks/javascript/#modules) section. ### Public SDK surface reduced {% label="Breaking" labelType="error" %} The package root now focuses on `createFeaturevisor`, the `Featurevisor` type, child instances, evaluations, events, diagnostics, modules, and Featurevisor data types. `DatafileReader`, logger and emitter implementations, evaluator dependency objects, and other internal helpers are no longer public APIs. If application code used `DatafileReader` to inspect the active datafile, use the corresponding instance methods instead: | Removed usage | Instance replacement | | ------------------------------------ | ------------------------------- | | `reader.getRevision()` | `f.getRevision()` | | `reader.getSchemaVersion()` | `f.getSchemaVersion()` | | `reader.getSegment(key)` | `f.getSegment(key)` | | `reader.getFeature(key)` | `f.getFeature(key)` | | `reader.getFeatureKeys()` | `f.getFeatureKeys()` | | `reader.getVariableKeys(featureKey)` | `f.getVariableKeys(featureKey)` | | `reader.hasVariations(featureKey)` | `f.hasVariations(featureKey)` | Remove direct imports of undocumented logger, emitter, reader, and evaluator helpers. Use [diagnostics](/docs/sdks/javascript/#diagnostics), [events](/docs/sdks/javascript/#events), and the main instance API instead. The v1-only `DatafileContentV1`, `FeatureV1`, `VariationV1`, and `VariableV1` types have also been removed. Use the normal v2 datafile types. `VariableValue` no longer includes `undefined`; missing evaluated values use `null`. --- ## Other SDK usage The v3 SDKs use the same Featurevisor focused factory naming as the JavaScript SDK. The old instance factory names are removed as a clean breaking change. | SDK | v2 factory | v3 factory | | ------ | ------------------------------ | ---------------------------------- | | Go | `NewFeaturevisor` | `CreateFeaturevisor` | | Swift | `createInstance` | `createFeaturevisor` | | Java | `Featurevisor.createInstance` | `Featurevisor.createFeaturevisor` | | Ruby | `Featurevisor.create_instance` | `Featurevisor.create_featurevisor` | | Python | `create_instance` | `create_featurevisor` | | PHP | `Featurevisor::createInstance` | `Featurevisor::createFeaturevisor` | Go now uses `FeaturevisorOptions` instead of `Options`. Java now uses `Featurevisor.FeaturevisorOptions` instead of `Featurevisor.Options`. Swift and Python expose the main instance as `Featurevisor` instead of `FeaturevisorInstance`. Update imports, type annotations, factory calls, and options type names together when upgrading an application. See the relevant [SDK guide](/docs/sdks/) for language specific examples. Custom logger injection has also been removed from all v3 SDKs. Configure diagnostic verbosity with `logLevel`, update it with `setLogLevel` or the idiomatic equivalent, and send structured diagnostics to your observability system with `onDiagnostic`. Remove uses of `createLogger`, `create_logger`, `NewLogger`, logger handler options, and logger fields in SDK options. The same runtime changes apply across the v3 SDKs: - hooks are replaced by modules with `setup`, `before`, `bucketKey`, `bucketValue`, `after`, and `close` lifecycle callbacks; - `setDatafile` merges features and segments by default, with an explicit replace argument for the old behavior; - sticky values are configured on the main or child instance instead of individual evaluations; - diagnostics replace custom logger injection; - datafiles use schema version `2` only. The exact method names and callback types follow each language's conventions. Review the relevant [SDK guide](/docs/sdks/) before upgrading and update code and tests together. ## React and Vue SDK usage The React and Vue packages now expect the renamed `createFeaturevisor` function and `Featurevisor` type. Update any provider, application setup, or utility code that imports the old factory or type. The React provider still receives an already-created instance: ```jsx {% path="your-app/index.jsx" highlight="1,4" %} import { createFeaturevisor } from '@featurevisor/sdk' import { FeaturevisorProvider } from '@featurevisor/react' const f = createFeaturevisor({ datafile: datafileContent }) root.render( , ) ``` If an older Vue setup still passes `datafileUrl`, fetch the datafile before creating the instance instead: {% row %} {% column %} ```js {% title="Before" path="your-app/index.js" highlight="1,4" %} import { createInstance } from '@featurevisor/sdk' const f = createInstance({ datafileUrl: DATAFILE_URL, }) ``` {% /column %} {% column %} ```js {% title="After" path="your-app/index.js" highlight="1,3-4,6" %} import { createFeaturevisor } from '@featurevisor/sdk' const datafile = await fetch(DATAFILE_URL) .then(response => response.json()) const f = createFeaturevisor({ datafile }) ``` {% /column %} {% /row %} The Vue package no longer ships `useStatus` or `activateFeature`. To track exposure, register a [module](/docs/sdks/javascript/#modules) that reacts to evaluations in its `after` callback. Learn more in [React SDK](/docs/react/) and [Vue.js SDK](/docs/vue/) pages. --- ## Migration checklist Use this order for each existing project: 1. Upgrade `@featurevisor/cli` and declare environments explicitly when needed. 2. Move split environment files back into feature definitions, or model independent trees as sets. 3. Convert every scope into a target and add at least one target for building datafiles. 4. Decide whether to keep slash-separated namespace keys or migrate application and test references to dots. 5. Review multi-child `segments.not` expressions and remove empty logical arrays. 6. Replace assertion `scope` and `tag` fields with `target`, then run the complete test suite. 7. Build and inspect every target and environment datafile before publishing it. 8. Update Catalog, promotion, code-generation, and CI commands and output paths used by automation. 9. Upgrade each application SDK, update factory and type names, and replace logger and hook integrations. 10. Review `setDatafile`, sticky ownership, module cleanup, and removed helper imports in application code. 11. Run application tests against the same v3-generated datafiles that will be deployed. The project and application upgrades can remain separate deployments as described at the start of this guide.]]> https://featurevisor.com/docs/mutations Variable mutations Mutation variable values when overriding them to avoid full replacements src/app/docs/mutations/page.md https://featurevisor.com/docs/namespaces Namespaces Organize your features and segments under namespaces in a hierarchical way. src/app/docs/namespaces/page.md https://featurevisor.com/docs/plugins Plugins API Extend Featurevisor CLI with additional tooling using the plugins API. src/app/docs/plugins/page.md https://featurevisor.com/docs/projects Projects Learn how to create and manage Featurevisor projects src/app/docs/projects/page.md https://featurevisor.com/docs/promotions Promotions Copy feature definitions from one Featurevisor set to another with the promote command. src/app/docs/promotions/page.md https://featurevisor.com/docs/quick-start Quick start Quick start guide for Featurevisor src/app/docs/quick-start/page.md = 24.0.0 ## Initialize your project Run the following command to initialize your project: ```{% title="Command" %} $ npx @featurevisor/cli init ``` This is meant to be a completely separate repository from your application code. Learn more in [Projects](/docs/projects) page. ## Installation Once your project has been initialized, install all the dependencies: ```{% title="Command" %} $ npm install ``` ## Configure your project Featurevisor's project configuration is stored in `featurevisor.config.js` file, with minimum configuration looking like this: ```js {% path="featurevisor.config.js" %} module.exports = { tags: [ 'all', ], environments: [ 'staging', 'production' ], }; ``` Learn more in [Configuration](/docs/configuration). By default, Featurevisor defines [attributes](/docs/attributes), [segments](/docs/segments), and [features](/docs/features) as YAML files. If you want JSON, TOML, or any other format, see [custom parsers](/docs/advanced/custom-parsers) guide. ## Create an attribute Attributes are the building blocks of creating conditions. We will start by creating an attribute called `userId`: ```yml {% path="attributes/userId.yml" %} description: User ID type: string ``` Learn more in [Attributes](/docs/attributes). ## Create a segment Segments are reusable conditions that can be applied as rules in your features to target specific users or groups of users. Let's create a new attribute called `country` first: ```yml {% path="attributes/country.yml" %} description: Country type: string ``` Now, let's create a segment called `germany`: ```yml {% path="segments/germany.yml" %} description: Users from Germany conditions: - attribute: country operator: equals value: de ``` Learn more in [Segments](/docs/segments). ## Create a feature We have come to the most interesting part now. We can create a new `showBanner` feature, that controls showing a banner on our website: ```yml {% path="features/showBanner.yml" %} description: Show banner tags: - all # this makes sure the same User ID consistently gets the same experience bucketBy: userId rules: # staging rules staging: - key: everyone # unique key for this rule segments: '*' # targeting everyone percentage: 100 # rolled out to 100% of the traffic # production rules production: - key: de segments: germany # rolling out to Germany first percentage: 100 - key: everyone segments: '*' # everyone percentage: 0 # disabled for everyone else ``` Learn more in [Features](/docs/features) for complex setups involving [variations](/docs/features/#variations) and [variables](/docs/features/#variables). ## Create a target Targets decide the [datafiles](/docs/building-datafiles) (pure JSON files that [SDKs](/docs/sdks/) consume) that get built. The `init` command already created a default `all` target for you: ```yml {% path="targets/all.yml" %} description: All features tag: all ``` This target selects every feature tagged with `all`, and builds a `featurevisor-all.json` datafile in `datafiles/` directory. Learn more in [Targets](/docs/targets) and [Building datafiles](/docs/building-datafiles). ## Linting We can lint the content of all our definitions to make sure they are all valid: ```{% title="Command" %} $ npx featurevisor lint ``` Learn more in [Linting](/docs/linting). ## Build datafiles Datafiles are static JSON files that we expect our client-side applications to consume using the Featurevisor [SDKs](/docs/sdks/). Now that we have all the definitions in place, we can build the project: ```{% title="Command" %} $ npx featurevisor build ``` This will generate datafiles in the `datafiles` directory for each of your [targets](/docs/targets/) against each [environment](/docs/environments/) as defined in your [`featurevisor.config.js`](/docs/configuration/) file. With our example, we will have the following datafiles generated: ``` datafiles/ ├── staging/ │ └── featurevisor-all.json └── production/ └── featurevisor-all.json ``` Learn more in [Building datafiles](/docs/building-datafiles). ## Deploy datafiles This is the part where you deploy the datafiles to your CDN or any custom server. Once done, the URLs of the datafiles may look like `https://cdn.yoursite.com/production/featurevisor-all.json`. Learn more in [Deployment](/docs/deployment). ## Consume datafiles using the SDK Now that we have the datafiles deployed, we can consume them using Featurevisor [SDKs](/docs/sdks/). ### Install SDK In your application, install the Featurevisor SDK first: ```{% title="Command" %} $ npm install --save @featurevisor/sdk ``` Featurevisor JavaScript SDK is compatible with both Node.js and browser environments. Find more SDKs in other languages [here](/docs/sdks/). ### Initialize SDK You can initialize the SDK as follows: ```js {% path="your-app/index.js" %} import { createFeaturevisor } from '@featurevisor/sdk' const datafileUrl = 'https://cdn.yoursite.com/production/featurevisor-all.json' const datafileContent = await fetch(datafileUrl) .then((res) => res.json()) const f = createFeaturevisor({ datafile: datafileContent, }) ``` ### Set context Let the SDK know against what context the values should be evaluated: ```js const context = { userId: '123', country: 'de', } f.setContext(context) ``` ### Evaluate values Once the SDK is initialized, you can evaluate your features: ```js // flag status: true or false const isBannerEnabled = f.isEnabled('showBanner') ``` Featurevisor SDK will take care of evaluating the right value(s) for you synchronously against the provided `userId` and `country` attributes in the context. ### Variables & Variations Above example only makes use of the feature's boolean flag status only, but features may also contain [variables](/docs/features/#variables) and [variations](/docs/features/#variations), which can be evaluated with the SDK instance: ```js // variation: `control`, `treatment`, or more const bannerVariation = f.getVariation('showBanner', context) // variables const variableKey = 'myVariableKey' const myVariable = f.getVariable('showBanner', variableKey, context) ``` Find more examples of SDK usage [here](/docs/sdks/javascript/). ## Further reading - [Attributes](/docs/attributes) - [Segments](/docs/segments) - [Features](/docs/features) - [Targets](/docs/targets) - [Linting](/docs/linting) - [Testing](/docs/testing) - [Building datafiles](/docs/building-datafiles) - [Deployment](/docs/deployment) - [SDKs](/docs/sdks)]]> https://featurevisor.com/docs/react React SDK Learn how to use Featurevisor SDK with React for evaluating feature flags, variations, and variables src/app/docs/react/page.md response.json()) const f = createFeaturevisor({ datafile: datafileContent, }) f.setContext({ userId: '123', }) ReactDOM.render( , document.getElementById('root'), ) ``` ## React hooks The package comes with several hooks to use in your components: ### useFeaturevisor To access the bound methods coming from the underlying [JavaScript SDK](/docs/sdks/javascript) instance: ```jsx import React from 'react' import { useFeaturevisor } from '@featurevisor/react' function MyComponent(props) { const { isEnabled, getVariation, getVariable, getVariableBoolean, getVariableString, getVariableInteger, getVariableDouble, getVariableArray, getVariableObject, getVariableJSON, setContext, getContext, setSticky, } = useFeaturevisor() return

...

} ``` Learn more about the API from original [JavaScript SDK](/docs/sdks/javascript) documentation. The functions returned from the hook will not trigger any re-renders of the component if the [datafile](/docs/sdks/javascript/#setting-datafile) or [context](/docs/sdks/javascript/#context) changes at the instance level. The ones below are reactive. ### useFlag Check if a feature is [enabled](/docs/flags) or not: ```jsx import React from 'react' import { useFlag } from '@featurevisor/react' function MyComponent(props) { const isEnabled = useFlag('myFeatureKey') if (isEnabled) { return

Feature is enabled

} return

Feature is disabled

} ``` ### useVariation Get a feature's evaluated [variation](/docs/variations): ```jsx import React from 'react' import { useVariation } from '@featurevisor/react' function MyComponent(props) { const variation = useVariation('myFeatureKey') if (variation === 'b') { return

B variation

; } if (variation === 'c') { return

C variation

; } // default return

Default experience

; }; ``` ### useVariable Get a feature's evaluated [variable](/docs/variables) value: ```jsx import React from 'react': import { useVariable } from '@featurevisor/react' function MyComponent(props) { const colorValue = useVariable('myFeatureKey', 'color') return

Color: {colorValue}

; }; ``` ### useSdk If you want to access the full Featurevisor [SDK](/docs/sdks/javascript) instance: ```jsx import React from 'react' import { useSdk } from '@featurevisor/react' function MyComponent(props) { const f = useSdk() return

...

} ``` ## Passing additional context All the evaluation hooks accept an optional argument for passing additional component-level [context](/docs/sdks/javascript/#context): ```js const context = { // ... additional context here in component } useFlag(featureKey, context) useVariation(featureKey, context) useVariable(featureKey, variableKey, context) ``` ## Reactivity All the evaluation hooks are reactive. This means that your components will automatically re-render when: - a newer [datafile is set](/docs/sdks/javascript/#setting-datafile) - [context is set or updated](/docs/sdks/javascript/#context) - [sticky features are set or updated](/docs/sdks/javascript/#sticky) The re-rendering logic is smart enough to compare previously known value with the new evaluated value, and will only re-render the component if the value has changed. If you do not want any reactivity, you are better off using the Featurevisor SDK instance directly in your component either via `useSdk` or `useFeaturevisor` hooks. ## Optimization Given the nature of components in React, they can re-render many times. You are advised to minimize the number of calls to Featurevisor SDK in your components by using memoization techniques. ## Example repository You can find a fully functional example of a React application using Featurevisor SDK here: [https://github.com/featurevisor/featurevisor-example-react](https://github.com/featurevisor/featurevisor-example-react).]]>
https://featurevisor.com/docs/react-native React Native SDK Learn how to use Featurevisor SDK with React Native for evaluating feature flags when building iOS and Android apps src/app/docs/react-native/page.md Feature is {isEnabled ? 'enabled' : 'disabled'} } ``` ## Example repository You can find a fully functioning example app built with React Native and Featurevisor SDK here: [https://github.com/featurevisor/featurevisor-example-react-native](https://github.com/featurevisor/featurevisor-example-react-native).]]> https://featurevisor.com/docs/roadmap Roadmap Future plans and roadmap for Featurevisor src/app/docs/roadmap/page.md https://featurevisor.com/docs/schemas Reusable schemas for variables Define schemas once and reuse them when defining multiple variables spanning across multiple features src/app/docs/schemas/page.md https://featurevisor.com/docs/scopes Scopes Scopes have been replaced by Targets in Featurevisor. src/app/docs/scopes/page.md https://featurevisor.com/docs/sdks SDKs Learn how to use Featurevisor SDKs src/app/docs/sdks/page.md https://featurevisor.com/docs/sdks/browser Browser SDK Learn how to use Featurevisor SDK in browser environments src/app/docs/sdks/browser/page.md https://featurevisor.com/docs/sdks/go Go SDK Learn how to use Featurevisor Go SDK src/app/docs/sdks/go/page.md ## Installation In your Go application, install the SDK using Go modules: ```bash go get github.com/featurevisor/featurevisor-go ``` ## Public API The main runtime API is `featurevisor.CreateFeaturevisor()`: ```go f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{ Datafile: datafileContent, }) ``` Most applications only need `CreateFeaturevisor`, the `Featurevisor` instance type, and `FeaturevisorOptions`. Public extension and observability types include `FeaturevisorModule`, `FeaturevisorDiagnostic`, and the datafile model types. ## Initialization The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly: ```go package main import ( "io" "net/http" "github.com/featurevisor/featurevisor-go" ) func main() { datafileURL := "https://cdn.yoursite.com/datafile.json" resp, err := http.Get(datafileURL) if err != nil { panic(err) } defer resp.Body.Close() datafileBytes, err := io.ReadAll(resp.Body) if err != nil { panic(err) } var datafileContent featurevisor.DatafileContent if err := datafileContent.FromJSON(string(datafileBytes)); err != nil { panic(err) } f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{ Datafile: datafileContent, }) } ``` ## Evaluation types We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/): - [**Flag**](#check-if-enabled) (`bool`): whether the feature is enabled or not - [**Variation**](#getting-variation) (`string`): the variation of the feature (if any) - [**Variables**](#getting-variables): variable values of the feature (if any) These evaluations are run against the provided context. ## Context Contexts are [attribute](https://featurevisor.com/docs/attributes) values that we pass to SDK for evaluating [features](https://featurevisor.com/docs/features) against. Think of the conditions that you define in your [segments](https://featurevisor.com/docs/segments/), which are used in your feature's [rules](https://featurevisor.com/docs/features/#rules). They are plain maps: ```go context := featurevisor.Context{ "userId": "123", "country": "nl", // ...other attributes } ``` Context can be passed to SDK instance in various different ways, depending on your needs: ### Setting initial context You can set context at the time of initialization: ```go import ( "github.com/featurevisor/featurevisor-go" ) f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{ Context: featurevisor.Context{ "deviceId": "123", "country": "nl", }, }) ``` This is useful for values that don't change too frequently and available at the time of application startup. ### Setting after initialization You can also set more context after the SDK has been initialized: ```go f.SetContext(featurevisor.Context{ "userId": "234", }) ``` This will merge the new context with the existing one (if already set). ### Replacing existing context If you wish to fully replace the existing context, you can pass `true` in second argument: ```go f.SetContext(featurevisor.Context{ "deviceId": "123", "userId": "234", "country": "nl", "browser": "chrome", }, true) // replace existing context ``` ### Manually passing context You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations: ```go context := featurevisor.Context{ "userId": "123", "country": "nl", } isEnabled := f.IsEnabled("my_feature", context) variation := f.GetVariation("my_feature", context) variableValue := f.GetVariable("my_feature", "my_variable", context) ``` When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value. Further details for each evaluation types are described below. ## Check if enabled Once the SDK is initialized, you can check if a feature is enabled or not: ```go featureKey := "my_feature" isEnabled := f.IsEnabled(featureKey) if isEnabled { // do something } ``` You can also pass additional context per evaluation: ```go isEnabled := f.IsEnabled(featureKey, featurevisor.Context{ // ...additional context }) ``` ## Getting variation If your feature has any [variations](https://featurevisor.com/docs/features/#variations) defined, you can evaluate them as follows: ```go featureKey := "my_feature" variation := f.GetVariation(featureKey) if variation != nil && *variation == "treatment" { // do something for treatment variation } else { // handle default/control variation } ``` Additional context per evaluation can also be passed: ```go variation := f.GetVariation(featureKey, featurevisor.Context{ // ...additional context }) ``` ## Getting variables Your features may also include [variables](https://featurevisor.com/docs/features/#variables), which can be evaluated as follows: ```go variableKey := "bgColor" bgColorValue := f.GetVariable("my_feature", variableKey) ``` Additional context per evaluation can also be passed: ```go bgColorValue := f.GetVariable("my_feature", variableKey, featurevisor.Context{ // ...additional context }) ``` ### Type specific methods Next to generic `GetVariable()` methods, there are also type specific methods available for convenience: ```go f.GetVariableBoolean(featureKey, variableKey, context) f.GetVariableString(featureKey, variableKey, context) f.GetVariableInteger(featureKey, variableKey, context) f.GetVariableDouble(featureKey, variableKey, context) f.GetVariableArray(featureKey, variableKey, context) f.GetVariableObject(featureKey, variableKey, context) f.GetVariableJSON(featureKey, variableKey, context) ``` Type specific methods do not coerce values. `GetVariableInteger()` returns `nil` for the string `"1"`, and `GetVariableBoolean()` returns `nil` for the string `"true"`. For typed arrays/objects, use `Into` methods with pointer outputs: ```go var items []string _ = f.GetVariableArrayInto(featureKey, variableKey, context, &items) var cfg MyConfig _ = f.GetVariableObjectInto(featureKey, variableKey, context, &cfg) ``` `context` and `OverrideOptions` are optional and can be passed before the output pointer. ## Getting all evaluations You can get evaluations of all features available in the SDK instance: ```go allEvaluations := f.GetAllEvaluations(featurevisor.Context{}) fmt.Printf("%+v\n", allEvaluations) // { // myFeature: { // enabled: true, // variation: "control", // variables: { // myVariableKey: "myVariableValue", // }, // }, // // anotherFeature: { // enabled: true, // variation: "treatment", // } // } ``` This is handy especially when you want to pass all evaluations from a backend application to the frontend. ## Sticky For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/): Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; create a child with `SpawnOptions{Sticky: ...}` when a child needs its own sticky state. ### Initialize with sticky ```go import ( "github.com/featurevisor/featurevisor-go" ) f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{ Sticky: &featurevisor.StickyFeatures{ "myFeatureKey": { Enabled: true, // optional Variation: func() *featurevisor.VariationValue { v := featurevisor.VariationValue("treatment") return &v }(), Variables: map[string]interface{}{ "myVariableKey": "myVariableValue", }, }, "anotherFeatureKey": { Enabled: false, }, }, }) ``` Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process. ### Set sticky afterwards You can also set sticky features after the SDK is initialized: ```go f.SetSticky(featurevisor.StickyFeatures{ "myFeatureKey": { Enabled: true, Variation: func() *featurevisor.VariationValue { v := featurevisor.VariationValue("treatment") return &v }(), Variables: map[string]interface{}{ "myVariableKey": "myVariableValue", }, }, "anotherFeatureKey": { Enabled: false, }, }, true) // replace existing sticky features (false by default) ``` ## Setting datafile You may also initialize the SDK without passing `datafile`, and set it later on: ```go f.SetDatafile(datafileContent) ``` `SetDatafile` accepts either parsed `featurevisor.DatafileContent` or a raw JSON string. ### Merging by default By default, `SetDatafile(datafile)` merges the incoming datafile with the SDK instance's existing datafile: - incoming `Features` and `Segments` override matching keys - existing `Features` and `Segments` that are missing from the incoming datafile are kept - `Revision`, `SchemaVersion`, and `FeaturevisorVersion` are taken from the incoming datafile This means you can call `SetDatafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. ### Replacing Pass `true` as the second argument to replace the stored datafile entirely: ```go f.SetDatafile(datafileContent, true) // replace existing datafile ``` ### Loading datafiles on demand Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront. This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application: ```go f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{}) func loadDatafile(target string) { url := fmt.Sprintf("https://cdn.yoursite.com/production/featurevisor-%s.json", target) resp, err := http.Get(url) if err != nil { return } defer resp.Body.Close() datafileBytes, err := io.ReadAll(resp.Body) if err != nil { return } f.SetDatafile(string(datafileBytes)) } loadDatafile("products") // later, when the user reaches checkout loadDatafile("checkout") ``` ### Updating datafile You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile-set) event that you can listen and react to accordingly. The triggers for setting the datafile again can be: - periodic updates based on an interval (like every 5 minutes), or - reacting to: - a specific event in your application (like a user action), or - an event served via websocket or server-sent events (SSE) ### Interval-based update Here's an example of using interval-based update: ```go import ( "time" "io" "net/http" "github.com/featurevisor/featurevisor-go" ) func updateDatafile(f *featurevisor.Featurevisor, datafileURL string) { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { resp, err := http.Get(datafileURL) if err != nil { continue } defer resp.Body.Close() datafileBytes, err := io.ReadAll(resp.Body) if err != nil { continue } var datafileContent featurevisor.DatafileContent if err := datafileContent.FromJSON(string(datafileBytes)); err != nil { continue } f.SetDatafile(datafileContent) } } // Start the update goroutine go updateDatafile(f, datafileURL) ``` ## Diagnostics By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix. ### Levels Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`. Set the level during initialization or update it afterwards: ```go logLevel := featurevisor.LogLevelDebug f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{ LogLevel: &logLevel, }) f.SetLogLevel(featurevisor.LogLevelInfo) ``` ### Handler Use `OnDiagnostic` to send structured diagnostics to your observability system: ```go f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{ LogLevel: &logLevel, OnDiagnostic: func(diagnostic featurevisor.FeaturevisorDiagnostic) { fmt.Println(diagnostic.Level, diagnostic.Code, diagnostic.Message) }, }) ``` Modules can also subscribe to diagnostics or report their own from `Setup` via the provided module API. Every diagnostic has `Level`, `Code`, `Message`, and an object-shaped `Details` map. Optional `Module`, `ModuleName`, and `OriginalError` fields describe provenance. Evaluation metadata belongs in `Details`. Diagnostic handlers are isolated from SDK behavior. A panic in a handler does not stop other handlers or evaluations. ## Events Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime. You can listen to these events that can occur at various stages in your application: ### `datafile_set` ```go unsubscribe := f.On(featurevisor.EventNameDatafileSet, func(details featurevisor.EventDetails) { revision := details["revision"] // new revision previousRevision := details["previousRevision"] revisionChanged := details["revisionChanged"] // true if revision has changed // list of feature keys that have new updates, // and you should re-evaluate them features := details["features"] // handle here }) // stop listening to the event unsubscribe() ``` The `features` array will contain keys of features that have either been: - added, or - updated, or - removed compared to the previous datafile content that existed in the SDK instance. ### `context_set` ```go unsubscribe := f.On(featurevisor.EventNameContextSet, func(details featurevisor.EventDetails) { replaced := details["replaced"] // true if context was replaced context := details["context"] // the new context fmt.Println("Context set") }) ``` ### `sticky_set` ```go unsubscribe := f.On(featurevisor.EventNameStickySet, func(details featurevisor.EventDetails) { replaced := details["replaced"] // true if sticky features got replaced features := details["features"] // list of all affected feature keys fmt.Println("Sticky features set") }) ``` ### `error` ```go unsubscribe := f.On(featurevisor.EventNameError, func(details featurevisor.EventDetails) { diagnostic := details["diagnostic"] fmt.Println(diagnostic) }) ``` The `error` event is emitted for diagnostics whose level is `error`. ## Evaluation details Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context: ```go // flag evaluation := f.EvaluateFlag(featureKey, context) // variation evaluation := f.EvaluateVariation(featureKey, context) // variable evaluation := f.EvaluateVariable(featureKey, variableKey, context) ``` The returned object will always contain the following properties: - `FeatureKey`: the feature key - `Reason`: the reason how the value was evaluated And optionally these properties depending on whether you are evaluating a feature variation or a variable: - `BucketValue`: the bucket value between 0 and 100,000 - `RuleKey`: the rule key - `Error`: the error object - `Enabled`: if feature itself is enabled or not - `Variation`: the variation object - `VariationValue`: the variation value - `VariableKey`: the variable key - `VariableValue`: the variable value - `VariableSchema`: the variable schema - `VariableOverrideIndex`: index of matched variable override when applicable ## Modules Modules allow you to intercept the evaluation process and customize it further as per your needs. ### Defining a module A module is a simple struct with a recommended unique `Name` and optional functions: If `Setup` panics, the module is not registered. Featurevisor removes subscriptions created during setup, reports `module_setup_error`, and calls `Close` when present. ```go import ( "github.com/featurevisor/featurevisor-go" ) myCustomModule := &featurevisor.FeaturevisorModule{ // recommended for diagnostics and removal Name: "my-custom-module", // rest of the properties below are all optional per module Setup: func(api featurevisor.FeaturevisorModuleApi) { api.ReportDiagnostic(featurevisor.FeaturevisorModuleReportedDiagnostic{ Level: featurevisor.LogLevelInfo, Code: "module_ready", Message: "Module is ready", }) }, // before evaluation Before: func(options featurevisor.EvaluateOptions) featurevisor.EvaluateOptions { // update context before evaluation if options.Context == nil { options.Context = featurevisor.Context{} } options.Context["someAdditionalAttribute"] = "value" return options }, // after evaluation After: func(evaluation featurevisor.Evaluation, options featurevisor.EvaluateOptions) featurevisor.Evaluation { if evaluation.Reason == "error" { // log error return evaluation } return evaluation }, // configure bucket key BucketKey: func(options featurevisor.ConfigureBucketKeyOptions) featurevisor.BucketKey { // return custom bucket key return options.BucketKey }, // configure bucket value (between 0 and 100,000) BucketValue: func(options featurevisor.ConfigureBucketValueOptions) featurevisor.BucketValue { // return custom bucket value return options.BucketValue }, Close: func() { // clean up module resources }, } ``` ### Registering modules You can register modules at the time of SDK initialization: ```go import ( "github.com/featurevisor/featurevisor-go" ) f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{ Modules: []*featurevisor.FeaturevisorModule{ myCustomModule, }, }) ``` Or after initialization: ```go removeModule := f.AddModule(myCustomModule) removeModule() ``` ## Child instance When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications. But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request. That's where child instances come in handy: ```go childF := f.Spawn(featurevisor.Context{ // user or request specific context "userId": "123", }) ``` Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone: ```go isEnabled := childF.IsEnabled("my_feature") variation := childF.GetVariation("my_feature") variableValue := childF.GetVariable("my_feature", "my_variable") ``` Similar to parent SDK, child instances also support several additional methods: - `SetContext` - `SetSticky` - `IsEnabled` - `GetVariation` - `GetVariable` - `GetVariableBoolean` - `GetVariableString` - `GetVariableInteger` - `GetVariableDouble` - `GetVariableArray` - `GetVariableArrayInto` - `GetVariableObject` - `GetVariableObjectInto` - `GetVariableJSON` - `GetAllEvaluations` - `On` - `Close` ## Close Both primary and child instances support a `.Close()` method, that removes forgotten event listeners (via `On` method) and cleans up any potential memory leaks. ```go f.Close() ``` ## CLI usage This package also provides a CLI tool for running your Featurevisor [project](https://featurevisor.com/docs/projects/)'s test specs and benchmarking against this Go SDK: All three commands accept repeatable `--target=` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI. ### Test Learn more about testing [here](https://featurevisor.com/docs/testing/). ```bash go run cmd/main.go test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" ``` Additional options that are available: ```bash go run cmd/main.go test \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --quiet|verbose \ --onlyFailures \ --keyPattern="myFeatureKey" \ --assertionPattern="#1" ``` If you want to validate parity locally against the JavaScript SDK runner, you can use the bundled example project: ```bash cd /Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 npx featurevisor test # from this Go SDK repository root: go run cmd/main.go test \ --projectDirectoryPath="/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1" \ --onlyFailures # or: make test-example-1 ``` ### Benchmark Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking). ```bash go run cmd/main.go benchmark \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --environment="production" \ --feature="myFeatureKey" \ --context='{"country": "nl"}' \ --n=1000 ``` ### Assess distribution Learn more about assessing distribution [here](https://featurevisor.com/docs/cli/#assess-distribution). ```bash go run cmd/main.go assess-distribution \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --environment=production \ --feature=foo \ --variation \ --context='{"country": "nl"}' \ --populateUuid=userId \ --populateUuid=deviceId \ --n=1000 ``` ## GitHub repositories - See SDK repository here: [featurevisor/featurevisor-go](https://github.com/featurevisor/featurevisor-go) - See example application repository here: [featurevisor/featurevisor-example-go](https://github.com/featurevisor/featurevisor-example-go)]]> https://featurevisor.com/docs/sdks/java Java SDK Learn how to use Featurevisor Java SDK src/app/docs/sdks/java/page.md ## Installation In your Java application, update `pom.xml` to add the following: ### Repository For finding GitHub Package (public package): ```xml github GitHub Packages https://maven.pkg.github.com/featurevisor/featurevisor-java ``` ### Dependency Add Featurevisor Java SDK as a dependency with your desired version: ```xml com.featurevisor featurevisor-java 0.1.0 ``` Find latest version here: [https://github.com/featurevisor/featurevisor-java/packages](https://github.com/featurevisor/featurevisor-java/packages) ### Authentication To authenticate with GitHub Packages, in your `~/.m2/settings.xml` file, add the following: ```xml github YOUR_GITHUB_USERNAME YOUR_GITHUB_TOKEN ``` You can generate a new GitHub token with `read:packages` scope here: [https://github.com/settings/tokens](https://github.com/settings/tokens) See example application here: [https://github.com/featurevisor/featurevisor-example-java](https://github.com/featurevisor/featurevisor-example-java) ## Public API The main runtime API is `Featurevisor.createFeaturevisor()`: ```java import com.featurevisor.sdk.FeaturevisorLogLevel; Featurevisor f = Featurevisor.createFeaturevisor( new Featurevisor.FeaturevisorOptions().datafile(datafileContent) ); ``` Most applications only need `Featurevisor.createFeaturevisor`, the `Featurevisor` instance type, and `Featurevisor.FeaturevisorOptions`. Public extension and observability types include `FeaturevisorModule`, `FeaturevisorDiagnostic`, and the datafile model types. ## Initialization The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly: ```java import com.featurevisor.sdk.Featurevisor; // Load datafile content String datafileUrl = "https://cdn.yoursite.com/datafile.json"; String datafileContent = "..." // load your datafile content // Create SDK instance Featurevisor f = Featurevisor.createFeaturevisor( new Featurevisor.FeaturevisorOptions().datafile(datafileContent) ); ``` or by constructing a `Featurevisor.FeaturevisorOptions` object: ```java Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions() .datafile(datafileContent) ); ``` We will learn about several different options in the next sections. ## Evaluation types We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/): - [**Flag**](#check-if-enabled) (`boolean`): whether the feature is enabled or not - [**Variation**](#getting-variation) (`Object`): the variation of the feature (if any) - [**Variables**](#getting-variables): variable values of the feature (if any) These evaluations are run against the provided context. ## Context Contexts are [attribute](https://featurevisor.com/docs/attributes) values that we pass to SDK for evaluating [features](https://featurevisor.com/docs/features) against. Think of the conditions that you define in your [segments](https://featurevisor.com/docs/segments/), which are used in your feature's [rules](https://featurevisor.com/docs/features/#rules). They are plain maps: ```java Map context = new HashMap<>(); context.put("userId", "123"); context.put("country", "nl"); // ...other attributes ``` Context can be passed to SDK instance in various different ways, depending on your needs: ### Setting initial context You can set context at the time of initialization: ```java Map initialContext = new HashMap<>(); initialContext.put("deviceId", "123"); initialContext.put("country", "nl"); Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions() .datafile(datafileContent) .context(initialContext)); ``` This is useful for values that don't change too frequently and available at the time of application startup. ### Setting after initialization You can also set more context after the SDK has been initialized: ```java Map additionalContext = new HashMap<>(); additionalContext.put("userId", "123"); additionalContext.put("country", "nl"); f.setContext(additionalContext); ``` This will merge the new context with the existing one (if already set). ### Replacing existing context If you wish to fully replace the existing context, you can pass `true` in second argument: ```java Map newContext = new HashMap<>(); newContext.put("deviceId", "123"); newContext.put("userId", "234"); newContext.put("country", "nl"); newContext.put("browser", "chrome"); f.setContext(newContext, true); // replace existing context ``` ### Manually passing context You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations: ```java Map context = new HashMap<>(); context.put("userId", "123"); context.put("country", "nl"); boolean isEnabled = f.isEnabled("my_feature", context); String variation = f.getVariation("my_feature", context); String variableValue = f.getVariableString("my_feature", "my_variable", context); ``` When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value. Further details for each evaluation types are described below. ## Check if enabled Once the SDK is initialized, you can check if a feature is enabled or not: ```java String featureKey = "my_feature"; boolean isEnabled = f.isEnabled(featureKey); if (isEnabled) { // do something } ``` You can also pass additional context per evaluation: ```java Map additionalContext = new HashMap<>(); // ...additional context boolean isEnabled = f.isEnabled(featureKey, additionalContext); ``` ## Getting variation If your feature has any [variations](https://featurevisor.com/docs/features/#variations) defined, you can evaluate them as follows: ```java String featureKey = "my_feature"; String variation = f.getVariation(featureKey); if ("treatment".equals(variation)) { // do something for treatment variation } else { // handle default/control variation } ``` Additional context per evaluation can also be passed: ```java String variation = f.getVariation(featureKey, additionalContext); ``` ## Getting variables Your features may also include [variables](https://featurevisor.com/docs/features/#variables), which can be evaluated as follows: ```java String variableKey = "bgColor"; String bgColorValue = f.getVariableString(featureKey, variableKey); ``` Additional context per evaluation can also be passed: ```java String bgColorValue = f.getVariableString(featureKey, variableKey, additionalContext); ``` ### Type specific methods Next to generic `getVariable()` methods, there are also type specific methods available for convenience: ```java f.getVariableBoolean(featureKey, variableKey, context); f.getVariableString(featureKey, variableKey, context); f.getVariableInteger(featureKey, variableKey, context); f.getVariableDouble(featureKey, variableKey, context); f.getVariableArray(featureKey, variableKey, context); f.>getVariableObject(featureKey, variableKey, context); f.getVariableObject(featureKey, variableKey, context); f.>getVariableJSON(featureKey, variableKey, context); f.getVariableJSON(featureKey, variableKey, context); f.getVariableJSONNode(featureKey, variableKey, context); ``` Type specific methods do not coerce values. `getVariableInteger()` returns `null` for the string `"1"`, and boolean getters return `null` for non-boolean values. For strongly typed decoding, additional overloads are available: ```java import com.fasterxml.jackson.core.type.TypeReference; // Array decoding using Class List items = f.getVariableArray(featureKey, variableKey, context, MyItem.class); // Array decoding using TypeReference List> rows = f.getVariableArray( featureKey, variableKey, context, new TypeReference>>() {} ); // Object decoding using Class MyConfig config = f.getVariableObject(featureKey, variableKey, context, MyConfig.class); // Object decoding using TypeReference Map> nested = f.getVariableObject( featureKey, variableKey, context, new TypeReference>>() {} ); ``` Typed overloads are additive and non-breaking. If decoding fails for the requested target type, these methods return `null`. For dynamic JSON values with unknown shape, use `getVariableJSONNode`: ```java import com.fasterxml.jackson.databind.JsonNode; JsonNode node = f.getVariableJSONNode(featureKey, variableKey, context); if (node != null && node.isObject()) { String nested = node.path("key").path("nested").asText(null); } ``` If a variable schema type is `json` and the resolved value is a malformed stringified JSON, JSON parsing fails safely and these methods return `null`: - `getVariable(...)` - `getVariableJSONNode(...)` - `getVariableJSON(...)` ## Getting all evaluations You can get evaluations of all features available in the SDK instance: ```java import com.featurevisor.sdk.EvaluatedFeatures; import com.featurevisor.sdk.EvaluatedFeature; EvaluatedFeatures allEvaluations = f.getAllEvaluations(context); // Access the evaluations map Map evaluations = allEvaluations.getValue(); System.out.println(evaluations); // { // "myFeature": { // "enabled": true, // "variation": "control", // "variables": { // "myVariableKey": "myVariableValue" // } // }, // // "anotherFeature": { // "enabled": true, // "variation": "treatment" // } // } ``` This is handy especially when you want to pass all evaluations from a backend application to the frontend. ## Sticky For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/): Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `new Featurevisor.SpawnOptions().sticky(...)` when a child needs its own sticky state. ### Initialize with sticky ```java Map stickyFeatures = new HashMap<>(); Map myFeatureSticky = new HashMap<>(); myFeatureSticky.put("enabled", true); myFeatureSticky.put("variation", "treatment"); Map myVariables = new HashMap<>(); myVariables.put("myVariableKey", "myVariableValue"); myFeatureSticky.put("variables", myVariables); stickyFeatures.put("myFeatureKey", myFeatureSticky); Map anotherFeatureSticky = new HashMap<>(); anotherFeatureSticky.put("enabled", false); stickyFeatures.put("anotherFeatureKey", anotherFeatureSticky); Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions() .datafile(datafile) .sticky(stickyFeatures)); ``` Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process. ### Set sticky afterwards You can also set sticky features after the SDK is initialized: ```java Map stickyFeatures = new HashMap<>(); // ... build sticky features map f.setSticky(stickyFeatures, true); // replace existing sticky features ``` ## Setting datafile You may also initialize the SDK without passing `datafile`, and set it later on: ```java f.setDatafile(datafileContent); ``` ### Merging by default By default, `setDatafile(datafile)` merges the incoming datafile with the SDK's stored datafile. Incoming top-level metadata is used, and incoming segments/features override existing segments/features with the same keys. This means you can call `setDatafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. This is what makes [loading datafiles on demand](#loading-datafiles-on-demand) possible. ### Replacing To replace the stored datafile entirely, pass `true`: ```java f.setDatafile(datafileContent, true); ``` ### Loading datafiles on demand Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront. This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application: ```java Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions()); void loadDatafile(String target) throws Exception { String url = "https://cdn.yoursite.com/production/featurevisor-" + target + ".json"; String json = fetchJson(url); // use your HTTP client of choice DatafileContent datafile = DatafileContent.fromJson(json); // merges into whatever was loaded before f.setDatafile(datafile); } loadDatafile("products"); // later, when the user reaches checkout loadDatafile("checkout"); ``` ### Updating datafile You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly. The triggers for setting the datafile again can be: - periodic updates based on an interval (like every 5 minutes), or - reacting to: - a specific event in your application (like a user action), or - an event served via websocket or server-sent events (SSE) ### Interval-based update Here's an example of using interval-based update: ```java // Using ScheduledExecutorService for periodic updates ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() -> { // Fetch new datafile content String newDatafileContent = // ... fetch from your CDN DatafileContent newDatafile = DatafileContent.fromJson(newDatafileContent); // Merge into the SDK's existing datafile f.setDatafile(newDatafile); }, 0, 5, TimeUnit.MINUTES); ``` ## Diagnostics By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix. ### Levels Available diagnostic levels are `FATAL`, `ERROR`, `WARN`, `INFO`, and `DEBUG`. Set the level during initialization or update it afterwards: ```java Featurevisor f = Featurevisor.createFeaturevisor( new Featurevisor.FeaturevisorOptions().logLevel(FeaturevisorLogLevel.DEBUG) ); f.setLogLevel(FeaturevisorLogLevel.INFO); ``` ### Handler Use `onDiagnostic` to send structured diagnostics to your observability system: ```java Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions() .logLevel(FeaturevisorLogLevel.INFO) .onDiagnostic(diagnostic -> { System.out.println(diagnostic.getLevel() + ": " + diagnostic.getCode()); })); ``` Every diagnostic has `level`, `code`, `message`, and an object-shaped `details` map. Optional `module`, `moduleName`, and `originalError` fields describe provenance. Evaluation metadata belongs in `details`. Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations. ## Events Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime. You can listen to these events that can occur at various stages in your application: ### `datafile_set` ```java Runnable unsubscribe = f.on("datafile_set", (event) -> { String revision = (String) event.get("revision"); // new revision String previousRevision = (String) event.get("previousRevision"); Boolean revisionChanged = (Boolean) event.get("revisionChanged"); // true if revision has changed // list of feature keys that have new updates, // and you should re-evaluate them @SuppressWarnings("unchecked") List features = (List) event.get("features"); // handle here }); // stop listening to the event unsubscribe.run(); ``` The `features` array will contain keys of features that have either been: - added, or - updated, or - removed compared to the previous datafile content that existed in the SDK instance. ### `context_set` ```java Runnable unsubscribe = f.on("context_set", (event) -> { Boolean replaced = (Boolean) event.get("replaced"); // true if context was replaced @SuppressWarnings("unchecked") Map context = (Map) event.get("context"); // the new context System.out.println("Context set"); }); ``` ### `sticky_set` ```java Runnable unsubscribe = f.on("sticky_set", (event) -> { Boolean replaced = (Boolean) event.get("replaced"); // true if sticky features got replaced @SuppressWarnings("unchecked") List features = (List) event.get("features"); // list of all affected feature keys System.out.println("Sticky features set"); }); ``` ### `error` ```java Emitter.UnsubscribeFunction unsubscribe = f.on(Emitter.EventName.ERROR, (event) -> { FeaturevisorDiagnostic diagnostic = (FeaturevisorDiagnostic) event.get("diagnostic"); System.err.println(diagnostic.getMessage()); }); ``` The `error` event is emitted for diagnostics whose level is `ERROR`. ## Evaluation details Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context: ```java // flag Map evaluation = f.evaluateFlag(featureKey, context); // variation Map evaluation = f.evaluateVariation(featureKey, context); // variable Map evaluation = f.evaluateVariable(featureKey, variableKey, context); ``` The returned object will always contain the following properties: - `featureKey`: the feature key - `reason`: the reason how the value was evaluated And optionally these properties depending on whether you are evaluating a feature variation or a variable: - `bucketValue`: the bucket value between 0 and 100,000 - `ruleKey`: the rule key - `error`: the error object - `enabled`: if feature itself is enabled or not - `variation`: the variation object - `variationValue`: the variation value - `variableKey`: the variable key - `variableValue`: the variable value - `variableSchema`: the variable schema ## Modules Modules allow you to intercept the evaluation process and customize it further as per your needs. ### Defining a module A module is a `FeaturevisorModule` with a unique `name` and optional lifecycle functions: If `setup` throws, the module is not registered. Featurevisor removes subscriptions created during setup, reports `module_setup_error`, and calls `close` when present. ```java FeaturevisorModule myCustomModule = new FeaturevisorModule("my-custom-module") .setup(api -> { System.out.println("Current revision: " + api.getRevision()); }) // before evaluation .before(options -> { Map context = new HashMap<>(options.getContext()); context.put("someAdditionalAttribute", "value"); return options.copy().context(context); }) // configure bucket key .bucketKey(options -> { String bucketKey = options.getBucketKey(); return bucketKey; }) // configure bucket value (between 0 and 100,000) .bucketValue(options -> { int bucketValue = options.getBucketValue(); return bucketValue; }) // after evaluation .after((evaluation, options) -> evaluation) // called by f.close() .close(() -> { // clean up resources }); ``` ### Registering modules You can register modules at the time of SDK initialization: ```java List modules = new ArrayList<>(); modules.add(myCustomModule); Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions() .datafile(datafile) .modules(modules)); ``` Or after initialization: ```java Runnable removeModule = f.addModule(myCustomModule); // removeModule.run(); // or: f.removeModule("my-custom-module"); ``` Modules receive an API during `setup` and can subscribe to diagnostics or report their own: ```java FeaturevisorModule module = new FeaturevisorModule("diagnostic-module") .setup(api -> { Runnable unsubscribe = api.onDiagnostic(diagnostic -> { // observe diagnostics from the SDK and other modules }); api.reportDiagnostic(new FeaturevisorDiagnostic() .level(FeaturevisorLogLevel.WARN) .code("custom_module_warning") .message("Something notable happened")); }); ``` ## Child instance When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications. But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request. That's where child instances come in handy: ```java Map childContext = new HashMap<>(); childContext.put("userId", "123"); ChildInstance childF = f.spawn(childContext); ``` Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone: ```java boolean isEnabled = childF.isEnabled("my_feature"); String variation = childF.getVariation("my_feature"); String variableValue = childF.getVariableString("my_feature", "my_variable"); ``` Similar to parent SDK, child instances also support several additional methods: - `setContext` - `setSticky` - `isEnabled` - `getVariation` - `getVariable` - `getVariableBoolean` - `getVariableString` - `getVariableInteger` - `getVariableDouble` - `getVariableArray` - `getVariableObject` - `getVariableJSON` - `getVariableJSONNode` - `getAllEvaluations` - `on` - `close` ## Close Both primary and child instances support a `.close()` method, that removes forgotten event listeners (via `on` method) and cleans up any potential memory leaks. ```java f.close(); ``` ## CLI usage This package also provides a CLI tool for running your Featurevisor project's test specs and benchmarking against this Java SDK: All three commands accept repeatable `--target=` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI. ### Test Learn more about testing [here](https://featurevisor.com/docs/testing/). ```bash $ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="test --projectDirectoryPath=/absolute/path/to/your/featurevisor/project" ``` Additional options that are available: ```bash $ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="test --projectDirectoryPath=/absolute/path/to/your/featurevisor/project --quiet --onlyFailures --keyPattern=myFeatureKey --assertionPattern=#1 --showDatafile --inflate=1" ``` The test runner builds base datafiles and Target datafiles, then uses a Target datafile when an assertion contains `target`. ### Benchmark Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking). ```bash $ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="benchmark --projectDirectoryPath=/absolute/path/to/your/featurevisor/project --environment=production --feature=myFeatureKey --context='{\"country\": \"nl\"}' --n=1000" ``` ### Assess distribution Learn more about assessing distribution [here](https://featurevisor.com/docs/cli/#assess-distribution). ```bash $ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="assess-distribution --projectDirectoryPath=/absolute/path/to/your/featurevisor/project --environment=production --feature=foo --variation --context='{\"country\": \"nl\"}' --populateUuid=userId --populateUuid=deviceId --n=1000" ``` ## GitHub repositories - See SDK repository here: [featurevisor/featurevisor-java](https://github.com/featurevisor/featurevisor-java) - See example application repository here: [featurevisor/featurevisor-example-java](https://github.com/featurevisor/featurevisor-example-java)]]> https://featurevisor.com/docs/sdks/javascript JavaScript SDK Learn how to use Featurevisor JavaScript SDK src/app/docs/sdks/javascript/page.md res.json()) const f = createFeaturevisor({ datafile: datafileContent, }) ``` ## Evaluation types We can evaluate 3 types of values against a particular feature: - [**Flag**](#check-if-enabled) (`boolean`): whether the feature is enabled or not - [**Variation**](#getting-variation) (`string`): the variation of the feature (if any) - [**Variables**](#getting-variables): variable values of the feature (if any) These evaluations are run against the provided context. ## Context Contexts are [attribute](/docs/attributes) values that we pass to SDK for evaluating [features](/docs/features) against. Think of the conditions that you define in your [segments](/docs/segments/), which are used in your feature's [rules](/docs/features/#rules). They are plain objects: ```js const context = { userId: '123', country: 'nl', // ...other attributes } ``` Context can be passed to SDK instance in various different ways, depending on your needs: ### Setting initial context You can set context at the time of initialization: ```js {% path="your-app/index.js" highlight="4-7" %} import { createFeaturevisor } from '@featurevisor/sdk' const f = createFeaturevisor({ context: { deviceId: '123', country: 'nl', }, }) ``` This is useful for values that don't change too frequently and available at the time of application startup. ### Setting after initialization You can also set more context after the SDK has been initialized: ```js f.setContext({ userId: '234', }) ``` This will merge the new context with the existing one (if already set). ### Replacing existing context If you wish to fully replace the existing context, you can pass `true` in second argument: ```js {% highlight="8" %} f.setContext( { deviceId: '123', userId: '234', country: 'nl', browser: 'chrome', }, true, // replace existing context ) ``` ### Manually passing context You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations: ```js const context = { userId: '123', country: 'nl', } const isEnabled = f.isEnabled('my_feature', context) const variation = f.getVariation('my_feature', context) const variableValue = f.getVariable('my_feature', 'my_variable', context) ``` When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value. Further details for each evaluation types are described below. ## Check if enabled Once the SDK is initialized, you can check if a feature is enabled or not: ```js const featureKey = 'my_feature' const isEnabled = f.isEnabled(featureKey) if (isEnabled) { // do something } ``` You can also pass additional context per evaluation: ```js const isEnabled = f.isEnabled(featureKey, { // ...additional context }) ``` ## Getting variation If your feature has any [variations](/docs/features/#variations) defined, you can evaluate them as follows: ```js const featureKey = 'my_feature' const variation = f.getVariation(featureKey) if (variation === "treatment") { // do something for treatment variation } else { // handle default/control variation } ``` Additional context per evaluation can also be passed: ```js const variation = f.getVariation(featureKey, { // ...additional context }) ``` ## Getting variables Your features may also include [variables](/docs/features/#variables), which can be evaluated as follows: ```js const variableKey = 'bgColor' const bgColorValue = f.getVariable(featureKey, variableKey) ``` Additional context per evaluation can also be passed: ```js const bgColorValue = f.getVariable(featureKey, variableKey, { // ...additional context }) ``` ### Type specific methods Next to generic `getVariable()` methods, there are also type specific methods available for convenience: ```ts f.getVariableBoolean(featureKey, variableKey, context = {}) f.getVariableString(featureKey, variableKey, context = {}) f.getVariableInteger(featureKey, variableKey, context = {}) f.getVariableDouble(featureKey, variableKey, context = {}) f.getVariableArray(featureKey, variableKey, context = {}) f.getVariableObject(featureKey, variableKey, context = {}) f.getVariableJSON(featureKey, variableKey, context = {}) ``` Type specific methods do not coerce values. For example, `getVariableInteger()` returns `null` for the string `"1"`, and `getVariableBoolean()` returns `null` for the string `"true"`. ## Getting all evaluations You can get evaluations of all features available in the SDK instance: ```js const allEvaluations = f.getAllEvaluations(context = {}) console.log(allEvaluations) // { // myFeature: { // enabled: true, // variation: "control", // variables: { // myVariableKey: "myVariableValue", // }, // }, // // anotherFeature: { // enabled: true, // variation: "treatment", // } // } ``` This is handy especially when you want to pass all evaluations from a backend application to the frontend. ## Sticky For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](/docs/building-datafiles/): ### Initialize with sticky ```js import { createFeaturevisor } from '@featurevisor/sdk' const f = createFeaturevisor({ sticky: { myFeatureKey: { enabled: true, // optional variation: 'treatment', variables: { myVariableKey: 'myVariableValue', }, }, anotherFeatureKey: { enabled: false, }, }, }) ``` Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process. ### Set sticky afterwards You can also set sticky features after the SDK is initialized: ```js f.setSticky( { myFeatureKey: { enabled: true, variation: 'treatment', variables: { myVariableKey: 'myVariableValue', }, }, anotherFeatureKey: { enabled: false, }, }, // replace existing sticky features (false by default) true ) ``` ## Setting datafile You may also initialize the SDK without passing `datafile`, and set it later on: ```js f.setDatafile(datafileContent) ``` ### Merging by default By default, `setDatafile` merges the incoming datafile with the SDK instance's existing datafile: - incoming `features` and `segments` override matching keys - existing `features` and `segments` that are missing from the incoming datafile are kept - `revision`, `schemaVersion`, and `featurevisorVersion` are taken from the incoming datafile This means you can call `setDatafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. This is what makes [loading datafiles on demand](#loading-datafiles-on-demand) possible. ### Replacing Pass `true` as the second argument to replace the existing datafile entirely, discarding anything loaded before: ```js f.setDatafile(datafileContent, true) ``` ### Loading datafiles on demand Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront. This pairs well with [targets](/docs/targets/), where each target produces a smaller datafile for a specific part of your application. You can load the datafile for the current part, and load others only when the user reaches them: ```js {% path="your-app/index.js" %} import { createFeaturevisor } from '@featurevisor/sdk' // one shared instance for the whole application const f = createFeaturevisor({}) async function loadDatafile(target) { const url = `https://cdn.yoursite.com/production/featurevisor-${target}.json` const datafile = await fetch(url).then((res) => res.json()) // merges into whatever was loaded before f.setDatafile(datafile) } // load the first part now await loadDatafile('products') // later, when the user navigates to checkout, // load its datafile without losing the products features await loadDatafile('checkout') ``` Each `setDatafile` call emits a [`datafile_set`](#datafile-set) event with the list of affected features, so you can re-evaluate and re-render the relevant parts of your UI. Learn more in [Loading datafiles on demand](/docs/use-cases/on-demand-datafiles/). ### Updating datafile You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile-set) event that you can listen and react to accordingly. The triggers for setting the datafile again can be: - periodic updates based on an interval (like every 5 minutes), or - reacting to: - a specific event in your application (like a user action), or - an event served via websocket or server-sent events (SSE) ### Interval-based update Here's an example of using interval-based update: ```js {% highlight="7" %} const interval = 5 * 60 * 1000 // 5 minutes setTimeout(function () { fetch(datafileUrl) .then((res) => res.json()) .then((datafileContent) => { // replace the previously loaded full datafile f.setDatafile(datafileContent, true) }) }, interval) ``` ## Diagnostics By default, Featurevisor SDKs report diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix. ### Levels These are all the available diagnostic levels: - `fatal` - `error` - `warn` - `info` - `debug` ### Customizing levels If you choose `debug` level to make diagnostics more verbose, you can set it at the time of SDK initialization. Setting `debug` level will report all diagnostics, including `info`, `warn`, and `error` levels. ```js import { createFeaturevisor } from '@featurevisor/sdk' const f = createFeaturevisor({ logLevel: 'debug', }) ``` You can also update the diagnostic level from SDK instance afterwards: ```js f.setLogLevel('debug') ``` ### Handler You can pass your own diagnostic handler if you do not wish to print diagnostics to the console: ```js const f = createFeaturevisor({ logLevel: 'info', onDiagnostic: function (diagnostic) { const { level, code, message, details, originalError, } = diagnostic // send to your observability system }, }) ``` Every diagnostic has `level`, `code`, `message`, and an object-shaped `details` field. Optional `module`, `moduleName`, and `originalError` fields describe module and error provenance. Evaluation-specific values such as `featureKey`, `variableKey`, `reason`, and `evaluation` are nested in `details`. Diagnostic handlers are isolated from SDK behavior. If a handler throws, Featurevisor reports the handler failure to the console and continues running other handlers and evaluations. Further diagnostic levels like `info` and `debug` will help you understand how feature variations and variables are evaluated in the runtime against a given context. ## Events Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime. You can listen to these events that can occur at various stages in your application: ### `datafile_set` ```js const unsubscribe = f.on('datafile_set', function ({ revision, // new revision previousRevision, revisionChanged, // true if revision has changed replaced, // true if datafile replaced previous content instead of merging // list of feature keys that have new updates, // and you should re-evaluate them features, }) { // handle here }) // stop listening to the event unsubscribe() ``` The `features` array will contain keys of features that have either been: - added, or - updated, or - removed compared to the previous datafile content that existed in the SDK instance. ### `context_set` ```js const unsubscribe = f.on("context_set", ({ replaced, // true if context was replaced context, // the new context }) => { console.log('Context set') }) ``` ### `sticky_set` ```js const unsubscribe = f.on("sticky_set", ({ replaced, // true if sticky features got replaced features, // list of all affected feature keys }) => { console.log('Sticky features set') }) ``` ### `error` ```js const unsubscribe = f.on('error', ({ diagnostic }) => { console.error(diagnostic.message, diagnostic) }) ``` ## Evaluation details Besides diagnostics with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context: ```js // flag const evaluation = f.evaluateFlag(featureKey, context = {}) // variation const evaluation = f.evaluateVariation(featureKey, context = {}) // variable const evaluation = f.evaluateVariable(featureKey, variableKey, context = {}) ``` The returned object will always contain the following properties: - `featureKey`: the feature key - `reason`: the reason how the value was evaluated And optionally these properties depending on whether you are evaluating a feature variation or a variable: - `bucketValue`: the bucket value between 0 and 100,000 - `ruleKey`: the rule key - `error`: the error object - `enabled`: if feature itself is enabled or not - `variation`: the variation object - `variationValue`: the variation value - `variableKey`: the variable key - `variableValue`: the variable value - `variableSchema`: the variable schema ## Modules Modules allow you to intercept the evaluation process and customize it further as per your needs. ### Defining a module A module is a simple object with an optional unique `name`, an optional `setup` lifecycle function, and optional evaluation callbacks: ```ts import type { FeaturevisorModule } from "@featurevisor/sdk" const myCustomModule: FeaturevisorModule = { name: 'my-custom-module', setup: function ({ getRevision, onDiagnostic, reportDiagnostic }) { const revision = getRevision() onDiagnostic( function (diagnostic) { // modules can subscribe to diagnostics using their own log level }, { logLevel: 'warn', }, ) reportDiagnostic({ level: 'info', code: 'module_ready', message: `Module ready for revision ${revision}`, }) }, // before evaluation before: function (options) { const { type, // `flag` | `variation` | `variable` featureKey, variableKey, // if type is `variable` context, } = options // update context before evaluation options.context = { ...options.context, someAdditionalAttribute: 'value', } return options }, // after evaluation after: function (evaluation, options) { const { reason // `error` | `feature_not_found` | `variable_not_found` | ... } = evaluation if (reason === "error") { // log error } return evaluation }, // configure bucket key bucketKey: function (options) { const { featureKey, context, bucketBy, bucketKey, // default bucket key } = options // return custom bucket key return bucketKey }, // configure bucket value (between 0 and 100,000) bucketValue: function (options) { const { featureKey, context, bucketKey, bucketValue, // default bucket value } = options // return custom bucket value return bucketValue }, close: function () { // clean up resources when f.close() is called }, } ``` If `setup` throws, the module is not registered. Featurevisor removes diagnostic subscriptions created during setup, reports a `module_setup_error` diagnostic, and calls the module's `close` callback when present. ### Registering modules You can register modules at the time of SDK initialization: ```js import { createFeaturevisor } from '@featurevisor/sdk' const f = createFeaturevisor({ modules: [ myCustomModule ], }) ``` Or after initialization: ```js const removeModule = f.addModule(myCustomModule) // removeModule() f.removeModule('my-custom-module') ``` ## Child instance When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications. But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request. That's where child instances come in handy: ```js const childF = f.spawn({ // user or request specific context userId: '123', }) ``` Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone: ```js const isEnabled = childF.isEnabled('my_feature') const variation = childF.getVariation('my_feature') const variableValue = childF.getVariable('my_feature', 'my_variable') ``` Similar to parent SDK, child instances also support several additional methods: - `setContext` - `setSticky` - `isEnabled` - `getVariation` - `getVariable` - `getVariableBoolean` - `getVariableString` - `getVariableInteger` - `getVariableDouble` - `getVariableArray` - `getVariableObject` - `getVariableJSON` - `getAllEvaluations` - `on` - `close` ## Close Both primary and child instances support a `.close()` method, that removes forgotten event listeners (via `on` method) and cleans up any potential memory leaks. ```js f.close() ```]]> https://featurevisor.com/docs/sdks/nodejs Node.js SDK Learn how to use Featurevisor SDK in Node.js src/app/docs/sdks/nodejs/page.md https://featurevisor.com/docs/sdks/php PHP SDK Learn how to use Featurevisor PHP SDK src/app/docs/sdks/php/page.md ## Installation In your PHP application, install the SDK using [Composer](https://getcomposer.org/): ``` $ composer require featurevisor/featurevisor-php ``` ## Public API The main runtime API is `Featurevisor::createFeaturevisor()`: ```php use Featurevisor\Featurevisor; $f = Featurevisor::createFeaturevisor([ "datafile" => $datafileContent, ]); ``` Most applications only need this factory and the returned `Featurevisor` instance. Public extension and observability APIs include modules, diagnostics, events, and the datafile arrays accepted by the factory. ## Initialization The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly: ```php $datafileContent ]); ``` ## Evaluation types We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/): - [**Flag**](#check-if-enabled) (`boolean`): whether the feature is enabled or not - [**Variation**](#getting-variation) (`string`): the variation of the feature (if any) - [**Variables**](#getting-variables): variable values of the feature (if any) These evaluations are run against the provided context. ## Context Contexts are [attribute](https://featurevisor.com/docs/attributes) values that we pass to SDK for evaluating [features](https://featurevisor.com/docs/features) against. Think of the conditions that you define in your [segments](https://featurevisor.com/docs/segments/), which are used in your feature's [rules](https://featurevisor.com/docs/features/#rules). They are plain objects: ```php $context = [ "userId" => "123", "country" => "nl", // ...other attributes ]; ``` Context can be passed to SDK instance in various different ways, depending on your needs: ### Setting initial context You can set context at the time of initialization: ```php use Featurevisor\Featurevisor; $f = Featurevisor::createFeaturevisor([ "context" => [ "deviceId" => "123", "country" => "nl", ], ]); ``` This is useful for values that don't change too frequently and available at the time of application startup. ### Setting after initialization You can also set more context after the SDK has been initialized: ```php $f->setContext([ "userId" => "123", "country" => "nl", ]); ``` This will merge the new context with the existing one (if already set). ### Replacing existing context If you wish to fully replace the existing context, you can pass `true` in second argument: ```php $f->setContext( [ "deviceId" => "123", "userId" => "234", "country" => "nl", "browser" => "chrome", ], true // replace existing context ); ``` ### Manually passing context You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations: ```php $context = [ "userId" => "123", "country" => "nl", ]; $isEnabled = $f->isEnabled('my_feature', $context); $variation = $f->getVariation('my_feature', $context); $variableValue = $f->getVariable('my_feature', 'my_variable', $context); ``` When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value. Further details for each evaluation types are described below. ## Check if enabled Once the SDK is initialized, you can check if a feature is enabled or not: ```php $featureKey = 'my_feature'; $isEnabled = $f->isEnabled($featureKey); if ($isEnabled) { // do something } ``` You can also pass additional context per evaluation: ```php $isEnabled = $f->isEnabled($featureKey, [ // ...additional context ]); ``` ## Getting variation If your feature has any [variations](https://featurevisor.com/docs/features/#variations) defined, you can evaluate them as follows: ```php $featureKey = 'my_feature'; $variation = $f->getVariation($featureKey); if ($variation === "treatment") { // do something for treatment variation } else { // handle default/control variation } ``` Additional context per evaluation can also be passed: ```php $variation = $f->getVariation($featureKey, [ // ...additional context ]); ``` ## Getting variables Your features may also include [variables](https://featurevisor.com/docs/features/#variables), which can be evaluated as follows: ```php $variableKey = 'bgColor'; $bgColorValue = $f->getVariable($featureKey, $variableKey); ``` Additional context per evaluation can also be passed: ```php $bgColorValue = $f->getVariable($featureKey, $variableKey, [ // ...additional context ]); ``` ### Type specific methods Next to generic `getVariable()` methods, there are also type specific methods available for convenience: ```php $f->getVariableBoolean($featureKey, $variableKey, $context = []); $f->getVariableString($featureKey, $variableKey, $context = []); $f->getVariableInteger($featureKey, $variableKey, $context = []); $f->getVariableDouble($featureKey, $variableKey, $context = []); $f->getVariableArray($featureKey, $variableKey, $context = []); $f->getVariableObject($featureKey, $variableKey, $context = []); $f->getVariableJSON($featureKey, $variableKey, $context = []); ``` Type specific methods do not coerce values. `getVariableInteger()` returns `null` for the string `"1"`, and boolean getters return `null` for non-boolean values. ## Getting all evaluations You can get evaluations of all features available in the SDK instance: ```php $allEvaluations = $f->getAllEvaluations($context = []); print_r($allEvaluations); // [ // myFeature: [ // enabled: true, // variation: "control", // variables: [ // myVariableKey: "myVariableValue", // ], // ], // // anotherFeature: [ // enabled: true, // variation: "treatment", // ] // ] ``` This is handy especially when you want to pass all evaluations from a backend application to the frontend. ## Sticky For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/): Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn($context, ['sticky' => ...])` when a child needs its own sticky state. ### Initialize with sticky ```php use Featurevisor\Featurevisor; $f = Featurevisor::createFeaturevisor([ "sticky" => [ "myFeatureKey" => [ "enabled" => true, // optional "variation" => 'treatment', "variables" => [ "myVariableKey" => 'myVariableValue', ], ], "anotherFeatureKey" => [ "enabled" => false, ], ], ]); ``` Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process. ### Set sticky afterwards You can also set sticky features after the SDK is initialized: ```php $f->setSticky( [ "myFeatureKey" => [ "enabled" => true, "variation" => 'treatment', "variables" => [ "myVariableKey" => 'myVariableValue', ], ], "anotherFeatureKey" => [ "enabled" => false, ], ], // replace existing sticky features (false by default) true ]); ``` ## Setting datafile You may also initialize the SDK without passing `datafile`, and set it later on: ```php $f->setDatafile($datafileContent); ``` ### Merging by default By default, `setDatafile($datafileContent)` merges the incoming datafile with the SDK instance's existing datafile: - incoming `features` and `segments` override matching keys - existing `features` and `segments` that are missing from the incoming datafile are kept - `revision`, `schemaVersion`, and `featurevisorVersion` are taken from the incoming datafile This means you can call `setDatafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. ### Replacing To replace the stored datafile completely, pass `true` as the second argument: ```php $f->setDatafile($datafileContent, true); ``` ### Loading datafiles on demand Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront. This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application: ```php $f = Featurevisor::createFeaturevisor([]); function loadDatafile($f, string $target): void { $url = "https://cdn.yoursite.com/production/featurevisor-$target.json"; $datafile = json_decode(file_get_contents($url), true); // merges into whatever was loaded before $f->setDatafile($datafile); } loadDatafile($f, 'products'); // later, when the user reaches checkout loadDatafile($f, 'checkout'); ``` ### Updating datafile You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly. The triggers for setting the datafile again can be: - periodic updates based on an interval (like every 5 minutes), or - reacting to: - a specific event in your application (like a user action), or - an event served via websocket or server-sent events (SSE) ## Diagnostics By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix. ### Levels Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`. Set the level during initialization or update it afterwards: ```php $f = Featurevisor::createFeaturevisor([ "logLevel" => "debug", ]); $f->setLogLevel("info"); ``` ### Handler Use `onDiagnostic` to send structured diagnostics to your observability system: ```php $f = Featurevisor::createFeaturevisor([ "logLevel" => "info", "onDiagnostic" => function (array $diagnostic) { // send $diagnostic to your observability system }, ]); ``` Every diagnostic has `level`, `code`, `message`, and an object-shaped `details` value. Optional `module`, `moduleName`, and `originalError` fields describe provenance. Evaluation metadata belongs in `details`. Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations. ## Events Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime. You can listen to these events that can occur at various stages in your application: ### `datafile_set` ```php $unsubscribe = $f->on('datafile_set', function ($event) { $revision = $event['revision']; // new revision $previousRevision = $event['previousRevision']; $revisionChanged = $event['revisionChanged']; // true if revision has changed $replaced = $event['replaced']; // true if datafile was replaced instead of merged // list of feature keys that have new updates, // and you should re-evaluate them $features = $event['features']; // handle here }); // stop listening to the event $unsubscribe(); ``` The `features` array will contain keys of features that have either been: - added, or - updated, or - removed compared to the previous datafile content that existed in the SDK instance. ### `context_set` ```php $unsubscribe = $f->on('context_set', function ($event) { $replaced = $event['replaced']; // true if context was replaced $context = $event['context']; // the new context echo "Context set"; }); ``` ### `sticky_set` ```php $unsubscribe = $f->on('sticky_set', function ($event) { $replaced = $event['replaced']; // true if sticky features got replaced $features = $event['features']; // list of all affected feature keys echo "Sticky features set"; }); ``` ### `error` ```php $unsubscribe = $f->on('error', function ($event) { echo $event['diagnostic']['message']; }); ``` The `error` event is emitted for diagnostics whose level is `error`. ## Evaluation details Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context: ```php // flag $evaluation = $f->evaluateFlag($featureKey, $context = []); // variation $evaluation = $f->evaluateVariation($featureKey, $context = []); // variable $evaluation = $f->evaluateVariable($featureKey, $variableKey, $context = []); ``` The returned object will always contain the following properties: - `featureKey`: the feature key - `reason`: the reason how the value was evaluated And optionally these properties depending on whether you are evaluating a feature variation or a variable: - `bucketValue`: the bucket value between 0 and 100,000 - `ruleKey`: the rule key - `error`: the error object - `enabled`: if feature itself is enabled or not - `variation`: the variation object - `variationValue`: the variation value - `variableKey`: the variable key - `variableValue`: the variable value - `variableSchema`: the variable schema ## Modules Modules allow you to intercept the evaluation process, report diagnostics, and customize behavior further as per your needs. ### Defining a module A module is a simple object with a unique required `name` and optional functions: If `setup` throws, the module is not registered. Featurevisor removes subscriptions created during setup, reports `module_setup_error`, and calls `close` when present. ```php $myCustomModule = [ // only required property 'name' => 'my-custom-module', // rest of the properties below are all optional per module // setup receives a module API 'setup' => function ($api) { $revision = $api['getRevision'](); $unsubscribe = $api['onDiagnostic'](function (array $diagnostic) { // observe diagnostics reported by other modules or the SDK }); $api['reportDiagnostic']([ 'level' => 'info', 'code' => 'custom_module_ready', 'message' => 'Custom module is ready', ]); }, // before evaluation 'before' => function ($options) { $type = $options['type']; // `flag` | `variation` | `variable` $featureKey = $options['featureKey']; $variableKey = $options['variableKey']; // if type is `variable` $context = $options['context']; // update context before evaluation $options['context'] = array_merge($options['context'], [ 'someAdditionalAttribute' => 'value', ]); return $options; }, // after evaluation 'after' => function ($evaluation, $options) { $reason = $evaluation['reason']; // `error` | `feature_not_found` | `variable_not_found` | ... if ($reason === "error") { // log error return; } }, // configure bucket key 'bucketKey' => function ($options) { $featureKey = $options['featureKey']; $context = $options['context']; $bucketBy = $options['bucketBy']; $bucketKey = $options['bucketKey']; // default bucket key // return custom bucket key return $bucketKey; }, // configure bucket value (between 0 and 100,000) 'bucketValue' => function ($options) { $featureKey = $options['featureKey']; $context = $options['context']; $bucketKey = $options['bucketKey']; $bucketValue = $options['bucketValue']; // default bucket value // return custom bucket value return $bucketValue; }, // cleanup 'close' => function () { // release module resources }, ]; ``` ### Registering modules You can register modules at the time of SDK initialization: ```php use Featurevisor\Featurevisor; $f = Featurevisor::createFeaturevisor([ 'modules' => [ $myCustomModule ], ]); ``` Or after initialization: ```php $removeModule = $f->addModule($myCustomModule); // $removeModule() // or remove later by name $f->removeModule('my-custom-module'); ``` ## Child instance When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications. But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request. That's where child instances come in handy: ```php $childF = $f->spawn([ // user or request specific context 'userId' => '123', ]); ``` Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone: ```php $isEnabled = $childF->isEnabled('my_feature'); $variation = $childF->getVariation('my_feature'); $variableValue = $childF->getVariable('my_feature', 'my_variable'); ``` Similar to parent SDK, child instances also support several additional methods: - `setContext` - `setSticky` - `isEnabled` - `getVariation` - `getVariable` - `getVariableBoolean` - `getVariableString` - `getVariableInteger` - `getVariableDouble` - `getVariableArray` - `getVariableObject` - `getVariableJSON` - `getAllEvaluations` - `on` - `close` ## Close Both primary and child instances support a `.close()` method. The primary instance also closes registered modules and removes diagnostic subscriptions. ```php $f->close(); ``` ## CLI usage This package also provides a CLI tool for running your Featurevisor project's test specs and benchmarking against this PHP SDK: All three commands accept repeatable `--target=` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI. ### Test Learn more about testing [here](https://featurevisor.com/docs/testing/). ``` $ vendor/bin/featurevisor test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" ``` Additional options that are available: ``` $ vendor/bin/featurevisor test \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --quiet|verbose \ --onlyFailures \ --keyPattern="myFeatureKey" \ --assertionPattern="#1" ``` If assertions include `target`, the runner builds and selects the corresponding Target datafile automatically via `npx featurevisor build --target= --environment= --json`. ### Benchmark Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking). ``` $ vendor/bin/featurevisor benchmark \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --environment="production" \ --feature="myFeatureKey" \ --context='{"country": "nl"}' \ --n=1000 ``` ### Assess distribution Learn more about assessing distribution [here](https://featurevisor.com/docs/cli/#assess-distribution). ``` $ vendor/bin/featurevisor assess-distribution \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --environment=production \ --feature=foo \ --variation \ --context='{"country": "nl"}' \ --populateUuid=userId \ --populateUuid=deviceId \ --n=1000 ``` ## GitHub repositories - See SDK repository here: [featurevisor/featurevisor-php](https://github.com/featurevisor/featurevisor-php) - See example application repository here: [featurevisor/featurevisor-php-example](https://github.com/featurevisor/featurevisor-php-example)]]> https://featurevisor.com/docs/sdks/python Python SDK Learn how to use Featurevisor Python SDK src/app/docs/sdks/python/page.md ## Installation ```bash pip install featurevisor ``` ## Public API The main runtime API is `create_featurevisor()`: ```python from featurevisor import Featurevisor, create_featurevisor f: Featurevisor = create_featurevisor({ "datafile": datafile_content, }) ``` Most applications only need `create_featurevisor` and the `Featurevisor` instance type. Public extension and observability APIs include `FeaturevisorModule`, diagnostics, events, and the datafile dictionaries accepted by the factory. ## Initialization Initialize the SDK with Featurevisor datafile content: ```python from urllib.request import urlopen import json from featurevisor import create_featurevisor datafile_url = "https://cdn.yoursite.com/datafile.json" with urlopen(datafile_url) as response: datafile_content = json.load(response) f = create_featurevisor({ "datafile": datafile_content, }) ``` ## Evaluation types We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/): - [**Flag**](#check-if-enabled) (`bool`): whether the feature is enabled or not - [**Variation**](#getting-variation) (`string`): the variation of the feature (if any) - [**Variables**](#getting-variables): variable values of the feature (if any) ## Context Context is a plain dictionary of attribute values used during evaluation: ```python context = { "userId": "123", "country": "nl", } ``` ### Setting initial context You can provide context at initialization: ```python f = create_featurevisor({ "context": { "deviceId": "123", "country": "nl", }, }) ``` ### Setting after initialization You can merge more context later: ```python f.set_context({ "userId": "234", }) ``` ### Replacing existing context Or replace the existing context: ```python f.set_context( { "deviceId": "123", "userId": "234", "country": "nl", "browser": "chrome", }, True, ) ``` ### Manually passing context You can also pass additional per-evaluation context: ```python is_enabled = f.is_enabled("my_feature", {"country": "nl"}) variation = f.get_variation("my_feature", {"country": "nl"}) variable_value = f.get_variable("my_feature", "my_variable", {"country": "nl"}) ``` ## Check if enabled ```python if f.is_enabled("my_feature"): pass ``` ## Getting variation ```python variation = f.get_variation("my_feature") if variation == "treatment": pass ``` ## Getting variables ```python bg_color = f.get_variable("my_feature", "bgColor") ``` ### Type specific methods Typed convenience methods are also available: ```python f.get_variable_boolean(feature_key, variable_key, context={}) f.get_variable_string(feature_key, variable_key, context={}) f.get_variable_integer(feature_key, variable_key, context={}) f.get_variable_double(feature_key, variable_key, context={}) f.get_variable_array(feature_key, variable_key, context={}) f.get_variable_object(feature_key, variable_key, context={}) f.get_variable_json(feature_key, variable_key, context={}) ``` Type specific methods do not coerce values. `get_variable_integer()` returns `None` for the string `"1"`, and boolean getters return `None` for non-boolean values. ## Getting all evaluations ```python all_evaluations = f.get_all_evaluations() ``` ## Sticky ### Initialize with sticky You can pin feature evaluations with sticky values: Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn(context, {"sticky": ...})` when a child needs its own sticky state. ```python f = create_featurevisor({ "sticky": { "myFeatureKey": { "enabled": True, "variation": "treatment", "variables": { "myVariableKey": "myVariableValue", }, } } }) ``` ### Set sticky afterwards Or update them later: ```python f.set_sticky({ "myFeatureKey": { "enabled": False, } }) ``` ## Setting datafile You may initialize the SDK without passing `datafile`, and set it later on. The SDK accepts either parsed JSON content or a JSON string: ```python f.set_datafile(datafile_content) f.set_datafile(json.dumps(datafile_content)) ``` ### Merging by default By default, `set_datafile(datafile)` merges incoming content into the SDK's current datafile: - top-level metadata such as `schemaVersion`, `revision`, and `featurevisorVersion` comes from the incoming datafile - `segments` are merged, with incoming entries overriding existing ones - `features` are merged, with incoming entries overriding existing ones This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. This is what makes [loading datafiles on demand](#loading-datafiles-on-demand) possible. ### Replacing To fully replace the stored datafile, pass `True` as the second argument: ```python f.set_datafile(datafile_content, True) ``` ### Loading datafiles on demand Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront. This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application. You can load the datafile for the current part, and load others only when the user reaches them: ```python from urllib.request import urlopen import json from featurevisor import create_featurevisor f = create_featurevisor({}) def load_datafile(target): url = f"https://cdn.yoursite.com/production/featurevisor-{target}.json" with urlopen(url) as response: datafile = json.load(response) # merges into whatever was loaded before f.set_datafile(datafile) load_datafile("products") # later, when the user reaches checkout load_datafile("checkout") ``` ### Updating datafile You can set the datafile as many times as you want in your application, which will emit a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly. ### Interval-based update Here's a minimal interval-style example: ```python import json import threading from urllib.request import urlopen def update_datafile(): with urlopen(datafile_url) as response: f.set_datafile(json.load(response)) threading.Timer(5 * 60, update_datafile).start() update_datafile() ``` ## Diagnostics By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix. ### Levels Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`. Set the level during initialization or update it afterwards: ```python f = create_featurevisor({"logLevel": "debug"}) f.set_log_level("info") ``` ### Handler Use `onDiagnostic` to send structured diagnostics to your observability system: ```python f = create_featurevisor({ "logLevel": "info", "onDiagnostic": lambda diagnostic: print( diagnostic["level"], diagnostic["code"], diagnostic["message"], ), }) ``` Every diagnostic has `level`, `code`, `message`, and an object-shaped `details` dictionary. Optional `module`, `moduleName`, and `originalError` fields describe provenance. Evaluation metadata belongs in `details`. Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations. ## Events Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime. ### `datafile_set` ```python def handle_datafile_set(event): revision = event["revision"] previous_revision = event["previousRevision"] revision_changed = event["revisionChanged"] features = event["features"] replaced = event["replaced"] unsubscribe = f.on("datafile_set", handle_datafile_set) unsubscribe() ``` The `features` list will contain keys of features that have either been added, updated, or removed compared to the previous datafile content. ### `context_set` ```python unsubscribe = f.on("context_set", lambda event: print(event["context"])) unsubscribe() ``` ### `sticky_set` ```python unsubscribe = f.on("sticky_set", lambda event: print(event["features"])) unsubscribe() ``` ### `error` ```python unsubscribe = f.on("error", lambda event: print(event["diagnostic"]["message"])) unsubscribe() ``` The `error` event is emitted for diagnostics reported with `level` set to `error`. ## Evaluation details Besides logging with debug level enabled, you can also get more details about how feature variations and variables are evaluated at runtime against a given context: ```python # flag evaluation = f.evaluate_flag(feature_key, context={}) # variation evaluation = f.evaluate_variation(feature_key, context={}) # variable evaluation = f.evaluate_variable(feature_key, variable_key, context={}) ``` The returned object will always contain the following properties: - `featureKey`: the feature key - `reason`: the reason how the value was evaluated And optionally these properties depending on whether you are evaluating a feature variation or a variable: - `bucketValue`: the bucket value between 0 and 100,000 - `ruleKey`: the rule key - `error`: the error object - `enabled`: if feature itself is enabled or not - `variation`: the variation object - `variationValue`: the variation value - `variableKey`: the variable key - `variableValue`: the variable value - `variableSchema`: the variable schema - `variableOverrideIndex`: index of matched variable override when applicable ## Modules Modules can intercept evaluation and participate in SDK lifecycle: - `setup` - `before` - `bucketKey` - `bucketValue` - `after` - `close` ### Defining a module ```python my_module = { "name": "my-module", "setup": lambda api: api["onDiagnostic"](lambda diagnostic: print(diagnostic)), "before": lambda options: {**options, "context": {**options["context"], "country": "nl"}}, "bucketKey": lambda options: options["bucketKey"], "bucketValue": lambda options: options["bucketValue"], "after": lambda evaluation, options: evaluation, "close": lambda: None, } ``` The module API passed to `setup` exposes `getRevision`, `onDiagnostic`, and `reportDiagnostic`. If `setup` raises an exception, the module is not registered. Featurevisor removes subscriptions created during setup, reports `module_setup_error`, and calls `close` when present. ### Registering modules Modules can be registered at initialization or afterwards: ```python f = create_featurevisor({ "modules": [my_module], }) remove_module = f.add_module(my_module) remove_module() f.remove_module("my-module") ``` ## Child instance ```python child = f.spawn({"country": "de"}) child.is_enabled("my_feature") ``` ## Close ```python f.close() ``` ## CLI usage The Python package also exposes a CLI: ```bash python -m featurevisor test python -m featurevisor benchmark python -m featurevisor assess-distribution ``` These commands are intended for use from inside a Featurevisor project and rely on `npx featurevisor` being available locally. All three commands accept repeatable `--target=` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI. ### Test Run Featurevisor test specs using the Python SDK: ```bash python -m featurevisor test \ --projectDirectoryPath=/path/to/featurevisor-project ``` Useful options: ```bash python -m featurevisor test --keyPattern=foo python -m featurevisor test --assertionPattern=variation python -m featurevisor test --onlyFailures python -m featurevisor test --showDatafile python -m featurevisor test --verbose ``` The Python test runner builds base datafiles and Target datafiles with `npx featurevisor build --json`. Assertions containing `target` are evaluated against the matching Target datafile. ### Benchmark Benchmark repeated Python SDK evaluations against a built datafile: ```bash python -m featurevisor benchmark \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --context='{"userId":"123"}' \ --n=1000 ``` For variation benchmarks: ```bash python -m featurevisor benchmark \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --variation \ --context='{"userId":"123"}' ``` For variable benchmarks: ```bash python -m featurevisor benchmark \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --variable=my_variable_key \ --context='{"userId":"123"}' ``` ### Assess distribution Inspect enabled/disabled and variation distribution over repeated evaluations: ```bash python -m featurevisor assess-distribution \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --context='{"country":"nl"}' \ --n=1000 ``` You can also populate UUID-based context keys per iteration: ```bash python -m featurevisor assess-distribution \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --populateUuid=userId \ --populateUuid=deviceId ``` ## GitHub repositories - See SDK repository here: [featurevisor/featurevisor-python](https://github.com/featurevisor/featurevisor-python) - See example application repository here: [featurevisor/featurevisor-example-python](https://github.com/featurevisor/featurevisor-example-python)]]> https://featurevisor.com/docs/sdks/ruby Ruby SDK Learn how to use Featurevisor Ruby SDK src/app/docs/sdks/ruby/page.md ## Installation Add this line to your application's Gemfile: ```ruby gem 'featurevisor' ``` And then execute: ```bash $ bundle install ``` Or install it yourself as: ```bash $ gem install featurevisor ``` ## Public API The main runtime API is `Featurevisor.create_featurevisor`: ```ruby f = Featurevisor.create_featurevisor( datafile: datafile_content ) ``` Most applications only need this factory and the returned `Featurevisor::Instance`. Public extension and observability APIs include modules, diagnostics, events, and the datafile structures accepted by the factory. ## Initialization The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly: ```ruby require 'featurevisor' require 'net/http' require 'json' # Fetch datafile from URL datafile_url = 'https://cdn.yoursite.com/datafile.json' response = Net::HTTP.get_response(URI(datafile_url)) # Parse JSON with symbolized keys (required) datafile_content = JSON.parse(response.body, symbolize_names: true) # Create SDK instance f = Featurevisor.create_featurevisor( datafile: datafile_content ) ``` **Important**: When parsing JSON datafiles, you must use `symbolize_names: true` to ensure proper key handling by the SDK. Alternatively, you can pass a JSON string directly and the SDK will parse it automatically: ```ruby # Option 1: Parse JSON yourself (recommended) datafile_content = JSON.parse(json_string, symbolize_names: true) f = Featurevisor.create_featurevisor(datafile: datafile_content) # Option 2: Pass JSON string directly (automatic parsing) f = Featurevisor.create_featurevisor(datafile: json_string) ``` ## Evaluation types We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/): - [**Flag**](#check-if-enabled) (`boolean`): whether the feature is enabled or not - [**Variation**](#getting-variation) (`string`): the variation of the feature (if any) - [**Variables**](#getting-variables): variable values of the feature (if any) These evaluations are run against the provided context. ## Context Contexts are [attribute](https://featurevisor.com/docs/attributes/) values that we pass to SDK for evaluating [features](https://featurevisor.com/docs/features/) against. Think of the conditions that you define in your [segments](https://featurevisor.com/docs/segments/), which are used in your feature's [rules](https://featurevisor.com/docs/features/#rules). They are plain hashes: ```ruby context = { userId: '123', country: 'nl', # ...other attributes } ``` Context can be passed to SDK instance in various different ways, depending on your needs: ### Setting initial context You can set context at the time of initialization: ```ruby require 'featurevisor' f = Featurevisor.create_featurevisor( context: { deviceId: '123', country: 'nl' } ) ``` This is useful for values that don't change too frequently and available at the time of application startup. ### Setting after initialization You can also set more context after the SDK has been initialized: ```ruby f.set_context({ userId: '234' }) ``` This will merge the new context with the existing one (if already set). ### Replacing existing context If you wish to fully replace the existing context, you can pass `true` in second argument: ```ruby f.set_context({ deviceId: '123', userId: '234', country: 'nl', browser: 'chrome' }, true) # replace existing context ``` ### Manually passing context You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations: ```ruby context = { userId: '123', country: 'nl' } is_enabled = f.is_enabled('my_feature', context) variation = f.get_variation('my_feature', context) variable_value = f.get_variable('my_feature', 'my_variable', context) ``` When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value. Further details for each evaluation types are described below. ## Check if enabled Once the SDK is initialized, you can check if a feature is enabled or not: ```ruby feature_key = 'my_feature' is_enabled = f.is_enabled(feature_key) if is_enabled # do something end ``` You can also pass additional context per evaluation: ```ruby is_enabled = f.is_enabled(feature_key, { # ...additional context }) ``` ## Getting variation If your feature has any [variations](https://featurevisor.com/docs/features/#variations) defined, you can evaluate them as follows: ```ruby feature_key = 'my_feature' variation = f.get_variation(feature_key) if variation == 'treatment' # do something for treatment variation else # handle default/control variation end ``` Additional context per evaluation can also be passed: ```ruby variation = f.get_variation(feature_key, { # ...additional context }) ``` ## Getting variables Your features may also include [variables](https://featurevisor.com/docs/features/#variables), which can be evaluated as follows: ```ruby variable_key = 'bgColor' bg_color_value = f.get_variable('my_feature', variable_key) ``` Additional context per evaluation can also be passed: ```ruby bg_color_value = f.get_variable('my_feature', variable_key, { # ...additional context }) ``` ### Type specific methods Next to generic `get_variable()` methods, there are also type specific methods available for convenience: ```ruby f.get_variable_boolean(feature_key, variable_key, context = {}) f.get_variable_string(feature_key, variable_key, context = {}) f.get_variable_integer(feature_key, variable_key, context = {}) f.get_variable_double(feature_key, variable_key, context = {}) f.get_variable_array(feature_key, variable_key, context = {}) f.get_variable_object(feature_key, variable_key, context = {}) f.get_variable_json(feature_key, variable_key, context = {}) ``` Type specific methods do not coerce values. `get_variable_integer` returns `nil` for the string `"1"`, and boolean getters return `nil` for non-boolean values. ## Getting all evaluations You can get evaluations of all features available in the SDK instance: ```ruby all_evaluations = f.get_all_evaluations({}) puts all_evaluations # { # myFeature: { # enabled: true, # variation: "control", # variables: { # myVariableKey: "myVariableValue", # }, # }, # # anotherFeature: { # enabled: true, # variation: "treatment", # } # } ``` This is handy especially when you want to pass all evaluations from a backend application to the frontend. ## Sticky For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/): Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn(context, sticky: ...)` when a child needs its own sticky state. ### Initialize with sticky ```ruby require 'featurevisor' f = Featurevisor.create_featurevisor( sticky: { myFeatureKey: { enabled: true, # optional variation: 'treatment', variables: { myVariableKey: 'myVariableValue' } }, anotherFeatureKey: { enabled: false } } ) ``` Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process. ### Set sticky afterwards You can also set sticky features after the SDK is initialized: ```ruby f.set_sticky({ myFeatureKey: { enabled: true, variation: 'treatment', variables: { myVariableKey: 'myVariableValue' } }, anotherFeatureKey: { enabled: false } }, true) # replace existing sticky features (false by default) ``` ## Setting datafile You may also initialize the SDK without passing `datafile`, and set it later on: ```ruby # Parse with symbolized keys before setting datafile_content = JSON.parse(json_string, symbolize_names: true) f.set_datafile(datafile_content) # Or pass JSON string directly for automatic parsing f.set_datafile(json_string) ``` **Important**: When calling `set_datafile()`, ensure JSON is parsed with `symbolize_names: true` if you're parsing it yourself. ### Merging by default By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's current datafile: - top-level metadata such as `schemaVersion`, `revision`, and `featurevisorVersion` comes from the incoming datafile - `segments` are merged, with incoming entries overriding existing ones - `features` are merged, with incoming entries overriding existing ones This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. ### Replacing To fully replace the stored datafile, pass `true` as the second argument: ```ruby f.set_datafile(datafile_content, true) ``` ### Loading datafiles on demand Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront. This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application: ```ruby require "open-uri" f = Featurevisor.create_featurevisor({}) def load_datafile(f, target) url = "https://cdn.yoursite.com/production/featurevisor-#{target}.json" datafile = JSON.parse(URI.open(url).read, symbolize_names: true) # merges into whatever was loaded before f.set_datafile(datafile) end load_datafile(f, "products") # later, when the user reaches checkout load_datafile(f, "checkout") ``` ### Updating datafile You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly. The triggers for setting the datafile again can be: - periodic updates based on an interval (like every 5 minutes), or - reacting to: - a specific event in your application (like a user action), or - an event served via websocket or server-sent events (SSE) ### Interval-based update Here's an example of using interval-based update: ```ruby require 'net/http' require 'json' def update_datafile(f, datafile_url) loop do sleep(5 * 60) # 5 minutes begin response = Net::HTTP.get_response(URI(datafile_url)) datafile_content = JSON.parse(response.body) f.set_datafile(datafile_content) rescue => e # handle error puts "Failed to update datafile: #{e.message}" end end end # Start the update thread Thread.new { update_datafile(f, datafile_url) } ``` ## Diagnostics By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix. ### Levels Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`. Set the level during initialization or update it afterwards: ```ruby f = Featurevisor.create_featurevisor(log_level: "debug") f.set_log_level("info") ``` ### Handler Use `on_diagnostic` to send structured diagnostics to your observability system: ```ruby f = Featurevisor.create_featurevisor( log_level: "info", on_diagnostic: ->(diagnostic) { puts "#{diagnostic[:level]} #{diagnostic[:code]} #{diagnostic[:message]}" } ) ``` Every diagnostic has `:level`, `:code`, `:message`, and an object-shaped `:details` hash. Optional `:module`, `:moduleName`, and `:originalError` fields describe provenance. Evaluation metadata belongs in `:details`. Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations. ## Events Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime. You can listen to these events that can occur at various stages in your application: ### `datafile_set` ```ruby unsubscribe = f.on('datafile_set') do |event| revision = event[:revision] # new revision previous_revision = event[:previousRevision] revision_changed = event[:revisionChanged] # true if revision has changed # list of feature keys that have new updates, # and you should re-evaluate them features = event[:features] # handle here end # stop listening to the event unsubscribe.call ``` The `features` array will contain keys of features that have either been: - added, or - updated, or - removed compared to the previous datafile content that existed in the SDK instance. The event also includes `replaced`, which is `true` when the datafile replaced the previous content instead of merging into it. ### `context_set` ```ruby unsubscribe = f.on('context_set') do |event| replaced = event[:replaced] # true if context was replaced context = event[:context] # the new context puts 'Context set' end ``` ### `sticky_set` ```ruby unsubscribe = f.on('sticky_set') do |event| replaced = event[:replaced] # true if sticky features got replaced features = event[:features] # list of all affected feature keys puts 'Sticky features set' end ``` ### `error` ```ruby unsubscribe = f.on('error') do |event| diagnostic = event[:diagnostic] code = diagnostic[:code] message = diagnostic[:message] puts "Featurevisor error: #{code} #{message}" end ``` The `error` event is emitted for diagnostics reported with `level: "error"`. ## Evaluation details Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context: ```ruby # flag evaluation = f.evaluate_flag(feature_key, context = {}) # variation evaluation = f.evaluate_variation(feature_key, context = {}) # variable evaluation = f.evaluate_variable(feature_key, variable_key, context = {}) ``` The returned object will always contain the following properties: - `feature_key`: the feature key - `reason`: the reason how the value was evaluated And optionally these properties depending on whether you are evaluating a feature variation or a variable: - `bucket_value`: the bucket value between 0 and 100,000 - `rule_key`: the rule key - `error`: the error object - `enabled`: if feature itself is enabled or not - `variation`: the variation object - `variation_value`: the variation value - `variable_key`: the variable key - `variable_value`: the variable value - `variable_schema`: the variable schema ## Modules Modules allow you to intercept the evaluation process and customize SDK behavior. ### Defining a module A module is a simple hash with a unique recommended `name` and optional lifecycle functions: If `setup` raises an exception, the module is not registered. Featurevisor removes subscriptions created during setup, reports `module_setup_error`, and calls `close` when present. ```ruby require 'featurevisor' my_custom_module = { # recommended, and used for duplicate detection/removal name: 'my-custom-module', # rest of the properties below are all optional per module # setup once, when the module is registered setup: ->(api) { revision = api[:get_revision].call api[:on_diagnostic].call(->(diagnostic) { puts diagnostic[:message] }) }, # before evaluation before: ->(options) { # update context before evaluation options[:context] = options[:context].merge({ someAdditionalAttribute: 'value' }) options }, # after evaluation after: ->(evaluation, options) { reason = evaluation[:reason] if reason == 'error' # log error return end }, # configure bucket key bucket_key: ->(options) { # return custom bucket key options[:bucket_key] }, # configure bucket value (between 0 and 100,000) bucket_value: ->(options) { # return custom bucket value options[:bucket_value] }, # cleanup when module is removed or SDK is closed close: -> { # cleanup here } } ``` The module API passed to `setup` exposes: - `get_revision` - `on_diagnostic` - `report_diagnostic` ### Registering modules You can register modules at the time of SDK initialization: ```ruby require 'featurevisor' f = Featurevisor.create_featurevisor( modules: [my_custom_module] ) ``` Or after initialization: ```ruby remove_module = f.add_module(my_custom_module) # remove later by calling the returned function remove_module.call # or remove by name f.remove_module('my-custom-module') ``` ## Child instance When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications. But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request. That's where child instances come in handy: ```ruby child_f = f.spawn({ # user or request specific context userId: '123' }) ``` Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone: ```ruby is_enabled = child_f.is_enabled('my_feature') variation = child_f.get_variation('my_feature') variable_value = child_f.get_variable('my_feature', 'my_variable') ``` Similar to parent SDK, child instances also support several additional methods: - `set_context` - `set_sticky` - `is_enabled` - `get_variation` - `get_variable` - `get_variable_boolean` - `get_variable_string` - `get_variable_integer` - `get_variable_double` - `get_variable_array` - `get_variable_object` - `get_variable_json` - `get_all_evaluations` - `on` - `close` ## Close Both primary and child instances support a `.close()` method, that removes forgotten event listeners (via `on` method) and cleans up any potential memory leaks. ```ruby f.close() ``` ## CLI usage This package also provides a CLI tool for running your Featurevisor [project](https://featurevisor.com/docs/projects/)'s test specs and benchmarking against this Ruby SDK. - Global installation: you can access it as `featurevisor` - Local installation: you can access it as `bundle exec featurevisor` - From this repository: you can access it as `bin/featurevisor` ### Test Learn more about testing [here](https://featurevisor.com/docs/testing/). ```bash $ bundle exec featurevisor test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" ``` Additional options that are available: ```bash $ bundle exec featurevisor test \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --quiet|--verbose \ --onlyFailures \ --keyPattern="myFeatureKey" \ --assertionPattern="#1" ``` The Ruby test runner builds base datafiles and Target datafiles in memory via `npx featurevisor build --json`. When an assertion contains `target`, it is evaluated against the matching Target datafile. All three commands accept repeatable `--target=` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI. ### Test against local monorepo's example-1 ```bash $ cd /absolute/path/to/featurevisor-ruby $ bundle exec ruby bin/featurevisor test --projectDirectoryPath=/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 --onlyFailures $ make test-example-1 ``` ### Benchmark Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking). ```bash $ bundle exec featurevisor benchmark \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --environment="production" \ --feature="myFeatureKey" \ --context='{"country": "nl"}' \ --n=1000 ``` ### Assess distribution Learn more about assessing distribution [here](https://featurevisor.com/docs/cli/#assess-distribution). ```bash $ bundle exec featurevisor assess-distribution \ --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \ --environment=production \ --feature=foo \ --variation \ --context='{"country": "nl"}' \ --populateUuid=userId \ --populateUuid=deviceId \ --n=1000 ``` ## GitHub repositories - See SDK repository here: [featurevisor/featurevisor-ruby](https://github.com/featurevisor/featurevisor-ruby) - See example application repository here: [featurevisor/featurevisor-example-ruby](https://github.com/featurevisor/featurevisor-example-ruby)]]> https://featurevisor.com/docs/sdks/swift Swift SDK Learn how to use Featurevisor Swift SDK src/app/docs/sdks/swift/page.md ## Installation In your Swift application, add this package using Swift Package Manager: ```swift .package(url: "https://github.com/featurevisor/featurevisor-swift2.git", from: "0.1.0") ``` Then add the product dependency: ```swift .product(name: "Featurevisor", package: "featurevisor-swift2") ``` ## Public API The main runtime API is `createFeaturevisor()`: ```swift let f: Featurevisor = createFeaturevisor( FeaturevisorOptions(datafile: datafileContent) ) ``` Most applications only need `createFeaturevisor`, `Featurevisor`, and `FeaturevisorOptions`. Public extension and observability types include `FeaturevisorModule`, `FeaturevisorDiagnostic`, and the datafile model types. ## Initialization The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly: ```swift import Foundation import Featurevisor let datafileURL = URL(string: "https://cdn.yoursite.com/datafile.json")! let data = try Data(contentsOf: datafileURL) let datafileContent = try DatafileContent.fromData(data) let f = createFeaturevisor( FeaturevisorOptions( datafile: datafileContent ) ) ``` ## Evaluation types We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/): - [**Flag**](#check-if-enabled) (`Bool`): whether the feature is enabled or not - [**Variation**](#getting-variation) (`String`): the variation of the feature (if any) - [**Variables**](#getting-variables): variable values of the feature (if any) These evaluations are run against the provided context. ## Context Contexts are [attribute](https://featurevisor.com/docs/attributes) values that we pass to SDK for evaluating [features](https://featurevisor.com/docs/features) against. Think of the conditions that you define in your [segments](https://featurevisor.com/docs/segments/), which are used in your feature's [rules](https://featurevisor.com/docs/features/#rules). They are plain dictionaries: ```swift let context: Context = [ "userId": .string("123"), "country": .string("nl"), ] ``` ### Setting initial context You can set context at the time of initialization: ```swift let f = createFeaturevisor( FeaturevisorOptions( context: [ "deviceId": .string("123"), "country": .string("nl"), ] ) ) ``` ### Setting after initialization You can also set more context after the SDK has been initialized: ```swift f.setContext([ "userId": .string("234"), ]) ``` This will merge the new context with the existing one (if already set). ### Replacing existing context If you wish to fully replace the existing context, you can pass `true` in second argument: ```swift f.setContext( [ "deviceId": .string("123"), "userId": .string("234"), "country": .string("nl"), "browser": .string("chrome"), ], replace: true ) ``` ### Manually passing context You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations: ```swift let context: Context = [ "userId": .string("123"), "country": .string("nl"), ] let isEnabled = f.isEnabled("my_feature", context) let variation = f.getVariation("my_feature", context) let variableValue = f.getVariable("my_feature", "my_variable", context) ``` When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value. ## Check if enabled Once the SDK is initialized, you can check if a feature is enabled or not: ```swift let featureKey = "my_feature" let isEnabled = f.isEnabled(featureKey) if isEnabled { // do something } ``` You can also pass additional context per evaluation: ```swift let isEnabled = f.isEnabled(featureKey, [ // ...additional context ]) ``` ## Getting variation If your feature has any [variations](https://featurevisor.com/docs/features/#variations) defined, you can evaluate them as follows: ```swift let featureKey = "my_feature" let variation = f.getVariation(featureKey) if variation == "treatment" { // do something for treatment variation } else { // handle default/control variation } ``` Additional context per evaluation can also be passed: ```swift let variation = f.getVariation(featureKey, [ // ...additional context ]) ``` ## Getting variables Your features may also include [variables](https://featurevisor.com/docs/features/#variables), which can be evaluated as follows: ```swift let variableKey = "bgColor" let bgColorValue = f.getVariable("my_feature", variableKey) ``` Additional context per evaluation can also be passed: ```swift let bgColorValue = f.getVariable("my_feature", variableKey, [ // ...additional context ]) ``` ### Type specific methods Next to generic `getVariable()` methods, there are also type specific methods available for convenience: ```swift f.getVariableBoolean(featureKey, variableKey, context) f.getVariableString(featureKey, variableKey, context) f.getVariableInteger(featureKey, variableKey, context) f.getVariableDouble(featureKey, variableKey, context) f.getVariableArray(featureKey, variableKey, context) f.getVariableObject(featureKey, variableKey, context) f.getVariableJSON(featureKey, variableKey, context) ``` Type specific methods do not coerce strings or booleans into numbers. They return `nil` when the value does not match the requested type. ## Getting all evaluations You can get evaluations of all features available in the SDK instance: ```swift let allEvaluations = f.getAllEvaluations([:]) print(allEvaluations) ``` This is handy especially when you want to pass all evaluations from a backend application to the frontend. ## Sticky For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/): Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `SpawnOptions(sticky: ...)` when a child needs its own sticky state. ### Initialize with sticky ```swift let f = createFeaturevisor( FeaturevisorOptions( sticky: [ "myFeatureKey": EvaluatedFeature( enabled: true, variation: "treatment", variables: ["myVariableKey": .string("myVariableValue")] ), "anotherFeatureKey": EvaluatedFeature(enabled: false), ] ) ) ``` ### Set sticky afterwards ```swift f.setSticky([ "myFeatureKey": EvaluatedFeature( enabled: true, variation: "treatment", variables: ["myVariableKey": .string("myVariableValue")] ), "anotherFeatureKey": EvaluatedFeature(enabled: false), ], replace: true) ``` ## Setting datafile You may also initialize the SDK without passing `datafile`, and set it later on: ```swift f.setDatafile(datafileContent) ``` You can also set using raw JSON string: ```swift f.setDatafile(json: jsonString) ``` ### Merging by default By default, `setDatafile` merges the incoming datafile with the SDK instance's existing datafile: - incoming `features` and `segments` override matching keys - existing `features` and `segments` that are missing from the incoming datafile are kept - `revision`, `schemaVersion`, and `featurevisorVersion` are taken from the incoming datafile This means you can call `setDatafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. ### Replacing Pass `replace: true` to replace the stored datafile entirely: ```swift f.setDatafile(datafileContent, replace: true) f.setDatafile(json: jsonString, replace: true) ``` ### Loading datafiles on demand Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront. This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application: ```swift let f = createFeaturevisor(FeaturevisorOptions()) func loadDatafile(target: String) { let url = URL(string: "https://cdn.yoursite.com/production/featurevisor-\(target).json")! if let data = try? Data(contentsOf: url), let datafile = try? DatafileContent.fromData(data) { // merges into whatever was loaded before f.setDatafile(datafile) } } loadDatafile(target: "products") // later, when the user reaches checkout loadDatafile(target: "checkout") ``` ### Updating datafile You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly. ### Interval-based update ```swift import Foundation let interval: TimeInterval = 5 * 60 Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in if let data = try? Data(contentsOf: datafileURL), let datafile = try? DatafileContent.fromData(data) { f.setDatafile(datafile) } } ``` ## Diagnostics By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix. ### Levels Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`. Set the level during initialization or update it afterwards: ```swift let f = createFeaturevisor( FeaturevisorOptions(logLevel: .debug) ) f.setLogLevel(.info) ``` ### Handler Use `onDiagnostic` to send structured diagnostics to your observability system: ```swift let f = createFeaturevisor( FeaturevisorOptions( logLevel: .info, onDiagnostic: { diagnostic in print(diagnostic.level, diagnostic.code, diagnostic.message) } ) ) ``` Modules can also subscribe to diagnostics or report their own diagnostics from `setup` using the provided module API. Every diagnostic has `level`, `code`, `message`, and an object-shaped `details` dictionary. Optional `module`, `moduleName`, and `originalError` fields describe provenance. Evaluation metadata belongs in `details`. ## Events Featurevisor SDK implements a simple event emitter that allows you to listen to runtime events. ### `datafile_set` ```swift let unsubscribe = f.on(.datafileSet) { payload in print(payload.params) } unsubscribe() ``` ### `context_set` ```swift let unsubscribe = f.on(.contextSet) { _ in // handle context updates } unsubscribe() ``` ### `sticky_set` ```swift let unsubscribe = f.on(.stickySet) { _ in // handle sticky updates } unsubscribe() ``` ### `error` ```swift let unsubscribe = f.on(.error) { payload in if case .object(let diagnostic)? = payload.params["diagnostic"] { print(diagnostic["message"] ?? "") } } unsubscribe() ``` The `error` event is emitted for diagnostics whose level is `error`. ## Evaluation details If you need evaluation metadata, use: ```swift let flagDetails = f.evaluateFlag("my_feature") let variationDetails = f.evaluateVariation("my_feature") let variableDetails = f.evaluateVariable("my_feature", "my_variable") ``` ## Modules Modules allow you to intercept evaluation inputs and outputs. ### Defining a module ```swift let module = FeaturevisorModule( name: "my-module", setup: { api in api.reportDiagnostic( FeaturevisorDiagnostic( level: .info, code: "module_ready", message: "Module is ready" ) ) }, before: { options in var updated = options updated.dependencies.context["someAdditionalAttribute"] = .string("value") return updated }, bucketKey: { options in options.bucketKey }, bucketValue: { options in options.bucketValue }, after: { evaluation, _ in evaluation }, close: { // clean up module resources } ) ``` ### Registering modules ```swift let f = createFeaturevisor( FeaturevisorOptions( modules: [module] ) ) let removeModule = f.addModule(module) removeModule?() ``` ## Child instance You can spawn child instances with inherited context: ```swift let child = f.spawn([ "userId": .string("123"), ]) let enabled = child.isEnabled("my_feature") ``` ## Close To clear listeners and close resources: ```swift f.close() ``` ## CLI usage The package also ships an executable named `featurevisor`. All three commands accept repeatable `--target=` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI. ### Test ```bash swift run featurevisor test \ --projectDirectoryPath=/path/to/featurevisor-project ``` ### Benchmark ```bash swift run featurevisor benchmark \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --context='{"userId":"123"}' \ --n=1000 ``` ### Assess distribution ```bash swift run featurevisor assess-distribution \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --populateUuid=userId \ --n=1000 ``` ## GitHub repositories - See SDK repository here: [featurevisor/featurevisor-swift2](https://github.com/featurevisor/featurevisor-swift2) - See example application repository here: [featurevisor/featurevisor-example-swift](https://github.com/featurevisor/featurevisor-example-swift)]]> https://featurevisor.com/docs/segments Segments Learn how to create segments in Featurevisor src/app/docs/segments/page.md https://featurevisor.com/docs/sets Sets Split a Featurevisor project into independent sets that each own their own features, segments, attributes, and tests. src/app/docs/sets/page.md /`. ## Working with a single set Pass `--set=` to limit a command to one set: ```{% title="Command" %} $ npx featurevisor build --set=storefront $ npx featurevisor test --set=admin ``` When you print a datafile as JSON in a project with sets enabled, you have to pick a set: ```{% title="Command" %} $ npx featurevisor build --set=storefront --environment=production --json --pretty ``` ## Testing Tests run for every set by default: ```{% title="Command" %} $ npx featurevisor test ``` Test specs live inside each set's own `tests` directory, next to the features and segments they cover. To run tests for a single set, pass `--set`: ```{% title="Command" %} $ npx featurevisor test --set=storefront ``` ## Promotion between sets When sets model release lanes, you can copy definitions from one set to another using [promotions](/docs/promotions/). For example, you can promote a feature's definition from `dev` to `staging`, and later from `staging` to `production`. Read more in the [Promotions](/docs/promotions/) page. ## Comparison Sets sit alongside the other ways Featurevisor helps you organize a project: - [Environments](/docs/environments/): different sets of rules per feature, defined inside the same feature file - [Tags](/docs/tags/): group features so [targets](/docs/targets/) can select them - [Namespaces](/docs/namespaces/): organize features and segments hierarchically within a single tree - [Sets](/docs/sets/): split the project into independent trees that each own their own definitions]]> https://featurevisor.com/docs/site Status site generator Learn how to generate status website using Featurevisor src/app/docs/site/page.md https://featurevisor.com/docs/skills Skills for AI Agents Learn about skills for AI Agents in Featurevisor src/app/docs/skills/page.md /featurevisor create a showWishlist feature, tagged web, starting at 5% in production ### Spin up an A/B test > /featurevisor add an A/B test on the pricing page - control vs treatment, 50/50, only in Germany and the Netherlands ### Remote configuration > /featurevisor expose a checkoutConfig feature with paymentMethods (array) and allowDiscountCode (boolean), override paymentMethods to [paypal, ideal] for NL ### Write a complex targeting segment > /featurevisor create a segment for adult iPhone users in NL or DE who are not on the newsletter ### Set up mutually exclusive experiments > /featurevisor put oneClickCheckout and expressShipping in a mutex group, 50/50 slots ### Force-enable for QA in production > /featurevisor let the QA team see wishlist in production while it’s still at 0% for everyone else ### Answer "what's enabled where?" > /featurevisor list every feature enabled in production that has an A/B test running and no test spec ### Debug why an evaluation went the way it did > /featurevisor why is checkout disabled for userId 123 in NL? ### Generate test specs for an existing feature > /featurevisor write tests for sidebar covering all variations, all environments, and the QA force rule ### Find and clean up dead weight > /featurevisor find unused segments and attributes, plus any duplicate segments ### Deprecate a feature > /featurevisor deprecate the oldCheckout feature with a 2-week grace period ### Black Friday segment > /featurevisor add a Black Friday segment and ramp showPromoBanner to 100% in it ### Simulate traffic distribution > /featurevisor simulate the 25% wishlist rollout against 10k users — will it actually hit 25%? ### Explain a feature > /featurevisor explain the feature checkoutRedesign ### Start a new project > /featurevisor set up a new Featurevisor project with staging and production environments, and web and ios tags ### Integrate into an application > /featurevisor wire the checkout feature into my React app, with the datafile refreshing every 5 minutes ## Source The content of the skills is open source and available on [GitHub](https://github.com/featurevisor/featurevisor/tree/main/skills).]]> https://featurevisor.com/docs/state-files State files Learn about Featurevisor state files src/app/docs/state-files/page.md https://featurevisor.com/docs/tags Tagging features Tag your features to load them in your application via smaller datafiles src/app/docs/tags/page.md res.json()) const f = createFeaturevisor({ datafile: datafileContent, }) ``` Learn more about [SDKs](/docs/sdks). ## Testing features against targets When writing a feature's test spec, use the `target` property: ```yml {% path="tests/features/my_feature.spec.yml" highlight="8" %} feature: my_feature assertions: - environment: production at: 90 context: country: nl target: web expectedToBeEnabled: true ``` The test runner builds target datafiles in memory automatically: ``` $ npx featurevisor test ```]]> https://featurevisor.com/docs/targets Targets Define Featurevisor datafile targets with feature patterns, tag filters, and build-time context. src/app/docs/targets/page.md https://featurevisor.com/docs/testing Testing Learn how to test your features and segments in Featurevisor with declarative specs src/app/docs/testing/page.md https://featurevisor.com/docs/testing-features Testing features Learn how to test your features in Featurevisor with YAML specs src/app/docs/testing-features/page.md https://featurevisor.com/docs/tracking/google-analytics Google Analytics (GA) Learn how to integrate Featurevisor SDK with Google Tag Manager & Google Analytics src/app/docs/tracking/google-analytics/page.md https://featurevisor.com/docs/use-cases/decouple-releases-from-deployments Decouple feature releases from application deployments Learn how to decouple your feature releases from your application deployments using Featurevisor. src/app/docs/use-cases/decouple-releases-from-deployments/page.md https://featurevisor.com/docs/use-cases/dependencies Managing Feature Dependencies Learn how to manage feature dependencies using Featurevisor src/app/docs/use-cases/dependencies/page.md https://featurevisor.com/docs/use-cases/deprecation Deprecating feature flags safely Learn how to deprecate features and experiments safely with Featurevisor src/app/docs/use-cases/deprecation/page.md https://featurevisor.com/docs/use-cases/entitlements User entitlements Learn how to manage user entitlements using Featurevisor src/app/docs/use-cases/entitlements/page.md ", "name": "Erlich Bachman", "plan": "premium", // or `free` "country": "us" } ``` ## Attributes Let's start defining our Featurevisor attributes for your application. We will use them throughout this guide at various stages. ### `userId` This attribute will be used to identify the user in the runtime. The `id` field from the response of the User Profile service will be used for this purpose. ```yml {% path="attributes/userId.yml" %} description: User ID type: string ``` ### `country` This attribute will be used to identify the country of the user in the runtime. The `country` field from the response of the User Profile service will be used for this purpose. ```yml {% path="attributes/country.yml" %} description: Country codes in lowercase like us, nl, de, etc. type: string ``` ## Feature We will be creating a new feature called `plan` that will be used to control the entitlements of your users against various different plans. ```yml {% path="features/plan.yml" %} description: Plans and their entitlements against known User tags: - all bucketBy: userId # we define a variable called `entitlements`, # that will be an array of strings variablesSchema: entitlements: type: array defaultValue: - likePosts - commentOnPosts # we aren't running an experiment here, # and will rely on sticky features for users, # therefore weight distribution of variations are not relevant variations: - value: free weight: 100 - value: premium weight: 0 variables: entitlements: - likePosts - commentOnPosts - createPosts # extra entitlement for premium users only # this is a core application config, # and is recommended to be rolled out to 100% of the traffic rules: production: - key: everyone segments: '*' percentage: 100 ``` ## Evaluating entitlements with SDKs Now that we have defined our feature, we can use Featurevisor SDKs to evaluate the entitlements of your users in the runtime. First, initialize the SDK: ```js {% path="your-app/index.js" %} import { createFeaturevisor } from '@featurevisor/sdk' const DATAFILE_URL = 'https://cdn.yoursite.com/datafile.json' const datafileContent = await fetch(DATAFILE_URL) .then((res) => res.json()) const f = createFeaturevisor({ datafile: datafileContent, }) ``` Fetch your User's ID and Plan info from your User Profile service and make it available: ```js const userProfile = await fetch('https://api.yoursite.com/profile') .then((res) => res.json()) ``` Set sticky features in the SDK for known user: ```js // we want our known user to be always bucketed // into the same plan (variation) as User Profile service suggests f.setSticky({ plan: { enabled: true, variation: userProfile.plan, }, }) ``` Get available entitlements for the known user: ```js const featureKey = 'plan' const variableKey = 'entitlements' const context = { userId: userProfile.id, country: userProfile.country, } const entitlements = f.getVariable(featureKey, variableKey, context) ``` The `entitlements` variable will contain an array of all entitlements the user should have against their current plan. ```js const canCreatePosts = entitlements.includes('createPosts') const canLikePosts = entitlements.includes('likePosts') const canCommentOnPosts = entitlements.includes('commentOnPosts') ``` ## Managing entitlements in one place As your entitlements and number of plans grow, you can use Featurevisor to manage them all declaratively in one place. Your custom User Profile service only needs to be aware of the plan of the user, and nothing more unless you have any custom user specific overrides. Since Featurevisor JavaScript SDK is universal and works in both Node.js and browser environments, you can use it to evaluate your users' entitlements in your backend as well as in frontend. {% callout type="note" title="Always verify in backend" %} Please note that entitlements check in frontend is never a substitute for backend checks. You should always check entitlements in your backend before performing any action. {% /callout %} ## User overrides It is possible in specific circumstances, that you may want to override the entitlements of a user irrespective of what plan they are on. For example, if a user is on a free plan, but you want to give them access to create posts for free for a limited time. We can expect our User Profile service to optionally provide the override information given this is about a specific individual user: ```js // GET /profile { "id": "", "plan": "free", "country": "us", // optional field for overrides "overrideEntitlements": [ "likePosts", "commentOnPosts", "createPosts" ] } ``` We can then use the `overrideEntitlements` field from User Profile and set it as a sticky feature in Featurevisor SDK: ```js f.setStickyFeatures({ plan: { enabled: true, variation: userProfile.plan, variables: userProfile.overrideEntitlements ? // user overrides { entitlements: userProfile.overrideEntitlements } : // otherwise leave empty {}, }, }) ``` You can now continue evaluating entitlements as before using the SDK, and the user will have the overridden entitlements. ## Conditional entitlements It is possible that you may want to offer a specific entitlement to your users based on their location. We aren't talking about running experiments here targeting one specific country, but more like an entitlement that can only ever be available in one single country only. For the sake of this guide, let's assume your social media app can legally allow your users to upload videos in the US only in `premium` plan, and nowhere else. We can declare that config in our feature's definition as follows: ```yml {% path="features/plan.yml" %} # ... variations: - value: free weight: 100 - value: premium weight: 0 variables: entitlements: - likePosts - commentOnPosts - createPosts variableOverrides: entitlements: - conditions: - attribute: country operator: equals value: us value: - likePosts - commentOnPosts - createPosts - uploadVideos # for US users only # ... ``` The entitlements array may look repetitive here, but you can also take an approach of breaking down your entitlements into multiple variables instead of one as you see fit. ## Separate variables per entitlement If you do not wish to have a single variable for all entitlements, you can break them down into multiple variables as follows: ```yml {% path="features/plan.yml" %} description: Plans and their entitlements against known User tags: - all bucketBy: userId variablesSchema: canLikePosts: type: boolean defaultValue: true canCommentOnPosts: type: boolean defaultValue: true canCreatePosts: type: boolean defaultValue: false canUploadVideos: type: boolean defaultValue: false variations: - value: free weight: 100 - value: premium weight: 0 variables: canCreatePosts: true canUploadVideos: false variableOverrides: canUploadVideos: - conditions: - attribute: country operator: equals value: us value: true rules: production: - key: everyone segments: '*' percentage: 100 ``` This will then require you to evaluate each entitlement separately in your application code using Featurevisor SDKs: ```js const canCreatePosts = f.getVariable('plan', 'canCreatePosts', context) if (canCreatePosts) { // show create post button } ``` ## Conclusion When your application and its architecture grows big, and you have multiple teams working and shipping in a distributed fashion, it can become hard to manage entitlements in one place. Having them declared in one place as a single source of truth can help you manage them better, and also help you avoid any accidental entitlements leaks.]]> https://featurevisor.com/docs/use-cases/establishing-ownership Establishing feature ownership Learn how to handle feature ownership with Featurevisor src/app/docs/use-cases/establishing-ownership/page.md https://featurevisor.com/docs/use-cases/experiments Experiments Learn how to experiment with A/B Tests and Multivariate Tests using Featurevisor. src/app/docs/use-cases/experiments/page.md res.json()) const f = createFeaturevisor({ datafile: datafileContent, }) ``` Now we can evaluate the `ctaButton` feature wherever we need to render the CTA button: ```js const featureKey = 'ctaButton' const context = { deviceId: 'device-123', country: 'nl', deviceType: 'iphone', } const ctaButtonVariation = f.getVariation(featureKey, context) if (ctaButtonVariation === 'treatment') { // render the new CTA button return 'Get started' } else { // render the original CTA button return 'Sign up' } ``` Here we see only two variation cases, but we could have had more than two variations in our A/B test experiment. ## Multivariate Test on Hero element Let's say we want to run a Multivariate Test on the Hero section of your landing page. Previously we only ran an A/B test on the CTA button's text, but now we want to run a Multivariate Test on the Hero section affecting some or all its elements. We can map our requirements in a table below: | Variation | Headline | CTA button text | | ---------- | ----------- | --------------- | | control | Welcome | Sign up | | treatment1 | Welcome | Get started | | treatment2 | Hello there | Sign up | | treatment2 | Hello there | Get started | Instead of creating a separate feature per element, we can create a single feature for the Hero section and define multiple variables for each element. The relationship can be visualized as: - one **feature** - having multiple **variations** - each variation having its own set of **variable values** ```yml {% path="features/hero.yml" %} description: Hero section tags: - all bucketBy: deviceId # define a schema of all variables # scoped under `hero` feature first variablesSchema: headline: type: string defaultValue: Welcome ctaButtonText: type: string defaultValue: Sign up variations: - value: control weight: 25 - value: treatment1 weight: 25 variables: # we only need to define variables inside variations, # if the values are different than the default values ctaButtonText: Get started - value: treatment2 weight: 25 variables: headline: Hello there - value: treatment3 weight: 25 variables: headline: Hello there ctaButtonText: Get started rules: production: - key: everyone segments: '*' percentage: 100 ``` We just set up our first Multivariate test experiment that is: - rolled out to 100% of our traffic to everyone - with an even 25% split among all its variations - with each variation having different values for the variables ## Evaluating variables In your application, you can access the variables of the `hero` feature as follows: ```js {% path="your-app/index.js" %} const featureKey = 'hero' const context = { deviceId: 'device-123' } const headline = f.getVariable(featureKey, 'headline', context) const ctaButtonText = f.getVariable(featureKey, 'ctaButtonText', context) ``` Use the values inside your hero element (component) when you render it. ## Tracking activations We understood how to create features for defining simple A/B tests and also more complex multivariates using variables in Featurevisor, and then evaluate them in the runtime in our applications when we need those values. But we also need to track the performance of our experiments to understand which variation is doing better than others. This is where [modules API](/docs/sdks/javascript/#modules) comes in handy. Featurevisor SDK provides a way to register modules that can intercept the evaluation process and perform custom actions: ```js {% path="your-app/index.js" %} import { createFeaturevisor } from '@featurevisor/sdk' const f = createFeaturevisor({ datafile: '...', modules: [ { name: 'trackActivationsModule', // this module will be called after each variation evaluation after: function (evaluation) { const { reason, type, featureKey, variationValue } = evaluation; // error found if (reason === 'error') { return } // not a variation evaluation if (type !== "variation") { return } const feature = f.getFeature(featureKey); // feature has no variations if (!feature || !feature.variations) { return; } // track const { userId } = f.getContext(); const trackPayload = { event: 'featurevisorActivation', featureKey, variationValue, userId, } // send the trackPayload to your analytics service } } ], }) ``` As an example, you can refer to the guide of [Google Analytics](/docs/tracking/google-analytics/) for tracking purposes. {% callout type="note" title="Featurevisor is not an analytics platform" %} It is important to understand that Featurevisor is not an analytics platform. It is a feature management tool that helps you manage your features and experiments with a Git-based workflow, and helps evaluate your features in your application with its SDKs. {% /callout %} ## Mutually exclusive experiments Often times when we are running multiple experiments together, we want to make sure that they are mutually exclusive. This means that a user should not be bucketed into multiple experiments at the same time. In more plain words, the same user should not be exposed to multiple experiments together, and only one experiment at a time avoiding any overlap. One example: if User X is exposed to feature `hero` which is running our multivariate test, then the same User X should not be exposed to feature `wishlist` which is running some other A/B test in the checkout flow of the application. For those cases, you are recommended to see the [Groups](/docs/groups/) functionality of Featurevisor, which will help you achieve exactly that without your applications needing to do any extra code changes at all. ## Further reading You are highly recommended to read and understand the building blocks of Featurevisor which will help you make the most out of this tool: - [Attributes](/docs/attributes/): building block for conditions - [Segments](/docs/segments/): conditions for targeting users - [Features](/docs/features/): feature flags and variables with rollout rules - [Groups](/docs/groups/): mutually exclusive features - [SDKs](/docs/sdks/): how to consume datafiles in your applications ## Conclusion We learned how to use Featurevisor for: - Creating both simple A/B tests and more complex Multivariate tests - evaluate them in the runtime in our applications - track the performance of our experiments - activate the features when we are sure that the user has been exposed to them - make multiple experiments mutually exclusive if we need to Featurevisor can be a powerful tool in your experimentation toolkit, and can help you run experiments with a strong governance model in your organization given every change goes through a Pull Request in your Git repository and nothing gets merged without reviews and approvals.]]> https://featurevisor.com/docs/use-cases/microfrontends Microfrontends Architecture Learn how to manage your features in a microfrontends architecture. src/app/docs/use-cases/microfrontends/page.md # we will discuss this in next section below bucketBy: deviceId rules: staging: - key: everyone segments: '*' percentage: 100 production: - key: everyone segments: '*' percentage: 100 ``` ## Tagging features When we create or update any feature in your Featurevisor project, we can tag it with the microfrontend it belongs to: ```yml {% path="features/showMarketingBanner.yml" %} description: Shows marketing banner at the bottom of the page tags: - products # tagging with `products` microfrontend # ... ``` It is possible some features may overlap across microfrontends. For example, we may want to show a marketing banner to users in both the `products` and `checkout` microfrontends. In that case, we can tag the feature with both of them together: ```yml {% path="features/showMarketingBanner.yml" %} description: Shows marketing banner at the bottom of the page tags: - products - checkout # ... ``` ## Anonymous vs Authenticated Featurevisor relies on the feature's `bucketBy` property to determine how to [bucket](/docs/bucketing) the user. It's an easy decision to make when choosing the `bucketBy` value when the microfrontend is only accessible to either anonymous or authenticated users alone, and not both. | Microfrontend | Access | `bucketBy` | | ------------- | --------------- | ---------- | | `products` | Everyone | ? | | `signup` | Anonymous users | `deviceId` | | `signin` | Anonymous users | `deviceId` | | `checkout` | Everyone | ? | | `account` | Authenticated | `userId` | But what shall we do for microfrontends that are accessible to both anonymous and authenticated users? Like `products` and `checkout` in our example. {% callout type="note" title="Applies to both microfrontends and monoliths" %} This challenge applies to any application that deals with both anonymous and authenticated users, whether it's a microfrontends architecture or a monolithic application. {% /callout %} ### Design decision It's a design decision that we need to take here when defining our features. We can either choose to: - always use `deviceId` attribute for `bucketBy`, irrespective of whether the user is anonymous or authenticated, or - we can choose to use `deviceId` for anonymous users and `userId` for authenticated users. The drawback of using `deviceId` at all times for both anonymous and authenticated users is that it will result in inconsistent variations for logged in users across multiple sessions or devices. If we wish to get the maximum benefit of Featurevisor's consistent bucketing making sure the same logged in user sees the same variation across all devices and sessions, we can use: - `userId` for authenticated users, and - `deviceId` for anonymous users ### Bucket by first available attribute We can express that in our feature as follows: ```yml {% path="features/showMarketingBanner.yml" %} description: Shows marketing banner at the bottom of the page tags: - products - checkout bucketBy: # if `userId` is available then it will be used, # otherwise it will fall back to `deviceId` or: - userId - deviceId # ... ``` This will make sure when `userId` attribute is passed for evaluation to the SDK, it will be used for bucketing. Otherwise, it will fall back to `deviceId`. ## Review and approval workflow Given Featurevisor is a centralized feature management solution, it can help you manage your features in a microfrontends architecture in a single place conveniently. In our case, it is a single Git repository that contains all the features and their configurations, which will go through a review and approval workflow by all relevant teams before they can be deployed in the form of generated [datafiles](/docs/building-datafiles). ## Consuming datafiles in your microfrontend Once you have [built](/docs/building-datafiles) and [deployed](/docs/deployment) your datafiles, you can consume them using Featurevisor SDKs in your microfrontends. There are two common ways to do this. ### One instance per microfrontend Each microfrontend creates its own SDK instance with its own datafile: ```js {% path="products-microfrontend/index.js" %} // in `products` microfrontend import { createFeaturevisor } from '@featurevisor/sdk' const DATAFILE_URL = 'https://cdn.yoursite.com/production/featurevisor-products.json' const datafileContent = await fetch(DATAFILE_URL) .then(res => res.json()) const f = createFeaturevisor({ datafile: datafileContent, }) ``` Evaluate your features: ```js const featureKey = 'showMarketingBanner' const context = { deviceId: '...', // if available userId: '...', } const showMarketingBanner = f.isEnabled(featureKey, context) if (showMarketingBanner) { // render marketing banner } ``` While the snippets above suggest the usage of Featurevisor SDK in a single `products` microfrontend, it does not differ in any way if you were to use it in a monolithic application. This approach keeps each microfrontend fully isolated, which suits setups where microfrontends are developed and deployed as separate applications that do not share runtime state. ### One shared instance, loaded on demand If your microfrontends run together inside the same host application and can share state, you can instead create a **single** SDK instance and load each microfrontend's datafile only when the user reaches it. This works because [setDatafile](/docs/sdks/javascript/#setting-datafile) merges by default: loading a new microfrontend's datafile adds its features to the instance without removing the ones already loaded. ```js {% path="host-app/featurevisor.js" %} import { createFeaturevisor } from '@featurevisor/sdk' // one shared instance for the whole host application export const f = createFeaturevisor({}) const loadedMicrofrontends = new Set() export async function loadMicrofrontend(name) { if (loadedMicrofrontends.has(name)) { return } const url = `https://cdn.yoursite.com/production/featurevisor-${name}.json` const datafile = await fetch(url).then((res) => res.json()) f.setDatafile(datafile) // merges into whatever was loaded before loadedMicrofrontends.add(name) } ``` As the user navigates, load the datafile for the microfrontend they are entering: ```js {% path="host-app/router.js" %} import { f, loadMicrofrontend } from './featurevisor' // when the user opens the products page await loadMicrofrontend('products') // later, when they move to checkout, its features are // added without losing the products features await loadMicrofrontend('checkout') ``` This way the user only downloads the features for the parts of the application they actually visit, instead of everything on the first page load. Learn more in [Loading datafiles on demand](/docs/use-cases/on-demand-datafiles/). ## Conclusion We have seen how we can use Featurevisor to manage all our feature configurations in a microfrontends architecture in a single place declaratively, even if those features overlap and are used in multiple microfrontends together. We have also seen how [targets](/docs/targets/) let each microfrontend build a smaller datafile, and how the merge behavior of [setDatafile](/docs/sdks/javascript/#setting-datafile) lets a host application load those datafiles on demand as the user navigates, instead of downloading every feature upfront. We have also seen how to handle tricky situations like anonymous vs authenticated users, and how to make sure logged in users are bucketed in a way so they see the same variation across all devices and sessions consistently maintaining a solid user experience. The freedom and flexibility that microfrontends architecture brings in is great, but it also comes with its own set of challenges. Featurevisor can help you manage your features in a microfrontends architecture bringing all parties together with a strong reviews and approval workflow, and make sure your features are consistent across all your microfrontends for your users.]]> https://featurevisor.com/docs/use-cases/on-demand-datafiles Loading datafiles on demand Combine targets and merge-by-default setDatafile to load smaller datafiles as your application needs them. src/app/docs/use-cases/on-demand-datafiles/page.md res.json()) f.setDatafile(datafile) // merges into whatever was loaded before loadedTargets.add(target) } ``` Load only what the current page needs: ```js {% path="your-app/index.js" %} import { f, loadTarget } from './featurevisor' // on the products page await loadTarget('products') const showBanner = f.isEnabled('showMarketingBanner', { deviceId: '...' }) ``` Later, as the user navigates to another part of the application, load its datafile too. The features loaded earlier stay available: ```js {% path="your-app/checkout.js" %} import { f, loadTarget } from './featurevisor' // when the user reaches checkout await loadTarget('checkout') // both products and checkout features are now available on the same instance const useNewCheckout = f.isEnabled('newCheckout', { userId: '...' }) ``` ## Reacting to newly loaded features Each `setDatafile` call emits a [`datafile_set`](/docs/sdks/javascript/#datafile-set) event that includes the list of affected features. You can use it to re-evaluate and re-render the relevant part of your UI once a new datafile has been merged: ```js f.on('datafile_set', function ({ features }) { // `features` lists the keys that were added, updated, or removed // re-render the parts of the UI that depend on them }) ``` ## Things to keep in mind - Each target datafile is self-contained: it carries the features it needs along with the [segments](/docs/segments/) those features reference. Merging accumulates both. - If a feature is tagged for more than one target, it appears in each of those datafiles. Merging the same feature again is harmless, as the incoming copy simply overrides the matching key. - After a merge, the instance's revision reflects the revision of the most recently loaded datafile, not a combined value. ## Where this helps - [Microfrontends](/docs/use-cases/microfrontends/): each microfrontend loads its own datafile as the user navigates to it - Large single-page applications split by route or section - Any application where the features needed are only known once the user reaches a particular area]]> https://featurevisor.com/docs/use-cases/progressive-delivery Progressive Delivery Learn how to deliver features progressively to your users using Featurevisor. src/app/docs/use-cases/progressive-delivery/page.md https://featurevisor.com/docs/use-cases/rbac Role-Based Access Control (RBAC) Learn how to manage role based access control using Featurevisor src/app/docs/use-cases/rbac/page.md `segments/role-manager.yml` - `editor` => `segments/role-editor.yml` - `viewer` => `segments/role-viewer.yml` These segments are reusable across any number of features. Create them once, use them everywhere. ## Define features Now we can start defining [features](/docs/features) that target those [segments](/docs/segments). Because of how composable things are with Featurevisor, we can combine role segments with `and`, `or`, and `not` operators to express any access policy. Some examples below: ### Admin-only ```yml {% path="features/admin-settings-panel.yml" %} description: | Advanced settings panel visible only to admins and super admins tags: - web bucketBy: userId rules: production: - key: admins segments: or: - role-admin - role-super-admin percentage: 100 - key: everyone-else segments: "*" percentage: 0 ``` ### Managers and above ```yml {% path="features/billing-dashboard.yml" %} description: | Billing dashboard visible to managers, admins, and super admins tags: - web bucketBy: userId rules: production: - key: billing_roles segments: or: - role-manager - role-admin - role-super-admin percentage: 100 - key: everyone-else segments: "*" percentage: 0 ``` ### Everyone except viewers ```yml {% path="features/bulk-export.yml" %} description: | Bulk data export available to all roles except read-only viewers tags: - web bucketBy: userId rules: production: - key: non-viewers segments: not: - role-viewer percentage: 100 - key: everyone-else segments: "*" percentage: 0 ``` ### Roles with other segments We can also combine role requirements with other segments. Here, a new analytics featture is being rolled out to managers in the Netherlands first: ```yml {% path="features/advanced-analytics.yml" %} description: | Advanced analytics gradual rollout starting with managers in the Netherlands tags: - web bucketBy: userId rules: production: - key: nl-managers-early-access segments: and: - role-manager - netherlands # referencing segments/netherlands.yml percentage: 100 - key: everyone-else segments: "*" percentage: 0 ``` This is where Featurevisor truly shines, as RBAC and progressive delivery are not separate systems. They are composable rules in the same declarative file. ## Setting context in SDK The backend provides the roles for the current user, and application code passes them into the SDK [context](/docs/sdks/javascript/#context) in runtime: ```js {% path="your-app/index.js" %} import { createFeaturevisor } from '@featurevisor/sdk' const session = await fetch('/api/me').then((res) => res.json()) const datafileContent = await fetch(DATAFILE_URL).then((res) => res.json()) const f = createFeaturevisor({ context: { userId: session.userId, roles: session.roles, // e.g. ['admin', 'super-admin'] country: 'nl', }, datafile: datafileContent, }) ``` Learn more about building datafiles [here](/docs/building-datafiles). ## Evaluating features Now we can use the [SDK](/docs/sdks/javascript) instance to evaluate features wherever we please: ```js {% path="your-app/index.js" %} const canSeeAdminSettings = f.isEnabled('admin-settings-panel') const canSeeBillingDashboard = f.isEnabled('billing-dashboard') const canSeeBulkExport = f.isEnabled('bulk-export') const canSeeAdvancedAnalytics = f.isEnabled('advanced-analytics') ``` Featurevisor offers additional SDK support covering [React](/docs/react), [Go](/docs/sdks/go), [Python](/docs/sdks/python), [Java](/docs/sdks/java), [Swift](/docs/sdks/swift), and [more](/docs/sdks). ## Benefits by audience ### Product Managers Every feature's access policy is a readable file in Git: - You can see which roles unlock which capabilities - Changes go through Pull Requests with reviews and approvals - No more digging through application code scattered across multiple repositories/teams why a certain feature is accessible for some users and not others ### Backend Engineers Your API remains the authoritative source of roles for the current logged in user, and you are not losing any control. The pattern suggested in this guide means the frontend application(s) stops hard-coding role names in its code everywhere, which was always a maintenance liability and a potential source of inconsistency with what the backend actually enforces. The SDK reads the roles you provide, and does not invent them. ### Frontend Engineers You write `f.isEnabled('my_feature_key')`, and that's it. You become free from maintaining all the conditions and role-checking logic in your codebase, and you only ask "is this feature enabled?" When a role requirement changes, it's a feature definition change outside of your application codebase, not resulting in any code changes or refactors scattered across multiple components. The context object is populated once, against which all evaluations are run, and you don't have to keep passing it around for each and every evaluation which may happen from 20 different places. The same is true for any application (including backend services) that needs to evaluate features based on roles.]]> https://featurevisor.com/docs/use-cases/remote-configuration Remote configuration Learn how to manage remote configuration using Featurevisor src/app/docs/use-cases/remote-configuration/page.md res.json()) const f = createFeaturevisor({ datafile: datafileContent, }) ``` Now we can evaluate the variable in our application: ```js const featureKey = 'checkout' const variableKey = 'paymentMethods' const context = { userId: 'user-123', country: 'nl' } const paymentMethods = f.getVariable(featureKey, variableKey, context) console.log(paymentMethods) // [ // "creditCard", // "paypal", // "applePay", // "googlePay" // ] ``` With this evaluated ordered array of payment methods in the runtime, we can now render the list of payment methods in our checkout flow of the application without having to hardcode this list anywhere. ## Overriding variables by rules From above example, we can see that all our users will be able to use all the payment methods. But what if we want to restrict some payment methods to some users based in certain countries? We can do that by overriding the variables via our rollout [rules](/docs/features/#rules). Assuming we already have a [segment](/docs/segments) created for targeting users in the Netherlands: ```yml {% path="segments/netherlands.yml" %} description: Target users in the Netherlands conditions: - attribute: country operator: equals value: nl ``` We can now use this segment in our rollout rules and override the `paymentMethods` variable: ```yml {% path="features/checkout.yml" %} # ... rules: production: - key: nl segments: netherlands percentage: 100 variables: paymentMethods: - paypal - ideal - key: everyone segments: '*' percentage: 100 ``` Now when we evaluate our features, we will get different results for users in the Netherlands: ```js // Users in the Netherlands const context = { userId: 'user-234', country: 'nl' } const paymentMethods = f.getVariable(featureKey, variableKey, context) console.log(paymentMethods) // [ // "paypal", // "ideal" // ] ``` While rest of the world will still get the same result as before (that is the default value as defined in the variable schema): ```js // Users in the US const context = { userId: 'user-123', country: 'us' } const paymentMethods = f.getVariable(featureKey, variableKey, context) console.log(paymentMethods) // [ // "creditCard", // "paypal", // "applePay", // "googlePay" // ] ``` ## Other ways of overriding variables Depending on your needs, it is also possible to override variables: - at each [variation level](/docs/features/#overriding-variables) acting as an experiment, and also - at environment level by [forcing it](/docs/features/#force) You can see other use cases here detailing these approaches: - [A/B & Multi-variate testing](/docs/use-cases/experiments) - [Managing user entitlements](/docs/use-cases/entitlements) - [Testing in production](/docs/use-cases/testing-in-production) ## How do applications get latest configuration? There are two ways this can happen: - You can configure your SDK to keep refreshing the datafile at a certain **interval** (like every 30 seconds), or - When deploying your Featurevisor datafiles, you can broadcast a notification to all your applications to refresh their datafiles **manually** enabling over the air updates You can refer to the SDK guide here for [refreshing datafile](/docs/sdks/javascript/#refreshing-datafile). ## Full feature example Based on our original requirements, we can express the `checkout` feature with all its variables as follows: ```yml {% path="features/checkout.yml" %} description: Checkout flow configuration tags: - checkout bucketBy: userId variablesSchema: paymentMethods: type: array defaultValue: - creditCard - paypal - applePay - googlePay creditCards: type: array defaultValue: - visa - mastercard - amex shippingMethods: type: array defaultValue: - standard - express allowDiscountCode: type: boolean defaultValue: false rules: production: - key: nl segments: netherlands percentage: 100 variables: paymentMethods: - paypal - ideal allowDiscountCode: true - key: everyone segments: '*' percentage: 100 ``` Based on our requirements, we can keep overriding these variables against different rules as needed. Learn more about [variables](/docs/features/#variables), its supported [types](/docs/features/#variable-types), and how to [override](/docs/features/#overriding-variables) them in [features](/docs/features) documentation. ## Conclusion We learned about: - various benefits of separating runtime configuration from our application - how to break down different configuration parameters of our application into variables - having them defined in a feature declaratively using Featurevisor - overriding them using rollout rules based on our needs Overall, Featurevisor can help manage your application's runtime configuration in a highly readable and maintainable way for your team and your organization, with a strong review and approval process in one single place for everyone in the form of a Git repository.]]> https://featurevisor.com/docs/use-cases/testing-in-production Testing in production Learn how to coordinate testing in production with Featurevisor src/app/docs/use-cases/testing-in-production/page.md res.json()) const f = createFeaturevisor({ datafile: datafileContent, }) ``` Evaluate with the right set of attributes as context: ```js const featureKey = 'wishlist' const context = { userId: 'user-id-1', deviceId: 'device-id-1', } const isWishlistEnabled = f.isEnabled(featureKey, context) if (isWishlistEnabled) { // render the wishlist feature } ``` ## Anonymous users Not all features are only exposed to logged in users. Some features are exposed to anonymous users as well. Think of a new feature that is only available in the landing page of your application, and you want to expose it to all users, regardless of whether they are logged in or not. For those cases, we can rely on the `deviceId` value, which can be generated (can be an UUID) and persisted in the client-side. For browsers, this value can be generated and stored in localStorage for example. Once we know those values, all we have to do is go and update the `qa` segment only: ```yml {% path="segments/qa.yml" %} description: QA team members conditions: or: # the User IDs of QA team members - attribute: userId operator: in values: - 'user-id-1' - 'user-id-2' - 'user-id-3' - 'user-id-4' - 'user-id-5' # the Device IDs of QA team members - attribute: deviceId operator: in values: - 'device-id-1' - 'device-id-2' - 'device-id-3' - 'device-id-4' - 'device-id-5' ``` We turned our original condition in the segment to an `or` condition, and added the `deviceId` condition to it. This way, whenever any of the conditions match, we will consider the user (either logged in or not) as a QA team member. ## Gradual rollout Once the QA team has verified that the feature works as expected: - the `qa` segment can be removed from the feature's rule, and - we can begin rolling it out to a small percentage of our real users in production. ```yml {% path="features/wishlist.yml" %} # ... rules: production: - key: everyone segments: '*' percentage: 5 # 5% of the traffic ``` As we gain more confidence, we can increase the `percentage` value gradually all the way up to `100`. ## Conclusion We have just learned how to coordinate testing in production in our organization with Featurevisor, where we can expose features to a known subset of all users who can provide us early feedback (either manually or in an automated way), and how to evaluate those features with SDKs reliably. All done while maintaining a single source of truth for managing the QA segment, and without having to deploy any code changes of our application. Featurevisor is smart enough to not include any segment (like `qa`) in generated datafiles that is not being used actively in any of the feature flags belonging to any generated datafiles, so we don't have to worry about the datafile size growing unnecessarily.]]> https://featurevisor.com/docs/use-cases/trunk-based-development Trunk-based Development Learn how to achieve trunk-based development using Featurevisor src/app/docs/use-cases/trunk-based-development/page.md https://featurevisor.com/docs/variables Variables Learn about feature variables in Featurevisor src/app/docs/variables/page.md https://featurevisor.com/docs/variations Variations Learn about feature variations in Featurevisor src/app/docs/variations/page.md https://featurevisor.com/docs/vue Vue.js SDK Learn how to use Featurevisor SDK with Vue.js for evaluating feature flags src/app/docs/vue/page.md res.json()) const f = createFeaturevisor({ datafile, }) const app = createApp({ /* root component options */ }) setupApp(app, f) ``` This will set up the SDK instance in your Vue application, and make it available in all components later. ## Functions The package comes with several functions to use in your components: ### useFlag Check if feature is enabled or not: ```html ``` ### useVariation Get a feature's evaluated variation: ```html ``` ### useVariable Get a feature's evaluated variable value: ```html ``` ### useSdk Get the SDK instance: ```html ``` ## Tracking To track when a feature is exposed to a user, register a [module](/docs/sdks/javascript/#modules) on the SDK instance and react to evaluations in its `after` callback. See the [Google Analytics tracking](/docs/tracking/google-analytics/) guide for an example. ## Optimization Given the nature of components in Vue.js, they can re-render many times. You are advised to minimize the number of calls to Featurevisor SDK in your components by using memoization techniques. ## Example repository You can find a fully functional example of a Vue.js application using Featurevisor SDK here: [https://github.com/featurevisor/featurevisor-example-vue](https://github.com/featurevisor/featurevisor-example-vue). {% callout type="note" title="Help wanted with tests" %} We are looking for help with writing tests for this package. If you are interested, please take a look [here](https://github.com/featurevisor/featurevisor/tree/main/packages/vue). {% /callout %}]]>