@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.
@@ -1,44 +1,67 @@
1
1
  import {
2
2
  EvaluationContext,
3
- Provider,
4
- ResolutionDetails,
5
- ParseError,
6
3
  FlagNotFoundError,
7
4
  JsonValue,
8
- TargetingKeyMissingError,
5
+ OpenFeatureEventEmitter,
6
+ ParseError,
7
+ Provider,
8
+ ProviderEvents,
9
+ ResolutionDetails,
9
10
  StandardResolutionReasons,
10
- } from "@openfeature/js-sdk";
11
- import type SplitIO from "@splitsoftware/splitio/types/splitio";
11
+ TargetingKeyMissingError,
12
+ TrackingEventDetails
13
+ } from '@openfeature/server-sdk';
14
+ import { SplitFactory } from '@splitsoftware/splitio';
15
+ import type SplitIO from '@splitsoftware/splitio/types/splitio';
12
16
 
13
- export interface SplitProviderOptions {
14
- splitClient: SplitIO.IClient;
17
+ type SplitProviderOptions = {
18
+ splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
15
19
  }
16
20
 
17
21
  type Consumer = {
18
- key: string | undefined;
22
+ targetingKey: string | undefined;
23
+ trafficType: string;
19
24
  attributes: SplitIO.Attributes;
20
25
  };
21
26
 
22
- const CONTROL_VALUE_ERROR_MESSAGE = "Received the 'control' value from Split.";
27
+ const CONTROL_VALUE_ERROR_MESSAGE = 'Received the "control" value from Split.';
28
+ const CONTROL_TREATMENT = 'control';
23
29
 
24
30
  export class OpenFeatureSplitProvider implements Provider {
25
31
  metadata = {
26
- name: "split",
32
+ name: 'split',
27
33
  };
28
- private initialized: Promise<void>;
29
- private client: SplitIO.IClient;
30
-
31
- constructor(options: SplitProviderOptions) {
32
- this.client = options.splitClient;
33
- this.initialized = new Promise((resolve) => {
34
- this.client.on(this.client.Event.SDK_READY, () => {
35
- console.log(`${this.metadata.name} provider initialized`);
36
- resolve();
37
- });
38
- });
34
+
35
+ private client: SplitIO.IClient | SplitIO.IAsyncClient;
36
+ private trafficType: string;
37
+ public readonly events = new OpenFeatureEventEmitter();
38
+
39
+ private getSplitClient(options: SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK) {
40
+ if (typeof(options) === 'string') {
41
+ const splitFactory = SplitFactory({core: { authorizationKey: options } });
42
+ return splitFactory.client();
43
+ }
44
+
45
+ let splitClient;
46
+ try {
47
+ splitClient = (options as SplitIO.ISDK | SplitIO.IAsyncSDK).client();
48
+ } catch {
49
+ splitClient = (options as SplitProviderOptions).splitClient
50
+ }
51
+
52
+ return splitClient;
53
+ }
54
+
55
+ constructor(options: SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK) {
56
+ // Asume 'user' as default traffic type'
57
+ this.trafficType = 'user';
58
+ this.client = this.getSplitClient(options);
59
+ this.client.on(this.client.Event.SDK_UPDATE, () => {
60
+ this.events.emit(ProviderEvents.ConfigurationChanged);
61
+ });
39
62
  }
40
63
 
41
- async resolveBooleanEvaluation(
64
+ public async resolveBooleanEvaluation(
42
65
  flagKey: string,
43
66
  _: boolean,
44
67
  context: EvaluationContext
@@ -47,36 +70,20 @@ export class OpenFeatureSplitProvider implements Provider {
47
70
  flagKey,
48
71
  this.transformContext(context)
49
72
  );
73
+ const treatment = details.value.toLowerCase();
74
+
75
+ if ( treatment === 'on' || treatment === 'true' ) {
76
+ return { ...details, value: true };
77
+ }
50
78
 
51
- let value: boolean;
52
- switch (details.value as unknown) {
53
- case "on":
54
- value = true;
55
- break;
56
- case "off":
57
- value = false;
58
- break;
59
- case "true":
60
- value = true;
61
- break;
62
- case "false":
63
- value = false;
64
- break;
65
- case true:
66
- value = true;
67
- break;
68
- case false:
69
- value = false;
70
- break;
71
- case "control":
72
- throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
73
- default:
74
- throw new ParseError(`Invalid boolean value for ${details.value}`);
79
+ if ( treatment === 'off' || treatment === 'false' ) {
80
+ return { ...details, value: false };
75
81
  }
76
- return { ...details, value };
82
+
83
+ throw new ParseError(`Invalid boolean value for ${treatment}`);
77
84
  }
78
85
 
79
- async resolveStringEvaluation(
86
+ public async resolveStringEvaluation(
80
87
  flagKey: string,
81
88
  _: string,
82
89
  context: EvaluationContext
@@ -85,13 +92,10 @@ export class OpenFeatureSplitProvider implements Provider {
85
92
  flagKey,
86
93
  this.transformContext(context)
87
94
  );
88
- if (details.value === "control") {
89
- throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
90
- }
91
95
  return details;
92
96
  }
93
97
 
94
- async resolveNumberEvaluation(
98
+ public async resolveNumberEvaluation(
95
99
  flagKey: string,
96
100
  _: number,
97
101
  context: EvaluationContext
@@ -103,7 +107,7 @@ export class OpenFeatureSplitProvider implements Provider {
103
107
  return { ...details, value: this.parseValidNumber(details.value) };
104
108
  }
105
109
 
106
- async resolveObjectEvaluation<U extends JsonValue>(
110
+ public async resolveObjectEvaluation<U extends JsonValue>(
107
111
  flagKey: string,
108
112
  _: U,
109
113
  context: EvaluationContext
@@ -119,31 +123,82 @@ export class OpenFeatureSplitProvider implements Provider {
119
123
  flagKey: string,
120
124
  consumer: Consumer
121
125
  ): Promise<ResolutionDetails<string>> {
122
- if (!consumer.key) {
126
+ if (!consumer.targetingKey) {
123
127
  throw new TargetingKeyMissingError(
124
- "The Split provider requires a targeting key."
128
+ 'The Split provider requires a targeting key.'
125
129
  );
126
- } else {
127
- await this.initialized;
128
- const value = this.client.getTreatment(
129
- consumer.key,
130
- flagKey,
131
- consumer.attributes
130
+ }
131
+ if (flagKey == null || flagKey === '') {
132
+ throw new FlagNotFoundError(
133
+ 'flagKey must be a non-empty string'
132
134
  );
133
- const details: ResolutionDetails<string> = {
134
- value: value,
135
- variant: value,
136
- reason: StandardResolutionReasons.TARGETING_MATCH,
137
- };
138
- return details;
139
135
  }
136
+
137
+ await new Promise((resolve, reject) => {
138
+ this.readinessHandler(resolve, reject);
139
+ });
140
+
141
+ const { treatment: value, config }: SplitIO.TreatmentWithConfig = await this.client.getTreatmentWithConfig(
142
+ consumer.targetingKey,
143
+ flagKey,
144
+ consumer.attributes
145
+ );
146
+ if (value === CONTROL_TREATMENT) {
147
+ throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
148
+ }
149
+ const flagMetadata = { config: config ? config : '' };
150
+ const details: ResolutionDetails<string> = {
151
+ value: value,
152
+ variant: value,
153
+ flagMetadata: flagMetadata,
154
+ reason: StandardResolutionReasons.TARGETING_MATCH,
155
+ };
156
+ return details;
157
+ }
158
+
159
+ async track(
160
+ trackingEventName: string,
161
+ context: EvaluationContext,
162
+ details: TrackingEventDetails
163
+ ): Promise<void> {
164
+
165
+ // eventName is always required
166
+ if (trackingEventName == null || trackingEventName === '')
167
+ throw new ParseError('Missing eventName, required to track');
168
+
169
+ // targetingKey is always required
170
+ const { targetingKey, trafficType } = this.transformContext(context);
171
+ if (targetingKey == null || targetingKey === '')
172
+ throw new TargetingKeyMissingError('Missing targetingKey, required to track');
173
+
174
+ let value;
175
+ let properties: SplitIO.Properties = {};
176
+ if (details != null) {
177
+ if (details.value != null) {
178
+ value = details.value;
179
+ }
180
+ if (details.properties != null) {
181
+ properties = details.properties as SplitIO.Properties;
182
+ }
183
+ }
184
+
185
+ this.client.track(targetingKey, trafficType, trackingEventName, value, properties);
140
186
  }
141
187
 
142
- //Transform the context into an object useful for the Split API, an key string with arbitrary Split "Attributes".
188
+ public async onClose?(): Promise<void> {
189
+ return this.client.destroy();
190
+ }
191
+
192
+ //Transform the context into an object useful for the Split API, an key string with arbitrary Split 'Attributes'.
143
193
  private transformContext(context: EvaluationContext): Consumer {
144
- const { targetingKey, ...attributes } = context;
194
+ const { targetingKey, trafficType: ttVal, ...attributes } = context;
195
+ const trafficType =
196
+ ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
197
+ ? ttVal
198
+ : this.trafficType;
145
199
  return {
146
- key: targetingKey,
200
+ targetingKey,
201
+ trafficType,
147
202
  // Stringify context objects include date.
148
203
  attributes: JSON.parse(JSON.stringify(attributes)),
149
204
  };
@@ -169,7 +224,7 @@ export class OpenFeatureSplitProvider implements Provider {
169
224
  // we may want to allow the parsing to be customized.
170
225
  try {
171
226
  const value = JSON.parse(stringValue);
172
- if (typeof value !== "object") {
227
+ if (typeof value !== 'object') {
173
228
  throw new ParseError(
174
229
  `Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`
175
230
  );
@@ -179,4 +234,19 @@ export class OpenFeatureSplitProvider implements Provider {
179
234
  throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
180
235
  }
181
236
  }
182
- }
237
+
238
+ private async readinessHandler(onSdkReady: (params?: unknown) => void, onSdkTimedOut: () => void): Promise<void> {
239
+
240
+ const clientStatus = this.client.getStatus();
241
+ if (clientStatus.isReady) {
242
+ onSdkReady();
243
+ } else {
244
+ if (clientStatus.hasTimedout) {
245
+ onSdkTimedOut();
246
+ } else {
247
+ this.client.on(this.client.Event.SDK_READY_TIMED_OUT, onSdkTimedOut);
248
+ }
249
+ this.client.on(this.client.Event.SDK_READY, onSdkReady);
250
+ }
251
+ }
252
+ }
@@ -0,0 +1 @@
1
+ export * from './lib/js-split-provider';
@@ -0,0 +1,27 @@
1
+ import { EvaluationContext, JsonValue, OpenFeatureEventEmitter, Provider, ResolutionDetails, TrackingEventDetails } from '@openfeature/server-sdk';
2
+ import type SplitIO from '@splitsoftware/splitio/types/splitio';
3
+ type SplitProviderOptions = {
4
+ splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
5
+ };
6
+ export declare class OpenFeatureSplitProvider implements Provider {
7
+ metadata: {
8
+ name: string;
9
+ };
10
+ private client;
11
+ private trafficType;
12
+ readonly events: OpenFeatureEventEmitter;
13
+ private getSplitClient;
14
+ constructor(options: SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK);
15
+ resolveBooleanEvaluation(flagKey: string, _: boolean, context: EvaluationContext): Promise<ResolutionDetails<boolean>>;
16
+ resolveStringEvaluation(flagKey: string, _: string, context: EvaluationContext): Promise<ResolutionDetails<string>>;
17
+ resolveNumberEvaluation(flagKey: string, _: number, context: EvaluationContext): Promise<ResolutionDetails<number>>;
18
+ resolveObjectEvaluation<U extends JsonValue>(flagKey: string, _: U, context: EvaluationContext): Promise<ResolutionDetails<U>>;
19
+ private evaluateTreatment;
20
+ track(trackingEventName: string, context: EvaluationContext, details: TrackingEventDetails): Promise<void>;
21
+ onClose?(): Promise<void>;
22
+ private transformContext;
23
+ private parseValidNumber;
24
+ private parseValidJsonObject;
25
+ private readinessHandler;
26
+ }
27
+ export {};
@@ -1,7 +0,0 @@
1
- import tape from 'tape-catch';
2
-
3
- import clientSuite from './nodeSuites/client.spec.js';
4
-
5
- tape('## OpenFeature JavaScript Split Provider - tests', async function (assert) {
6
- assert.test('Client Tests', clientSuite);
7
- });