Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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)
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.

I would use the convention - Updated ...

something like, - Updated ConfigurationChanged event to forward SDK_UPDATE ...

- 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