Migration guides
Migrating from v1 to v2
- Defining attributes
- Defining segments
- Defining features
- Defining variable schemaBreaking
- When feature is disabled, use default variable valueNew
- When feature is disabled, serve different variable valueNew
- When feature is disabled, serve a specific variation valueNew
- Variable overrides from variationsBreaking
- Defining rulesBreaking
- Defining forced overridesBreaking
- Exposing feature in datafileBreaking
- Variation weight overridesNew
- Project configuration
- outputDirectoryPathBreaking
- datafileNamePatternNew
- CLI usage
- JavaScript SDK usage
- Upgrade to latest SDKNew
- Fetching datafileBreaking
- Refreshing datafileBreaking
- Getting variationSoft breaking
- Getting variableSoft breaking
- ActivationBreaking
- Sticky featuresBreaking
- Initial featuresBreaking
- Setting contextNew
- LoggingBreaking
- HooksNew
- Intercepting contextBreaking
- EventsBreaking
- Child instanceNew
- Get all evaluationsNew
- Configuring bucket keyBreaking
- Configuring bucket valueBreaking
- React SDK usage
- Testing features
- stickyNew
- expectedEvaluationsNew
- childrenNew
Detailed guide for migrating existing Featurevisor projects (using Featurevisor CLI) and applications (using Featurevisor SDKs) to latest v2.0.
Defining attributes#
Attribute as an object New#
Attribute values in context can now also be flat objects.
You can still continue to use other existing attribute types without any changes. This change is only if you wish to define attributes as objects.
Defining attribute#
description: My userId attributetype: string
description: My userCountry attributetype: string
description: My user attribute descriptiontype: objectproperties: id: type: string description: The user ID country: type: string description: The country of the user
Passing attribute in context#
When evaluating values in your application with SDKs, you can pass the value as an object:
const f; // Featurevisor SDK instanceconst context = { userId: '123', userCountry: 'nl', browser: 'chrome',}const isFeatureEnabled = f.isEnabled( 'myFeature', context)
const f; // Featurevisor SDK instanceconst context = { user: { id: '123', country: 'nl', }, browser: 'chrome',}const isFeatureEnabled = f.isEnabled( 'myFeature', context)
Dot separated path#
You can make use of dot-separated paths to specify nested attributes.
For example, inside features:
# ...bucketBy: userId
# ...bucketBy: user.id
And also in conditions:
description: Netherlands segmentconditions: - attribute: userCountry operator: equals value: nl
description: Netherlands segmentconditions: - attribute: user.country operator: equals value: nl
Learn more in Attributes page.
Defining segments#
Conditions targeting everyone New#
We can now use asterisks (*
) in conditions (either directly in segments or in features) to match any condition:
description: My segment descriptionconditions: '*'
This is very handy when you wish to start with an empty segment, then later add conditions to it.
Operator: exists New#
Checks if the attribute exists in the context:
description: My segment descriptionconditions: - attribute: browser operator: exists
const f; // Featurevisor SDK instanceconst context = { userId: '123', browser: 'chrome', // exists}const isFeatureEnabled = f.isEnabled( 'myFeature', context)
Operator: notExists New#
Checks if the attribute does not exist in the context:
description: My segment descriptionconditions: - attribute: browser operator: notExists
const f; // Featurevisor SDK instanceconst context = { userId: '123', // `browser` does not exist}const isFeatureEnabled = f.isEnabled( 'myFeature', context)
Operator: includes New#
Checks if a certain value is included in the attribute's array (of strings) value.
description: My segment descriptionconditions: - attribute: permissions operator: includes value: write
const f; // Featurevisor SDK instanceconst context = { userId: '123', permissions: [ 'read', 'write', // included 'delete', ],}const isFeatureEnabled = f.isEnabled( 'myFeature', context)
Operator: notIncludes New#
Checks if a certain value is not included in the attribute's array (of strings) value.
description: My segment descriptionconditions: - attribute: permissions operator: notIncludes value: write
const f; // Featurevisor SDK instanceconst context = { userId: '123', permissions: [ 'read', // 'write' is not included 'delete', ],}const isFeatureEnabled = f.isEnabled( 'myFeature', context)
Operator: matches New#
Checks if the attribute's value matches a regular expression:
description: My segment descriptionconditions: - attribute: userAgent operator: matches value: '(Chrome|Firefox)\/([6-9]\d|\d{3,})' # optional regex flags regexFlags: i
const f; // Featurevisor SDK instanceconst context = { userId: '123', userAgent: window.navigator.userAgent,}const isFeatureEnabled = f.isEnabled( 'myFeature', context)
Operator: notMatches New#
Checks if the attribute's value does not match a regular expression:
description: My segment descriptionconditions: - attribute: userAgent operator: notMatches value: '(Chrome|Firefox)\/([6-9]\d|\d{3,})' # optional regex flags regexFlags: i
const f; // Featurevisor SDK instanceconst context = { userId: '123', userAgent: window.navigator.userAgent,}const isFeatureEnabled = f.isEnabled( 'myFeature', context)
Learn more in Segments page.
Defining features#
Defining variable schema Breaking#
# ...variablesSchema: - key: myVariableKey type: string defaultValue: 'default value'
# ...variablesSchema: myVariableKey: type: string defaultValue: 'default value'
Learn more in Variables section.
When feature is disabled, use default variable value New#
When a feature itself is evaluated as disabled, its variable values by default always get evaluated as empty (undefined
in v1, and null
in v2).
Now, you can choose on a per variable basis whether to serve the default value if the feature is disabled or default to null
.
# ...variablesSchema: - key: myVariableKey type: string defaultValue: default value
# ...variablesSchema: myVariableKey: type: string defaultValue: default value # optionally serve default value # when feature is disabled useDefaultWhenDisabled: true
Learn more in Variables section.
When feature is disabled, serve different variable value New#
Instead of serving default value, if you want to a different value to be served for your variable whenthe feature itself is disabled, you can do this:
# ...variablesSchema: - key: myVariableKey type: string defaultValue: default value
# ...variablesSchema: myVariableKey: type: string defaultValue: default value # optionally serve different value # when feature is disabled disabledValue: different value for disabled feature
Learn more in Variables section.
When feature is disabled, serve a specific variation value New#
If the feature itself is evaluated as disabled, then its variation value will be evaluated as null
by default.
If you wish to serve a specific variation value in those cases, you can do this:
# ...variations: - value: control weight: 50 - value: treatment weight: 50
# ...variations: - value: control weight: 50 - value: treatment weight: 50disabledVariationValue: control
Learn more in Variations section.
Variable overrides from variations Breaking#
# ...variations: - value: control weight: 50 - value: treatment weight: 50 # had to be used together variables: - key: bgColor value: blue overrides: - segments: netherlands value: orange
# ...variations: - value: control weight: 50 - value: treatment weight: 50 # can be overridden independently variables: bgColor: blue variableOverrides: bgColor: - segments: netherlands value: orange
Learn more in Variables section.
Defining rules Breaking#
Rules have moved to top level of the feature definition, and the environments
property is no longer used.
This has resulted in less nesting and more clarity in defining rules.
# ...environments: production: rules: - key: everyone segments: '*' percentage: 100
# ...rules: production: - key: everyone segments: '*' percentage: 100
Learn more in Rules section.
Defining forced overrides Breaking#
Similar to rules above, force entries have moved to top level of the feature definition as well.
# ...environments: production: force: - segments: qa enabled: true
# ...force: production: - segments: qa enabled: true
Learn more in Force section.
Exposing feature in datafile Breaking#
The expose
property had a very rare use case, that controlled the inclusion of a feature in generated datafiles targeting a specific environment and/or tag.
# ...environments: production: expose: false
# ...expose: production: false
Learn more in Expose section.
Variation weight overrides New#
If you are running experiments, you can now override the weights of your variations on a per rule basis:
# ...variations: # common weights for all rules - value: control weight: 50 - value: treatment weight: 50rules: production: - key: netherlands segments: netherlands percentage: 100 # override the weights here for this rule alone variationWeights: control: 10 treatment: 90 - key: everyone segments: '*' percentage: 100
Learn more in Variations section.
Project configuration#
outputDirectoryPath Breaking#
Default output directory path has been changed from dist
to datafiles
.
This is to better reflect the contents of the directory.
module.exports = { // defaulted to this directory outputDirectoryPath: 'dist',}
module.exports = { // defaults to this directory datafilesDirectoryPath: 'datafiles',}
datafileNamePattern New#
Previously defaulted to datafile-%s.json
, it has been changed to featurevisor-%s.json
.
module.exports = { // no option available to customize it}
module.exports = { datafileNamePattern: 'featurevisor-%s.json',}
Learn more in Configuration page.
CLI usage#
Upgrade to latest CLI New#
In your Featurevisor project repository:
$ npm install --save @featurevisor/cli@2
Building v1 datafiles New#
It is understandable you may have applications that still consume v1 datafiles using v1 compatible SDKs.
To keep supporting both v1 and v2 from the same project in a backwards compatible way, you can build new v2 datafiles as usual:
$ npx featurevisor build
and on top of that, also build v1 datafiles:
$ npx featurevisor build \ --schema-version=1 \ --no-state-files \ --datafiles-dir=datafiles/v1
Using hash as datafile revision New#
By default, every time you build datafiles, a new revision is generated which is an incremental number.
$ npx featurevisor build
You may often have changes like updating a feature's description, which do not require a new revision number. To avoid that, you can pass --revisionFromHash
option to the CLI:
$ npx featurevisor build --revisionFromHash
If individual datafile contents do not change since last build, the revision will not change either. This helps implement caching when serving datafiles from CDN with ease.
Datafile naming convention Breaking#
Naming convention of built datafiles has been changed from datafile-tag-<tag>.json
to featurevisor-tag-<tag>.json
to help distinguish between Featurevisor datafiles and other datafiles that may be used in your project:
$ tree dist.├── production│ └── datafile-tag-all.json└── staging └── datafile-tag-all.json2 directories, 2 files
$ tree datafiles.├── production│ └── featurevisor-tag-all.json└── staging └── featurevisor-tag-all.json2 directories, 2 files
If you wish to maintain the old naming convention, you can update your project configuration:
module.exports = { // ... datafilesDirectoryPath: 'dist', datafileNamePattern: 'datafile-%s.json',}
JavaScript SDK usage#
Upgrade to latest SDK New#
In your application repository:
$ npm install --save @featurevisor/sdk@2
Fetching datafile Breaking#
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:
import { createInstance } from '@featurevisor/sdk'const DATAFILE_URL = '...'const f = createInstance({ datafileUrl: DATAFILE_URL, onReady: () => { console.log('SDK is ready') },})
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.
Refreshing datafile Breaking#
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:
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 refreshf.refresh()// stop/start refreshingf.stopRefreshing()f.startRefreshing()
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 intervalsetInterval(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.
Getting variation Soft breaking#
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.
const f; // Featurevisor SDK instanceconst context = { userId: '123' }// could be either `string` or `undefined`const variation = f.getVariation( 'myFeature', context)
const f; // Featurevisor SDK instanceconst context = { userId: '123' }// now either `string` or `null`const variation = f.getVariation( 'myFeature', context)
Getting variable Soft breaking#
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
.
const f; // Featurevisor SDK instanceconst context = { userId: '123' }// could be either value or `undefined`const variableValue = f.getVariable( 'myFeature', 'myVariableKey', context)
const f; // Featurevisor SDK instanceconst context = { userId: '123' }// now either value or `null`const variation = f.getVariable( 'myFeature', 'myVariableKey', context)
This is applicable for type specific SDK methods as well for variables:
getVariableString
getVariableBoolean
getVariableInteger
getVariableDouble
getVariableArray
getVariableObject
getVariableJSON
Activation Breaking#
Experiment activations are not handled by the SDK any more.
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)
import { createInstance } from '@featurevisor/sdk'const f; // Featurevisor SDK instanceconst context = { userId: '123' }const variation = f.getVariation("mFeature", context);// send to your analytics service heretrack('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.
Sticky features Breaking#
import { createInstance } from '@featurevisor/sdk'const stickyFeatures = { myFeatureKey: { enabled: true, variation: 'control', variables: { myVariableKey: 'myVariableValue', }, },}// when creating instanceconst f = createInstance({ stickyFeatures: stickyFeatures,})// replacing sticky features laterf.setStickyFeatures(stickyFeatures)
import { createInstance } from '@featurevisor/sdk'const stickyFeatures = { myFeatureKey: { enabled: true, variation: 'control', variables: { myVariableKey: 'myVariableValue', }, },}// when creating instanceconst f = createInstance({ sticky: stickyFeatures,})// replacing sticky features laterf.setSticky(stickyFeatures, true)
Unless true
is passed as the second argument, the sticky features will be merged with the existing ones.
Initial features Breaking#
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.
import { createInstance } from '@featurevisor/sdk'const initialFeatures = { myFeatureKey: { enabled: true, variation: 'control', variables: { myVariableKey: 'myVariableValue', }, },}// when creating instanceconst f = createInstance({ initialFeatures: initialFeatures,})
import { createInstance } from '@featurevisor/sdk'const initialFeatures = { myFeatureKey: { enabled: true, variation: 'control', variables: { myVariableKey: 'myVariableValue', }, },}// you can pass them as sticky insteadconst f = createInstance({ sticky: initialFeatures,})// fetch and set datafile afterf.setDatafile(datafileContent)// remove sticky features afterf.setSticky( {}, // replacing with empty object true)
Setting context New#
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ // ...})const isFeatureEnabled = f.isEnabled( 'myFeature', // pass context directly only { userId: '123' },)
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 entirelyf.setContext( { userId: '123', browser: 'firefox', }, true, // replace)// already set context will be used automaticallyconst isFeatureEnabled = f.isEnabled('myFeature')// you can still pass context directly// for overriding specific attributesconst isFeatureEnabled = f.isEnabled( 'myFeature', // still allows passing context directly { browser: 'edge' },)
Logging Breaking#
Instead of passing all log 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 Breaking#
import { createInstance, createLogger} from '@featurevisor/sdk'const f = createInstance({ logger: createLogger({ levels: [ 'debug', 'info', 'warn', 'error', ], })})
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.
Passing log level when creating SDK instance New#
Alternatively, you can also pass the log level directly when creating the SDK instance:
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ logLevel: 'debug',})
Setting log level after creating SDK instance Breaking#
You can also change the log level after creating the SDK instance:
f.setLogLevels([ 'error', 'warn', 'info', 'debug',])
f.setLogLevel('debug')
Read more in Logging section.
Hooks New#
Hooks are a set of new APIs allowing you to intercept the evaluation process and customize it.
A hook can be defined as follows:
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:
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ // ... hooks: [myCustomHook],})
You can also register the hook after creating the SDK instance:
const f; // Featurevisor SDK instanceconst removeHook = f.addHook(myCustomHook)// remove the hook laterremoveHook()
Intercepting context Breaking#
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ // ... interceptContext: function (context) { // modify context before evaluation return { ...context, someAdditionalAttribute: 'value', } },})
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 }, }, ],})
Events Breaking#
All the known events from v1 SDK have been removed in v2 SDK:
- Readiness: see fetching datafile
onReady
option and methodready
event
- Refreshing: see refreshing datafile
refresh
event and methodstartRefreshing
methodstopRefreshing
methodonRefresh
optionupdate
eventonUpdate
option
- Activation: see activation
activate
event and methodonActivate
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 New#
Will trigger when a datafile is set to the SDK instance:
const f; // Featurevisor SDK instanceconst 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 New#
Will trigger when context is set to the SDK instance:
const f; // Featurevisor SDK instanceconst unsubscribe = f.on("context_set", ({ replaced, // true if context was replaced context, // the new context}) => { console.log('Context set')})unsubscribe();
sticky_set New#
Will trigger when sticky features are set to the SDK instance:
const f; // Featurevisor SDK instanceconst 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 New#
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:
import { createInstance } from '@featurevisor/sdK'const f = createInstance({ datafile: datafileContent,})// set common context for allf.setContext({ apiVersion: '5.0.0',})
Afterwards, you can spawn child instances from it:
// 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 instanceconst isFeatureEnabled = childF.isEnabled('myFeature')
Similar to primary instance, you can also set context and sticky features in child instances:
// override child context later if neededchildF.setContext({ country: 'de',})// when evaluating, you can still pass additional contextconst 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 New#
You can get evaluation results of all your features currently loaded via datafile in the SDK instance:
const f; // Featurevisor SDK instanceconst 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 Breaking#
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ configureBucketKey: function (options) { const { featureKey, context, // default bucket key bucketKey, } = options return bucketKey },})
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 }, }, ],})
Configuring bucket value Breaking#
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ configureBucketValue: function (options) { const { featureKey, context, // default bucket value bucketValue, } = options return bucketValue },})
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 }, }, ],})
Learn more in JavaScript SDK 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 page.
Testing features#
sticky New#
Test specs of features can now also include sticky features, similar to SDK's API:
feature: myFeatureassertions: - description: My feature is enabled environment: production at: 100 context: country: nl sticky: myFeatureKey: enabled: true variation: control variables: myVariableKey: myVariableValue expectedToBeEnabled: true
expectedEvaluations New#
You can go deep with testing feature evaluations, including their evaluation reasons for example:
feature: myFeatureassertions: - 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 New#
Based on the new child instance API in SDK, you can also imitate testing against them via test specs:
feature: myFeatureassertions: - 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 page.