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
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,6 @@ dist
.tern-port

# Build output
es/
lib/
types/
/es/
/lib/
/types/
4 changes: 4 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.3.0 (February 12, 2025)
- ConfigurationChanged event now forwards SDK_UPDATE metadata from Split (flagsChanged, metadata with type and names)
- Requires @splitsoftware/splitio ^11.10.0 for SDK_UPDATE metadata support

1.2.0 (November 7, 2025)
- Updated @openfeature/server-sdk to 1.20.0
- Updated @splitsoftware/splitio to 11.8.0
Expand Down
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-pr

const authorizationKey = 'your auth key'
const provider = new OpenFeatureSplitProvider(authorizationKey);
OpenFeature.setProvider(provider);
await OpenFeature.setProviderAndWait(provider);
const client = OpenFeature.getClient('my-app');
// safe to evaluate
```

### Register the Split provider with OpenFeature using splitFactory
Expand All @@ -42,7 +44,9 @@ const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-pr
const authorizationKey = 'your auth key'
const splitFactory = SplitFactory({core: {authorizationKey}});
const provider = new OpenFeatureSplitProvider(splitFactory);
OpenFeature.setProvider(provider);
await OpenFeature.setProviderAndWait(provider);
const client = OpenFeature.getClient('my-app');
// safe to evaluate
```

### Register the Split provider with OpenFeature using splitClient
Expand All @@ -54,7 +58,9 @@ const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-pr
const authorizationKey = 'your auth key'
const splitClient = SplitFactory({core: {authorizationKey}}).client();
const provider = new OpenFeatureSplitProvider({splitClient});
OpenFeature.setProvider(provider);
await OpenFeature.setProviderAndWait(provider);
const client = OpenFeature.getClient('my-app');
// safe to evaluate
```

## Use of OpenFeature with Split
Expand Down Expand Up @@ -94,6 +100,23 @@ const booleanTreatment = await client.getBooleanDetails('boolFlag', false, conte
const config = booleanTreatment.flagMetadata.config
```

## 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` and `names` as JSON string).

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

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

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

## Tracking

To use track(eventName, context, details) you must provide:
Expand Down
70 changes: 45 additions & 25 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-js-split-provider",
"version": "1.2.0",
"version": "1.3.0",
"description": "Split OpenFeature Provider",
"files": [
"README.md",
Expand Down Expand Up @@ -36,12 +36,12 @@
},
"peerDependencies": {
"@openfeature/server-sdk": "^1.20.0",
"@splitsoftware/splitio": "^11.8.0"
"@splitsoftware/splitio": "^11.10.0"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@openfeature/server-sdk": "^1.20.0",
"@splitsoftware/splitio": "^11.8.0",
"@splitsoftware/splitio": "^11.10.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.3.1",
"copyfiles": "^2.4.1",
Expand Down
19 changes: 17 additions & 2 deletions src/__tests__/nodeSuites/client.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* eslint-disable jest/no-conditional-expect */
import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk';
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';
import { getLocalHostSplitClient, getSplitFactory } from '../testUtils';

import { OpenFeature } from '@openfeature/server-sdk';

const cases = [
[
'openfeature client tests mode: splitClient',
Expand Down Expand Up @@ -119,6 +118,22 @@ describe.each(cases)('%s', (label, getOptions) => {
expect(client.metadata.name).toBe('test');
});

test('Ready event includes Split metadata (readyFromCache, initialCacheLoad)', async () => {
const readyDetails = [];
const testProvider = new OpenFeatureSplitProvider(options);
testProvider.events.addHandler(ProviderEvents.Ready, (details) => readyDetails.push(details));
await OpenFeature.setProviderAndWait(testProvider);
expect(readyDetails.length).toBeGreaterThanOrEqual(1);
const withMetadata = readyDetails.find((d) => d && d.metadata);
expect(withMetadata).toBeDefined();
expect(withMetadata.providerName).toBe('split');
expect(typeof withMetadata.metadata.readyFromCache).toBe('boolean');
expect(typeof withMetadata.metadata.initialCacheLoad).toBe('boolean');
if (withMetadata.metadata.lastUpdateTimestamp != null) {
expect(typeof withMetadata.metadata.lastUpdateTimestamp).toBe('number');
}
});

test('evaluate Boolean without details test', async () => {
let details = await client.getBooleanDetails('some_other_feature', true);
expect(details.flagKey).toBe('some_other_feature');
Expand Down
19 changes: 9 additions & 10 deletions src/__tests__/nodeSuites/client_redis.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,24 @@ const startRedis = () => {
return promise;
};

let redisServer
let splitClient
let redisServer;
let splitClient;
let client;

beforeAll(async () => {
redisServer = await startRedis();
splitClient = getRedisSplitClient(redisPort);
const provider = new OpenFeatureSplitProvider({ splitClient });
await OpenFeature.setProviderAndWait(provider);
client = OpenFeature.getClient();
}, 30000);

afterAll(async () => {
await redisServer.close();
await splitClient.destroy();
if (redisServer) await redisServer.close();
if (splitClient && typeof splitClient.destroy === 'function') await splitClient.destroy();
});

describe('Regular usage - DEBUG strategy', () => {
splitClient = getRedisSplitClient(redisPort);
const provider = new OpenFeatureSplitProvider({ splitClient });

OpenFeature.setProviderAndWait(provider);
const client = OpenFeature.getClient();

test('Evaluate always on flag', async () => {
await client.getBooleanValue('always-on', false, {targetingKey: 'emma-ss'}).then(result => {
expect(result).toBe(true);
Expand Down
73 changes: 73 additions & 0 deletions src/__tests__/nodeSuites/provider.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable jest/no-conditional-expect */
import { ProviderEvents } from '@openfeature/server-sdk';
import { getLocalHostSplitClient, getSplitFactory } from '../testUtils';
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';

Expand Down Expand Up @@ -239,3 +240,75 @@ describe.each(cases)('%s', (label, getOptions) => {
expect(trackSpy).toHaveBeenCalledWith('u1', 'user', 'purchase', 9.99, { plan: 'pro', beta: true });
});
});

describe('provider events metadata', () => {
const SDK_UPDATE = 'state::update';

function createMockSplitClient() {
const listeners = {};
const mock = {
Event: { SDK_UPDATE },
getStatus: jest.fn().mockReturnValue({ isReady: true, hasTimedout: false }),
getTreatmentWithConfig: jest.fn().mockResolvedValue({ treatment: 'on', config: '' }),
on: jest.fn((event, cb) => {
listeners[event] = listeners[event] || [];
listeners[event].push(cb);
return mock;
}),
track: jest.fn(),
destroy: jest.fn(),
_emit(event, payload) {
(listeners[event] || []).forEach((cb) => cb(payload));
},
};
return mock;
}

test('ConfigurationChanged event includes metadata (type, names) and flagsChanged when FLAGS_UPDATE', async () => {
const mockClient = createMockSplitClient();
const provider = new OpenFeatureSplitProvider({ splitClient: mockClient });
const configChangedDetails = [];
provider.events.addHandler(ProviderEvents.ConfigurationChanged, (details) => configChangedDetails.push(details));

mockClient._emit(SDK_UPDATE, { type: 'FLAGS_UPDATE', names: ['flag1', 'flag2'] });

expect(configChangedDetails.length).toBe(1);
expect(configChangedDetails[0].providerName).toBe('split');
expect(configChangedDetails[0].metadata).toEqual({ type: 'FLAGS_UPDATE', names: '["flag1","flag2"]' });
expect(configChangedDetails[0].flagsChanged).toEqual(['flag1', 'flag2']);

await provider.onClose();
});

test('ConfigurationChanged event includes metadata without flagsChanged when SEGMENTS_UPDATE', async () => {
const mockClient = createMockSplitClient();
const provider = new OpenFeatureSplitProvider({ splitClient: mockClient });
const configChangedDetails = [];
provider.events.addHandler(ProviderEvents.ConfigurationChanged, (details) => configChangedDetails.push(details));

mockClient._emit(SDK_UPDATE, { type: 'SEGMENTS_UPDATE', names: ['seg1'] });

expect(configChangedDetails.length).toBe(1);
expect(configChangedDetails[0].providerName).toBe('split');
expect(configChangedDetails[0].metadata).toEqual({ type: 'SEGMENTS_UPDATE', names: '["seg1"]' });
expect(configChangedDetails[0].flagsChanged).toBeUndefined();

await provider.onClose();
});

test('ConfigurationChanged event includes only providerName when SDK_UPDATE payload is undefined', async () => {
const mockClient = createMockSplitClient();
const provider = new OpenFeatureSplitProvider({ splitClient: mockClient });
const configChangedDetails = [];
provider.events.addHandler(ProviderEvents.ConfigurationChanged, (details) => configChangedDetails.push(details));

mockClient._emit(SDK_UPDATE, undefined);

expect(configChangedDetails.length).toBe(1);
expect(configChangedDetails[0].providerName).toBe('split');
expect(configChangedDetails[0].metadata).toBeUndefined();
expect(configChangedDetails[0].flagsChanged).toBeUndefined();

await provider.onClose();
});
});
Loading