@splitsoftware/openfeature-js-split-provider 1.0.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.txt +29 -13
- package/LICENSE +1 -1
- package/README.md +74 -12
- package/es/index.js +1 -0
- package/es/lib/js-split-provider.js +155 -0
- package/lib/index.js +4 -0
- package/lib/lib/js-split-provider.js +159 -0
- package/package.json +37 -29
- package/src/__tests__/mocks/redis-commands.txt +11 -0
- package/src/__tests__/nodeSuites/client.spec.js +181 -139
- package/src/__tests__/nodeSuites/client_redis.spec.js +115 -0
- package/src/__tests__/nodeSuites/provider.spec.js +241 -109
- package/src/__tests__/testUtils/eventSourceMock.js +1 -2
- package/src/__tests__/testUtils/index.js +43 -0
- package/src/lib/js-split-provider.ts +143 -73
- package/types/index.d.ts +1 -0
- package/types/lib/js-split-provider.d.ts +27 -0
- package/src/__tests__/node.spec.js +0 -7
package/CHANGES.txt
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
1.
|
|
2
|
-
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
1.0
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
1
|
+
1.2.0 (November 7, 2025)
|
|
2
|
+
- Updated @openfeature/server-sdk to 1.20.0
|
|
3
|
+
- Updated @splitsoftware/splitio to 11.8.0
|
|
4
|
+
|
|
5
|
+
1.1.0 (September 12, 2025)
|
|
6
|
+
- Updated @openfeature/server-sdk to 1.19.0
|
|
7
|
+
- Updated @splitsoftware/splitio to 11.4.1
|
|
8
|
+
- Added support for tracking feature
|
|
9
|
+
- Added support for evaluate with details feature
|
|
10
|
+
- Added support for provider initialization using splitFactory and apiKey
|
|
11
|
+
- Replace @openfeature/js-sdk with @openfeature/server-sdk
|
|
12
|
+
|
|
12
13
|
1.0.4
|
|
13
|
-
- Fixes issue with TS build
|
|
14
|
-
- Up to date with spec 0.5.0 and @openfeature/js-sdk 0.5.0
|
|
14
|
+
- Fixes issue with TS build
|
|
15
|
+
- Up to date with spec 0.5.0 and @openfeature/js-sdk 0.5.0
|
|
16
|
+
|
|
17
|
+
1.0.3
|
|
18
|
+
- Adds types definitions for TypeScript
|
|
19
|
+
- Up to date with spec 0.4.0 and @openfeature/js-sdk 0.4.0
|
|
20
|
+
|
|
21
|
+
1.0.2
|
|
22
|
+
- Changes name from Node-specific implementation to generic JSON
|
|
23
|
+
- Up to date with spec 0.4.0 and @openfeature/js-sdk 0.4.0
|
|
24
|
+
|
|
25
|
+
1.0.1
|
|
26
|
+
- Fixes issues with flag details and error codes in negative cases, adds unit tests
|
|
27
|
+
- Up to date with spec 0.4.0 and @openfeature/nodejs-sdk v0.3.2
|
|
28
|
+
|
|
29
|
+
1.0.0
|
|
30
|
+
- First release. Up to date with spec 0.4.0, and @openfeature/nodejs-sdk v0.2.0
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
This Provider is designed to allow the use of OpenFeature with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience.
|
|
6
6
|
|
|
7
7
|
## Compatibility
|
|
8
|
+
It supports **Node.js version 14.x or later**.
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
## Getting started
|
|
@@ -19,19 +20,41 @@ npm install @splitsoftware/openfeature-js-split-provider
|
|
|
19
20
|
### Confirm peer dependencies are installed
|
|
20
21
|
```sh
|
|
21
22
|
npm install @splitsoftware/splitio
|
|
22
|
-
npm install @openfeature/
|
|
23
|
+
npm install @openfeature/server-sdk
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
### Register the Split provider with OpenFeature
|
|
26
|
+
### Register the Split provider with OpenFeature using sdk apiKey
|
|
26
27
|
```js
|
|
27
|
-
const OpenFeature = require('@openfeature/
|
|
28
|
+
const OpenFeature = require('@openfeature/server-sdk').OpenFeature;
|
|
29
|
+
const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-provider').OpenFeatureSplitProvider;
|
|
30
|
+
|
|
31
|
+
const authorizationKey = 'your auth key'
|
|
32
|
+
const provider = new OpenFeatureSplitProvider(authorizationKey);
|
|
33
|
+
OpenFeature.setProvider(provider);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Register the Split provider with OpenFeature using splitFactory
|
|
37
|
+
```js
|
|
38
|
+
const OpenFeature = require('@openfeature/server-sdk').OpenFeature;
|
|
39
|
+
const SplitFactory = require('@splitsoftware/splitio').SplitFactory;
|
|
40
|
+
const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-provider').OpenFeatureSplitProvider;
|
|
41
|
+
|
|
42
|
+
const authorizationKey = 'your auth key'
|
|
43
|
+
const splitFactory = SplitFactory({core: {authorizationKey}});
|
|
44
|
+
const provider = new OpenFeatureSplitProvider(splitFactory);
|
|
45
|
+
OpenFeature.setProvider(provider);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Register the Split provider with OpenFeature using splitClient
|
|
49
|
+
```js
|
|
50
|
+
const OpenFeature = require('@openfeature/server-sdk').OpenFeature;
|
|
28
51
|
const SplitFactory = require('@splitsoftware/splitio').SplitFactory;
|
|
29
52
|
const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-js-split-provider').OpenFeatureSplitProvider;
|
|
30
53
|
|
|
31
54
|
const authorizationKey = 'your auth key'
|
|
32
55
|
const splitClient = SplitFactory({core: {authorizationKey}}).client();
|
|
33
56
|
const provider = new OpenFeatureSplitProvider({splitClient});
|
|
34
|
-
|
|
57
|
+
OpenFeature.setProvider(provider);
|
|
35
58
|
```
|
|
36
59
|
|
|
37
60
|
## Use of OpenFeature with Split
|
|
@@ -59,9 +82,40 @@ const context: EvaluationContext = {
|
|
|
59
82
|
targetingKey: 'TARGETING_KEY',
|
|
60
83
|
};
|
|
61
84
|
OpenFeatureAPI.getInstance().setCtx(context)
|
|
62
|
-
|
|
85
|
+
```
|
|
63
86
|
If the context was set at the client or api level, it is not required to provide it during flag evaluation.
|
|
64
87
|
|
|
88
|
+
## Evaluate with details
|
|
89
|
+
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"]
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const booleanTreatment = await client.getBooleanDetails('boolFlag', false, context);
|
|
93
|
+
|
|
94
|
+
const config = booleanTreatment.flagMetadata.config
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Tracking
|
|
98
|
+
|
|
99
|
+
To use track(eventName, context, details) you must provide:
|
|
100
|
+
|
|
101
|
+
- A non-blank `eventName`.
|
|
102
|
+
- A context with:
|
|
103
|
+
- `targetingKey` (non-blank).
|
|
104
|
+
- `trafficType` (string, e.g. "user" or "account").
|
|
105
|
+
|
|
106
|
+
Optional:
|
|
107
|
+
|
|
108
|
+
- details with:
|
|
109
|
+
- `value`: numeric event value (defaults to 0).
|
|
110
|
+
- `properties`: map of attributes (prefer primitives: string/number/boolean/null).
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
```js
|
|
114
|
+
const context = { targetingKey: 'user-123', trafficType: 'account' }
|
|
115
|
+
const details = { value: 19.99, plan: 'pro', coupon: 'WELCOME10' }
|
|
116
|
+
|
|
117
|
+
client.track('checkout.completed', context, details)
|
|
118
|
+
```
|
|
65
119
|
## Submitting issues
|
|
66
120
|
|
|
67
121
|
The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/split-openfeature-provider-nodejs/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner.
|
|
@@ -80,16 +134,24 @@ To learn more about Split, contact hello@split.io, or get started with feature f
|
|
|
80
134
|
|
|
81
135
|
Split has built and maintains SDKs for:
|
|
82
136
|
|
|
83
|
-
* Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK)
|
|
84
|
-
* Javascript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK)
|
|
85
|
-
* Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK)
|
|
86
137
|
* .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK)
|
|
87
|
-
* Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK)
|
|
88
|
-
* PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK)
|
|
89
|
-
* Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK)
|
|
90
|
-
* GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK)
|
|
91
138
|
* Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK)
|
|
139
|
+
* Angular [Github](https://github.com/splitio/angular-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities)
|
|
140
|
+
* Elixir thin-client [Github](https://github.com/splitio/elixir-thin-client) [Docs](https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK)
|
|
141
|
+
* Flutter [Github](https://github.com/splitio/flutter-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/8096158017165-Flutter-plugin)
|
|
142
|
+
* GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK)
|
|
92
143
|
* iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK)
|
|
144
|
+
* Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK)
|
|
145
|
+
* JavaScript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK)
|
|
146
|
+
* JavaScript for Browser [Github](https://github.com/splitio/javascript-browser-client) [Docs](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK)
|
|
147
|
+
* Node.js [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK)
|
|
148
|
+
* PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK)
|
|
149
|
+
* PHP thin-client [Github](https://github.com/splitio/php-thin-client) [Docs](https://help.split.io/hc/en-us/articles/18305128673933-PHP-Thin-Client-SDK)
|
|
150
|
+
* Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK)
|
|
151
|
+
* React [Github](https://github.com/splitio/react-client) [Docs](https://help.split.io/hc/en-us/articles/360038825091-React-SDK)
|
|
152
|
+
* React Native [Github](https://github.com/splitio/react-native-client) [Docs](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK)
|
|
153
|
+
* Redux [Github](https://github.com/splitio/redux-client) [Docs](https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK)
|
|
154
|
+
* Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK)
|
|
93
155
|
|
|
94
156
|
For a comprehensive list of open source projects visit our [Github page](https://github.com/splitio?utf8=%E2%9C%93&query=%20only%3Apublic%20).
|
|
95
157
|
|
package/es/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/js-split-provider.js';
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { FlagNotFoundError, OpenFeatureEventEmitter, ParseError, ProviderEvents, StandardResolutionReasons, TargetingKeyMissingError } from '@openfeature/server-sdk';
|
|
2
|
+
import { SplitFactory } from '@splitsoftware/splitio';
|
|
3
|
+
const CONTROL_VALUE_ERROR_MESSAGE = 'Received the "control" value from Split.';
|
|
4
|
+
const CONTROL_TREATMENT = 'control';
|
|
5
|
+
export class OpenFeatureSplitProvider {
|
|
6
|
+
getSplitClient(options) {
|
|
7
|
+
if (typeof (options) === 'string') {
|
|
8
|
+
const splitFactory = SplitFactory({ core: { authorizationKey: options } });
|
|
9
|
+
return splitFactory.client();
|
|
10
|
+
}
|
|
11
|
+
let splitClient;
|
|
12
|
+
try {
|
|
13
|
+
splitClient = options.client();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
splitClient = options.splitClient;
|
|
17
|
+
}
|
|
18
|
+
return splitClient;
|
|
19
|
+
}
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.metadata = {
|
|
22
|
+
name: 'split',
|
|
23
|
+
};
|
|
24
|
+
this.events = new OpenFeatureEventEmitter();
|
|
25
|
+
// Asume 'user' as default traffic type'
|
|
26
|
+
this.trafficType = 'user';
|
|
27
|
+
this.client = this.getSplitClient(options);
|
|
28
|
+
this.client.on(this.client.Event.SDK_UPDATE, () => {
|
|
29
|
+
this.events.emit(ProviderEvents.ConfigurationChanged);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async resolveBooleanEvaluation(flagKey, _, context) {
|
|
33
|
+
const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
|
|
34
|
+
const treatment = details.value.toLowerCase();
|
|
35
|
+
if (treatment === 'on' || treatment === 'true') {
|
|
36
|
+
return { ...details, value: true };
|
|
37
|
+
}
|
|
38
|
+
if (treatment === 'off' || treatment === 'false') {
|
|
39
|
+
return { ...details, value: false };
|
|
40
|
+
}
|
|
41
|
+
throw new ParseError(`Invalid boolean value for ${treatment}`);
|
|
42
|
+
}
|
|
43
|
+
async resolveStringEvaluation(flagKey, _, context) {
|
|
44
|
+
const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
|
|
45
|
+
return details;
|
|
46
|
+
}
|
|
47
|
+
async resolveNumberEvaluation(flagKey, _, context) {
|
|
48
|
+
const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
|
|
49
|
+
return { ...details, value: this.parseValidNumber(details.value) };
|
|
50
|
+
}
|
|
51
|
+
async resolveObjectEvaluation(flagKey, _, context) {
|
|
52
|
+
const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
|
|
53
|
+
return { ...details, value: this.parseValidJsonObject(details.value) };
|
|
54
|
+
}
|
|
55
|
+
async evaluateTreatment(flagKey, consumer) {
|
|
56
|
+
if (!consumer.targetingKey) {
|
|
57
|
+
throw new TargetingKeyMissingError('The Split provider requires a targeting key.');
|
|
58
|
+
}
|
|
59
|
+
if (flagKey == null || flagKey === '') {
|
|
60
|
+
throw new FlagNotFoundError('flagKey must be a non-empty string');
|
|
61
|
+
}
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
this.readinessHandler(resolve, reject);
|
|
64
|
+
});
|
|
65
|
+
const { treatment: value, config } = await this.client.getTreatmentWithConfig(consumer.targetingKey, flagKey, consumer.attributes);
|
|
66
|
+
if (value === CONTROL_TREATMENT) {
|
|
67
|
+
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
|
|
68
|
+
}
|
|
69
|
+
const flagMetadata = { config: config ? config : '' };
|
|
70
|
+
const details = {
|
|
71
|
+
value: value,
|
|
72
|
+
variant: value,
|
|
73
|
+
flagMetadata: flagMetadata,
|
|
74
|
+
reason: StandardResolutionReasons.TARGETING_MATCH,
|
|
75
|
+
};
|
|
76
|
+
return details;
|
|
77
|
+
}
|
|
78
|
+
async track(trackingEventName, context, details) {
|
|
79
|
+
// eventName is always required
|
|
80
|
+
if (trackingEventName == null || trackingEventName === '')
|
|
81
|
+
throw new ParseError('Missing eventName, required to track');
|
|
82
|
+
// targetingKey is always required
|
|
83
|
+
const { targetingKey, trafficType } = this.transformContext(context);
|
|
84
|
+
if (targetingKey == null || targetingKey === '')
|
|
85
|
+
throw new TargetingKeyMissingError('Missing targetingKey, required to track');
|
|
86
|
+
let value;
|
|
87
|
+
let properties = {};
|
|
88
|
+
if (details != null) {
|
|
89
|
+
if (details.value != null) {
|
|
90
|
+
value = details.value;
|
|
91
|
+
}
|
|
92
|
+
if (details.properties != null) {
|
|
93
|
+
properties = details.properties;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.client.track(targetingKey, trafficType, trackingEventName, value, properties);
|
|
97
|
+
}
|
|
98
|
+
async onClose() {
|
|
99
|
+
return this.client.destroy();
|
|
100
|
+
}
|
|
101
|
+
//Transform the context into an object useful for the Split API, an key string with arbitrary Split 'Attributes'.
|
|
102
|
+
transformContext(context) {
|
|
103
|
+
const { targetingKey, trafficType: ttVal, ...attributes } = context;
|
|
104
|
+
const trafficType = ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
|
|
105
|
+
? ttVal
|
|
106
|
+
: this.trafficType;
|
|
107
|
+
return {
|
|
108
|
+
targetingKey,
|
|
109
|
+
trafficType,
|
|
110
|
+
// Stringify context objects include date.
|
|
111
|
+
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
parseValidNumber(stringValue) {
|
|
115
|
+
if (stringValue === undefined) {
|
|
116
|
+
throw new ParseError(`Invalid 'undefined' value.`);
|
|
117
|
+
}
|
|
118
|
+
const result = Number.parseFloat(stringValue);
|
|
119
|
+
if (Number.isNaN(result)) {
|
|
120
|
+
throw new ParseError(`Invalid numeric value ${stringValue}`);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
parseValidJsonObject(stringValue) {
|
|
125
|
+
if (stringValue === undefined) {
|
|
126
|
+
throw new ParseError(`Invalid 'undefined' JSON value.`);
|
|
127
|
+
}
|
|
128
|
+
// we may want to allow the parsing to be customized.
|
|
129
|
+
try {
|
|
130
|
+
const value = JSON.parse(stringValue);
|
|
131
|
+
if (typeof value !== 'object') {
|
|
132
|
+
throw new ParseError(`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async readinessHandler(onSdkReady, onSdkTimedOut) {
|
|
141
|
+
const clientStatus = this.client.getStatus();
|
|
142
|
+
if (clientStatus.isReady) {
|
|
143
|
+
onSdkReady();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
if (clientStatus.hasTimedout) {
|
|
147
|
+
onSdkTimedOut();
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.client.on(this.client.Event.SDK_READY_TIMED_OUT, onSdkTimedOut);
|
|
151
|
+
}
|
|
152
|
+
this.client.on(this.client.Event.SDK_READY, onSdkReady);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenFeatureSplitProvider = void 0;
|
|
4
|
+
const server_sdk_1 = require("@openfeature/server-sdk");
|
|
5
|
+
const splitio_1 = require("@splitsoftware/splitio");
|
|
6
|
+
const CONTROL_VALUE_ERROR_MESSAGE = 'Received the "control" value from Split.';
|
|
7
|
+
const CONTROL_TREATMENT = 'control';
|
|
8
|
+
class OpenFeatureSplitProvider {
|
|
9
|
+
getSplitClient(options) {
|
|
10
|
+
if (typeof (options) === 'string') {
|
|
11
|
+
const splitFactory = (0, splitio_1.SplitFactory)({ core: { authorizationKey: options } });
|
|
12
|
+
return splitFactory.client();
|
|
13
|
+
}
|
|
14
|
+
let splitClient;
|
|
15
|
+
try {
|
|
16
|
+
splitClient = options.client();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
splitClient = options.splitClient;
|
|
20
|
+
}
|
|
21
|
+
return splitClient;
|
|
22
|
+
}
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.metadata = {
|
|
25
|
+
name: 'split',
|
|
26
|
+
};
|
|
27
|
+
this.events = new server_sdk_1.OpenFeatureEventEmitter();
|
|
28
|
+
// Asume 'user' as default traffic type'
|
|
29
|
+
this.trafficType = 'user';
|
|
30
|
+
this.client = this.getSplitClient(options);
|
|
31
|
+
this.client.on(this.client.Event.SDK_UPDATE, () => {
|
|
32
|
+
this.events.emit(server_sdk_1.ProviderEvents.ConfigurationChanged);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async resolveBooleanEvaluation(flagKey, _, context) {
|
|
36
|
+
const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
|
|
37
|
+
const treatment = details.value.toLowerCase();
|
|
38
|
+
if (treatment === 'on' || treatment === 'true') {
|
|
39
|
+
return { ...details, value: true };
|
|
40
|
+
}
|
|
41
|
+
if (treatment === 'off' || treatment === 'false') {
|
|
42
|
+
return { ...details, value: false };
|
|
43
|
+
}
|
|
44
|
+
throw new server_sdk_1.ParseError(`Invalid boolean value for ${treatment}`);
|
|
45
|
+
}
|
|
46
|
+
async resolveStringEvaluation(flagKey, _, context) {
|
|
47
|
+
const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
|
|
48
|
+
return details;
|
|
49
|
+
}
|
|
50
|
+
async resolveNumberEvaluation(flagKey, _, context) {
|
|
51
|
+
const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
|
|
52
|
+
return { ...details, value: this.parseValidNumber(details.value) };
|
|
53
|
+
}
|
|
54
|
+
async resolveObjectEvaluation(flagKey, _, context) {
|
|
55
|
+
const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
|
|
56
|
+
return { ...details, value: this.parseValidJsonObject(details.value) };
|
|
57
|
+
}
|
|
58
|
+
async evaluateTreatment(flagKey, consumer) {
|
|
59
|
+
if (!consumer.targetingKey) {
|
|
60
|
+
throw new server_sdk_1.TargetingKeyMissingError('The Split provider requires a targeting key.');
|
|
61
|
+
}
|
|
62
|
+
if (flagKey == null || flagKey === '') {
|
|
63
|
+
throw new server_sdk_1.FlagNotFoundError('flagKey must be a non-empty string');
|
|
64
|
+
}
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
this.readinessHandler(resolve, reject);
|
|
67
|
+
});
|
|
68
|
+
const { treatment: value, config } = await this.client.getTreatmentWithConfig(consumer.targetingKey, flagKey, consumer.attributes);
|
|
69
|
+
if (value === CONTROL_TREATMENT) {
|
|
70
|
+
throw new server_sdk_1.FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
|
|
71
|
+
}
|
|
72
|
+
const flagMetadata = { config: config ? config : '' };
|
|
73
|
+
const details = {
|
|
74
|
+
value: value,
|
|
75
|
+
variant: value,
|
|
76
|
+
flagMetadata: flagMetadata,
|
|
77
|
+
reason: server_sdk_1.StandardResolutionReasons.TARGETING_MATCH,
|
|
78
|
+
};
|
|
79
|
+
return details;
|
|
80
|
+
}
|
|
81
|
+
async track(trackingEventName, context, details) {
|
|
82
|
+
// eventName is always required
|
|
83
|
+
if (trackingEventName == null || trackingEventName === '')
|
|
84
|
+
throw new server_sdk_1.ParseError('Missing eventName, required to track');
|
|
85
|
+
// targetingKey is always required
|
|
86
|
+
const { targetingKey, trafficType } = this.transformContext(context);
|
|
87
|
+
if (targetingKey == null || targetingKey === '')
|
|
88
|
+
throw new server_sdk_1.TargetingKeyMissingError('Missing targetingKey, required to track');
|
|
89
|
+
let value;
|
|
90
|
+
let properties = {};
|
|
91
|
+
if (details != null) {
|
|
92
|
+
if (details.value != null) {
|
|
93
|
+
value = details.value;
|
|
94
|
+
}
|
|
95
|
+
if (details.properties != null) {
|
|
96
|
+
properties = details.properties;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.client.track(targetingKey, trafficType, trackingEventName, value, properties);
|
|
100
|
+
}
|
|
101
|
+
async onClose() {
|
|
102
|
+
return this.client.destroy();
|
|
103
|
+
}
|
|
104
|
+
//Transform the context into an object useful for the Split API, an key string with arbitrary Split 'Attributes'.
|
|
105
|
+
transformContext(context) {
|
|
106
|
+
const { targetingKey, trafficType: ttVal, ...attributes } = context;
|
|
107
|
+
const trafficType = ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
|
|
108
|
+
? ttVal
|
|
109
|
+
: this.trafficType;
|
|
110
|
+
return {
|
|
111
|
+
targetingKey,
|
|
112
|
+
trafficType,
|
|
113
|
+
// Stringify context objects include date.
|
|
114
|
+
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
parseValidNumber(stringValue) {
|
|
118
|
+
if (stringValue === undefined) {
|
|
119
|
+
throw new server_sdk_1.ParseError(`Invalid 'undefined' value.`);
|
|
120
|
+
}
|
|
121
|
+
const result = Number.parseFloat(stringValue);
|
|
122
|
+
if (Number.isNaN(result)) {
|
|
123
|
+
throw new server_sdk_1.ParseError(`Invalid numeric value ${stringValue}`);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
parseValidJsonObject(stringValue) {
|
|
128
|
+
if (stringValue === undefined) {
|
|
129
|
+
throw new server_sdk_1.ParseError(`Invalid 'undefined' JSON value.`);
|
|
130
|
+
}
|
|
131
|
+
// we may want to allow the parsing to be customized.
|
|
132
|
+
try {
|
|
133
|
+
const value = JSON.parse(stringValue);
|
|
134
|
+
if (typeof value !== 'object') {
|
|
135
|
+
throw new server_sdk_1.ParseError(`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`);
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
throw new server_sdk_1.ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async readinessHandler(onSdkReady, onSdkTimedOut) {
|
|
144
|
+
const clientStatus = this.client.getStatus();
|
|
145
|
+
if (clientStatus.isReady) {
|
|
146
|
+
onSdkReady();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (clientStatus.hasTimedout) {
|
|
150
|
+
onSdkTimedOut();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.client.on(this.client.Event.SDK_READY_TIMED_OUT, onSdkTimedOut);
|
|
154
|
+
}
|
|
155
|
+
this.client.on(this.client.Event.SDK_READY, onSdkReady);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.OpenFeatureSplitProvider = OpenFeatureSplitProvider;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@splitsoftware/openfeature-js-split-provider",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Split OpenFeature Provider",
|
|
5
5
|
"files": [
|
|
6
6
|
"README.md",
|
|
@@ -17,29 +17,47 @@
|
|
|
17
17
|
"bugs": "https://github.com/splitio/openfeature-split-provider-js/issues",
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"author": "Josh Sirota <josh.sirota@split.io>",
|
|
20
|
-
"
|
|
21
|
-
|
|
20
|
+
"contributors": [
|
|
21
|
+
"Nicolas Zelaya <nicolas.zelaya@harness.io> (https://github.com/NicoZelaya)",
|
|
22
|
+
"Emiliano Sanchez <emiliano.sanchez@harness.io> (https://github.com/EmilianoSanchez)",
|
|
23
|
+
"Emmanuel Zamora <emmanuel.zamora@harness.io> (https://github.com/ZamoraEmmanuel)",
|
|
24
|
+
"SDK Team <sdks@harness.io>"
|
|
25
|
+
],
|
|
26
|
+
"main": "lib/index.js",
|
|
27
|
+
"types": "types/index.d.ts",
|
|
22
28
|
"engines": {
|
|
23
|
-
"
|
|
24
|
-
|
|
29
|
+
"node": ">=14"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"require": "./lib/index.js",
|
|
34
|
+
"import": "./es/index.js"
|
|
35
|
+
}
|
|
25
36
|
},
|
|
26
|
-
"dependencies": {},
|
|
27
37
|
"peerDependencies": {
|
|
28
|
-
"@openfeature/
|
|
29
|
-
"@splitsoftware/splitio": "^
|
|
38
|
+
"@openfeature/server-sdk": "^1.20.0",
|
|
39
|
+
"@splitsoftware/splitio": "^11.8.0"
|
|
30
40
|
},
|
|
31
41
|
"devDependencies": {
|
|
32
|
-
"@
|
|
33
|
-
"@
|
|
42
|
+
"@eslint/js": "^9.35.0",
|
|
43
|
+
"@openfeature/server-sdk": "^1.20.0",
|
|
44
|
+
"@splitsoftware/splitio": "^11.8.0",
|
|
45
|
+
"@types/jest": "^30.0.0",
|
|
46
|
+
"@types/node": "^24.3.1",
|
|
34
47
|
"copyfiles": "^2.4.1",
|
|
35
48
|
"cross-env": "^7.0.3",
|
|
49
|
+
"eslint": "^9.35.0",
|
|
50
|
+
"eslint-plugin-jest": "^28.14.0",
|
|
51
|
+
"globals": "^16.3.0",
|
|
52
|
+
"jest": "^29.7.0",
|
|
53
|
+
"jiti": "^2.5.1",
|
|
54
|
+
"redis-server": "^1.2.2",
|
|
36
55
|
"replace": "^1.2.1",
|
|
37
56
|
"rimraf": "^3.0.2",
|
|
38
|
-
"
|
|
39
|
-
"tape": "4.13.2",
|
|
40
|
-
"tape-catch": "1.0.6",
|
|
57
|
+
"ts-jest": "^29.4.1",
|
|
41
58
|
"ts-node": "^10.5.0",
|
|
42
|
-
"typescript": "4.
|
|
59
|
+
"typescript": "^4.9.5",
|
|
60
|
+
"typescript-eslint": "^8.43.0"
|
|
43
61
|
},
|
|
44
62
|
"scripts": {
|
|
45
63
|
"build-esm": "rimraf es && tsc -outDir es",
|
|
@@ -47,20 +65,10 @@
|
|
|
47
65
|
"build-cjs": "rimraf lib && tsc -outDir lib -m CommonJS",
|
|
48
66
|
"postbuild-cjs": "cross-env NODE_ENV=cjs node scripts/copy.packages.json.js && ./scripts/build_cjs_replace_imports.sh",
|
|
49
67
|
"build": "rimraf lib es && npm run build-cjs && npm run build-esm",
|
|
50
|
-
"check": "npm run check:
|
|
51
|
-
"check:
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"test": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/node.spec.js | tap-min",
|
|
56
|
-
"publish:rc": "npm run check && npm run build && npm publish --tag canary",
|
|
57
|
-
"publish:stable": "npm run check && npm run build && npm publish"
|
|
58
|
-
},
|
|
59
|
-
"greenkeeper": {
|
|
60
|
-
"ignore": [
|
|
61
|
-
"karma",
|
|
62
|
-
"karma-tap",
|
|
63
|
-
"karma-webpack"
|
|
64
|
-
]
|
|
68
|
+
"check": "npm run check:lint",
|
|
69
|
+
"check:lint": "eslint src",
|
|
70
|
+
"test": "cross-env NODE_ENV=test jest",
|
|
71
|
+
"publish:rc": "npm run check && npm run test && npm run build && npm publish --tag rc",
|
|
72
|
+
"publish:stable": "npm run check && npm run test && npm run build && npm publish"
|
|
65
73
|
}
|
|
66
74
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FLUSHDB
|
|
2
|
+
DEL 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT'
|
|
3
|
+
SADD 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT' UT_Segment_member
|
|
4
|
+
SET 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT.till' 1492721958710
|
|
5
|
+
SET 'REDIS_NODE_UT.SPLITIO.split.UT_IN_SEGMENT' '{"changeNumber":1492722104980,"trafficTypeName":"machine","name":"UT_IN_SEGMENT","seed":-202209840,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"","attribute":""},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"UT_SEGMENT"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100}],"label":"whitelisted segment"},{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment all"}]}'
|
|
6
|
+
SET 'REDIS_NODE_UT.SPLITIO.split.UT_NOT_IN_SEGMENT' '{"changeNumber":1492722747908,"trafficTypeName":"machine","name":"UT_NOT_IN_SEGMENT","seed":-56653132,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":""},"matcherType":"IN_SEGMENT","negate":true,"userDefinedSegmentMatcherData":{"segmentName":"UT_SEGMENT"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"not in segment UT_SEGMENT"}]}'
|
|
7
|
+
SET 'REDIS_NODE_UT.SPLITIO.split.UT_NOT_SET_MATCHER' '{"changeNumber":1492723024413,"trafficTypeName":"machine","name":"UT_NOT_SET_MATCHER","seed":-93553840,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":"permissions"},"matcherType":"CONTAINS_ANY_OF_SET","negate":true,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["create","delete","update"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"permissions does not contain any of [create, delete, ...]"}]}'
|
|
8
|
+
SET 'REDIS_NODE_UT.SPLITIO.split.UT_SET_MATCHER' '{"changeNumber":1492722926004,"trafficTypeName":"machine","name":"UT_SET_MATCHER","seed":-1995997836,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":"permissions"},"matcherType":"CONTAINS_ANY_OF_SET","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["admin","premium","idol"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"permissions contains any of [admin, premium, ...]"}]}'
|
|
9
|
+
SET 'REDIS_NODE_UT.SPLITIO.split.always-on' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-on","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}'
|
|
10
|
+
SET 'REDIS_NODE_UT.SPLITIO.split.always-o.n-with-config' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-o.n-with-config","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"o.n","size":100},{"treatment":"off","size":0}],"label":"in segment all"}],"configurations":{"o.n":"{\"color\":\"brown\"}"}}'
|
|
11
|
+
SET 'REDIS_NODE_UT.SPLITIO.splits.till' 1492723024413
|