Skip to content
Open
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
9 changes: 7 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
1.1.0 (February 12, 2026)
- Added ProviderEvents.Ready payload with Split SdkReadyMetadata
- Updated ConfigurationChanged to forward SdkUpdateMetadata in metadata
- Requires @splitsoftware/splitio-browserjs ^1.7.0 for SDK_UPDATE metadata support

1.0.0 (October 1, 2025)
- First release.
- Up to date with @openfeature/web-sdk v1.6.1, and @splitsoftware/splitio-browserjs 1.4.0
- First release.
- Up to date with @openfeature/web-sdk v1.6.1, and @splitsoftware/splitio-browserjs 1.4.0
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ const context: EvaluationContext = {
await OpenFeature.setContext(context)
```

## Configuration changed event (SDK_UPDATE)

When the Split SDK emits the `SDK_UPDATE` **event** (flags or segments changed), the provider emits OpenFeature’s `ConfigurationChanged` and forwards the event metadata. The metadata shape matches [javascript-commons SdkUpdateMetadata](https://github.com/splitio/javascript-commons): `type` is `'FLAGS_UPDATE' | 'SEGMENTS_UPDATE'` and `names` is the list of flag or segment names that were updated. Handlers receive [Provider Event Details](https://openfeature.dev/specification/types#provider-event-details): `flagsChanged` (when `type === 'FLAGS_UPDATE'`, the `names` array) and `metadata` (`type` as string).

Requires `@splitsoftware/splitio-browserjs` **1.7.0 or later** (metadata was added in 1.7.0).

```js
const { OpenFeature, ProviderEvents } = require('@openfeature/web-sdk');

const client = OpenFeature.getClient();
client.addHandler(ProviderEvents.ConfigurationChanged, (eventDetails) => {
console.log('Flags changed:', eventDetails.flagsChanged);
console.log('Event metadata:', eventDetails.metadata);
});

```
## Evaluate with details
Use the get*Details(...) APIs to get the value and rich context (variant, reason, error code, metadata). This provider includes the Split treatment config as a raw JSON string under flagMetadata["config"]

Expand Down
40 changes: 20 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/openfeature-web-split-provider",
"version": "1.0.0",
"version": "1.1.0",
"description": "Split OpenFeature Web Provider",
"files": [
"README.md",
Expand Down Expand Up @@ -32,12 +32,12 @@
},
"peerDependencies": {
"@openfeature/web-sdk": "^1.6.1",
"@splitsoftware/splitio-browserjs": "^1.4.0"
"@splitsoftware/splitio-browserjs": "^1.7.0"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@openfeature/web-sdk": "^1.6.1",
"@splitsoftware/splitio-browserjs": "^1.4.0",
"@splitsoftware/splitio-browserjs": "^1.7.0",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^9.35.0",
Expand Down
64 changes: 64 additions & 0 deletions src/__tests__/context.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { transformContext } from '../lib/context';

describe('context', () => {
describe('transformContext', () => {
const defaultTrafficType = 'user';

test('uses defaultTrafficType when context has no trafficType', () => {
const result = transformContext({ targetingKey: 'key-1' }, defaultTrafficType);
expect(result.trafficType).toBe('user');
expect(result.targetingKey).toBe('key-1');
expect(result.attributes).toEqual({});
});

test('uses context trafficType when present and non-empty', () => {
const result = transformContext(
{ targetingKey: 'key-1', trafficType: 'account' },
defaultTrafficType
);
expect(result.trafficType).toBe('account');
expect(result.targetingKey).toBe('key-1');
expect(result.attributes).toEqual({});
});

test('falls back to default when trafficType is empty string', () => {
const result = transformContext(
{ targetingKey: 'key-1', trafficType: '' },
defaultTrafficType
);
expect(result.trafficType).toBe('user');
});

test('falls back to default when trafficType is whitespace', () => {
const result = transformContext(
{ targetingKey: 'key-1', trafficType: ' ' },
defaultTrafficType
);
expect(result.trafficType).toBe('user');
});

test('passes remaining context as attributes', () => {
const result = transformContext(
{
targetingKey: 'key-1',
trafficType: 'user',
region: 'eu',
plan: 'pro',
},
defaultTrafficType
);
expect(result.attributes).toEqual({ region: 'eu', plan: 'pro' });
});

test('deep-clones attributes (no reference)', () => {
const attrs = { nested: { value: 1 } };
const result = transformContext(
{ targetingKey: 'k', ...attrs },
defaultTrafficType
);
expect(result.attributes).toEqual({ nested: { value: 1 } });
expect(result.attributes).not.toBe(attrs);
expect(result.attributes.nested).not.toBe(attrs.nested);
});
});
});
76 changes: 76 additions & 0 deletions src/__tests__/evaluation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { FlagNotFoundError, StandardResolutionReasons } from '@openfeature/web-sdk';
import { evaluateTreatment } from '../lib/evaluation';
import { CONTROL_TREATMENT } from '../lib/types';

describe('evaluation', () => {
describe('evaluateTreatment', () => {
let mockClient;

beforeEach(() => {
mockClient = {
getTreatmentWithConfig: jest.fn((flagKey, attributes) => ({
treatment: 'v1',
config: '{"x":1}',
})),
};
});

test('returns resolution details with value, variant, flagMetadata, reason', () => {
const consumer = { targetingKey: 'u1', trafficType: 'user', attributes: {} };
const result = evaluateTreatment(mockClient, 'my-flag', consumer);

expect(result.value).toBe('v1');
expect(result.variant).toBe('v1');
expect(result.flagMetadata).toEqual({ config: '{"x":1}' });
expect(result.reason).toBe(StandardResolutionReasons.TARGETING_MATCH);
expect(mockClient.getTreatmentWithConfig).toHaveBeenCalledWith('my-flag', {});
});

test('calls getTreatmentWithConfig with consumer attributes', () => {
const consumer = {
targetingKey: 'u1',
trafficType: 'account',
attributes: { region: 'eu', plan: 'pro' },
};
evaluateTreatment(mockClient, 'flag', consumer);
expect(mockClient.getTreatmentWithConfig).toHaveBeenCalledWith('flag', {
region: 'eu',
plan: 'pro',
});
});

test('uses empty string for config when config is falsy', () => {
mockClient.getTreatmentWithConfig.mockReturnValue({ treatment: 'on', config: null });
const result = evaluateTreatment(mockClient, 'f', {
targetingKey: undefined,
trafficType: 'user',
attributes: {},
});
expect(result.flagMetadata.config).toBe('');
});

test('throws FlagNotFoundError when flagKey is null', () => {
const consumer = { targetingKey: 'u1', trafficType: 'user', attributes: {} };
expect(() => evaluateTreatment(mockClient, null, consumer)).toThrow(FlagNotFoundError);
expect(() => evaluateTreatment(mockClient, null, consumer)).toThrow(
/flagKey must be a non-empty string/
);
});

test('throws FlagNotFoundError when flagKey is empty string', () => {
const consumer = { targetingKey: 'u1', trafficType: 'user', attributes: {} };
expect(() => evaluateTreatment(mockClient, '', consumer)).toThrow(FlagNotFoundError);
});

test('throws FlagNotFoundError when treatment is control', () => {
mockClient.getTreatmentWithConfig.mockReturnValue({
treatment: CONTROL_TREATMENT,
config: '',
});
const consumer = { targetingKey: 'u1', trafficType: 'user', attributes: {} };
expect(() => evaluateTreatment(mockClient, 'flag', consumer)).toThrow(FlagNotFoundError);
expect(() => evaluateTreatment(mockClient, 'flag', consumer)).toThrow(/control/);
});
});
});
Loading
Loading