Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

steps:
- name: Check out repository
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand All @@ -45,7 +45,7 @@ jobs:

steps:
- name: Check out repository
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:

steps:
- name: Check out repository
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
featurevisor (1.1.0)
featurevisor (2.0.0)
benchmark (>= 0, < 1)

GEM
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2025 Fahad Heylaal (https://fahad19.com)
Copyright (c) 2026 Fahad Heylaal (https://fahad19.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ f = Featurevisor.create_featurevisor(

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.

Concurrent evaluations are safe after an instance is configured. Do not mutate or close the same instance concurrently with evaluations. Serialize calls to `set_datafile`, `set_context`, `set_sticky`, `add_module`, `remove_module`, and `close`. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.

## Initialization

The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
Expand Down Expand Up @@ -697,6 +699,8 @@ f.remove_module('my-custom-module')

## Child instance

A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling `close` removes both child-owned listeners and subscriptions delegated to the parent.

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.
Expand All @@ -722,8 +726,11 @@ Similar to parent SDK, child instances also support several additional methods:

- `set_context`
- `set_sticky`
- `evaluate_flag`
- `is_enabled`
- `evaluate_variation`
- `get_variation`
- `evaluate_variable`
- `get_variable`
- `get_variable_boolean`
- `get_variable_string`
Expand Down Expand Up @@ -821,7 +828,7 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
Install the provider:

```ruby
gem "featurevisor-openfeature", "~> 1.1"
gem "featurevisor-openfeature", "~> 2.0"
```

It installs the matching `featurevisor` gem and the official `openfeature-sdk` dependency. The provider and base SDK deliberately share the same version, and the provider requires that exact Featurevisor version.
Expand Down Expand Up @@ -901,7 +908,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
- Run `bundle install`
- Push commit to `main` branch
- Wait for CI to complete
- Tag the release with the same version number, for example `v1.1.0`
- Tag the release with the same version number, for example `v2.0.0`
- The workflow verifies that the tag matches the shared version
- The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`

Expand Down
2 changes: 1 addition & 1 deletion bin/commands/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ def run_test_segment(assertion, segment, level)

if assertion.key?(:expectedToMatch)
expected_to_match = assertion[:expectedToMatch]
actual = instance.instance_variable_get(:@datafile_reader).all_conditions_are_matched(conditions, context)
actual = instance.instance_variable_get(:@datafile).all_conditions_are_matched(conditions, context)

if actual != expected_to_match
has_error = true
Expand Down
175 changes: 172 additions & 3 deletions conformance/sdk-v3.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": 1,
"version": 2,
"description": "Featurevisor v3 cross SDK compatibility contracts",
"bucketing": {
"minimum": 0,
Expand All @@ -26,7 +26,47 @@
"pattern": "chrome",
"flags": "g",
"values": ["chrome", "chrome", "firefox", "chrome"],
"matches": [true, true, false, true]
"matches": [true, true, false, true],
"portableCases": [
{
"pattern": "^chrome$",
"flags": "",
"value": "chrome",
"expected": true
},
{
"pattern": "^(chrome|firefox)$",
"flags": "i",
"value": "Firefox",
"expected": true
},
{
"pattern": "^second$",
"flags": "m",
"value": "first\nsecond",
"expected": true
},
{
"pattern": "first.*second",
"flags": "s",
"value": "first\nsecond",
"expected": true
},
{
"pattern": "\\(literal\\)",
"flags": "g",
"value": "(literal)",
"expected": true
}
],
"rejectedSyntax": [
"foo(?=bar)",
"(?<=foo)bar",
"(?:foo|bar)",
"(?<name>foo)",
"(foo)\\1",
"foo++"
]
},
"typedVariables": [
{ "type": "integer", "value": 1, "valid": true },
Expand All @@ -44,6 +84,135 @@
"diagnostics": {
"requiredFields": ["level", "code", "message", "details"],
"detailsType": "object",
"emptyDetailsJson": "{}"
"emptyDetailsJson": "{}",
"evaluationDetailFields": ["featureKey", "variableKey", "reason", "evaluation"],
"moduleEnvelopeFields": ["module", "moduleName", "originalError"],
"errorEventLevels": ["error"]
},
"numericBucketKeys": [
{ "value": 1.2345678901234567, "expected": "1.2345678901234567" },
{ "value": 0.30000000000000004, "expected": "0.30000000000000004" },
{ "value": 0.000001, "expected": "0.000001" },
{ "value": 1e-7, "expected": "1e-7" },
{ "value": 100000000000000000000, "expected": "100000000000000000000" },
{ "value": 1e21, "expected": "1e+21" }
],
"portableConditions": {
"regexFlags": ["g", "i", "m", "s"],
"rejectedRegexFlags": ["d", "u", "v", "y"],
"dateFormat": "ISO 8601 with an explicit timezone",
"dates": [
"2024-01-01T00:00:00Z",
"2024-01-01T01:00:00+01:00",
"2024-01-01T00:00:00.250Z",
"2024-01-01T01:00:00.250+01:00"
],
"semanticVersions": [
"1.2.3",
"1.2.3-beta.1",
"1.2.3+build.5"
],
"invalidSemanticVersion": "invalid",
"invalidSemanticVersionDiagnosticCode": "condition_match_error"
},
"conditionCases": [
{
"name": "strict primitive equality",
"condition": {
"attribute": "value",
"operator": "equals",
"value": 1
},
"context": { "value": "1" },
"expected": false
},
{
"name": "not negates implicit and",
"condition": {
"not": [
{ "attribute": "country", "operator": "equals", "value": "us" },
{ "attribute": "device", "operator": "equals", "value": "mobile" }
]
},
"context": { "country": "us", "device": "desktop" },
"expected": true
},
{
"name": "not with nested or means none match",
"condition": {
"not": [
{
"or": [
{ "attribute": "country", "operator": "equals", "value": "us" },
{ "attribute": "country", "operator": "equals", "value": "nl" }
]
}
]
},
"context": { "country": "de" },
"expected": true
},
{
"name": "empty not fails defensively",
"condition": { "not": [] },
"context": {},
"expected": false
},
{
"name": "fractional ISO date with offset",
"condition": {
"attribute": "date",
"operator": "before",
"value": "2024-01-01T00:00:00.500Z"
},
"context": { "date": "2024-01-01T01:00:00.250+01:00" },
"expected": true
}
],
"childInstances": {
"contextModel": "snapshot existing parent keys at spawn, inherit newly introduced parent keys, child keys win",
"closeRemovesLocalAndDelegatedSubscriptions": true,
"detailedEvaluationMethods": ["flag", "variation", "variable"],
"contextCase": {
"parentAtSpawn": { "country": "nl", "plan": "free" },
"child": { "country": "de" },
"parentAfterSpawn": { "country": "us", "plan": "pro", "region": "eu" },
"expected": { "country": "de", "plan": "free", "region": "eu" }
}
},
"defaults": {
"presenceBased": true,
"values": ["", 0, false, null],
"aggregateEvaluationPreservesEmptyVariation": true,
"aggregateCase": {
"datafile": {
"schemaVersion": "2",
"revision": "defaults",
"segments": {},
"features": {
"experiment": {
"key": "experiment",
"bucketBy": "userId",
"variations": [{ "value": "control" }],
"traffic": []
}
}
},
"defaultVariationValue": "",
"expected": {
"enabled": false,
"variation": ""
}
}
},
"diagnosticCase": {
"featureKey": "missing",
"expectedLevel": "warn",
"expectedCode": "feature_not_found",
"detailsMustBeObject": true
},
"nativeContexts": {
"numericTypesUseOneComparisonContract": true,
"primitiveNativeSlicesSupportIncludes": true
}
}
2 changes: 1 addition & 1 deletion gemfiles/base.gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
featurevisor (1.1.0)
featurevisor (2.0.0)
benchmark (>= 0, < 1)

GEM
Expand Down
8 changes: 4 additions & 4 deletions lib/featurevisor.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
require_relative "featurevisor/version"
require_relative "featurevisor/murmurhash"
require_relative "featurevisor/compare_versions"
require_relative "featurevisor/logger"
require_relative "featurevisor/diagnostics"
require_relative "featurevisor/emitter"
require_relative "featurevisor/conditions"
require_relative "featurevisor/datafile_reader"
require_relative "featurevisor/evaluation_data_provider"
require_relative "featurevisor/bucketer"
require_relative "featurevisor/modules"
require_relative "featurevisor/evaluate"
Expand All @@ -15,6 +15,6 @@
module Featurevisor
class Error < StandardError; end

private_constant :DatafileReader
private_constant :Logger
private_constant :InstanceEvaluationDataProvider
private_constant :DiagnosticReporter
end
Loading