Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change: raising the minimum supported peer version and dropping compatibility with @splitsoftware/splitio-browserjs 1.4.x–1.6.x.

},
"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