flagsmith-nodejs 6.0.1 → 6.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/.github/workflows/conventional-commit.yml +29 -0
- package/.github/workflows/pull_request.yaml +1 -1
- package/.github/workflows/release-please.yml +18 -0
- package/.gitmodules +1 -0
- package/.husky/pre-commit +0 -0
- package/.prettierignore +2 -1
- package/.prettierrc.cjs +9 -1
- package/.release-please-manifest.json +1 -0
- package/CHANGELOG.md +552 -0
- package/CODEOWNERS +1 -0
- package/README.md +2 -3
- package/build/cjs/flagsmith-engine/features/util.js +3 -3
- package/build/cjs/flagsmith-engine/index.d.ts +1 -1
- package/build/cjs/flagsmith-engine/index.js +2 -1
- package/build/cjs/flagsmith-engine/segments/models.js +7 -7
- package/build/cjs/flagsmith-engine/utils/hashing/index.js +1 -1
- package/build/cjs/index.d.ts +4 -4
- package/build/cjs/index.js +3 -1
- package/build/cjs/sdk/analytics.d.ts +1 -1
- package/build/cjs/sdk/analytics.js +3 -1
- package/build/cjs/sdk/index.d.ts +5 -5
- package/build/cjs/sdk/index.js +48 -10
- package/build/cjs/sdk/models.d.ts +25 -0
- package/build/cjs/sdk/models.js +25 -0
- package/build/cjs/sdk/types.d.ts +14 -4
- package/build/cjs/sdk/utils.d.ts +5 -4
- package/build/cjs/sdk/utils.js +16 -3
- package/build/esm/flagsmith-engine/features/models.js +1 -1
- package/build/esm/flagsmith-engine/features/util.js +3 -3
- package/build/esm/flagsmith-engine/index.d.ts +1 -1
- package/build/esm/flagsmith-engine/index.js +1 -1
- package/build/esm/flagsmith-engine/segments/models.js +7 -7
- package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
- package/build/esm/flagsmith-engine/utils/index.js +1 -1
- package/build/esm/index.d.ts +4 -4
- package/build/esm/index.js +3 -3
- package/build/esm/sdk/analytics.d.ts +1 -1
- package/build/esm/sdk/analytics.js +3 -1
- package/build/esm/sdk/index.d.ts +5 -5
- package/build/esm/sdk/index.js +48 -11
- package/build/esm/sdk/models.d.ts +25 -0
- package/build/esm/sdk/models.js +25 -0
- package/build/esm/sdk/types.d.ts +14 -4
- package/build/esm/sdk/utils.d.ts +5 -4
- package/build/esm/sdk/utils.js +14 -2
- package/flagsmith-engine/environments/util.ts +2 -2
- package/flagsmith-engine/features/models.ts +1 -1
- package/flagsmith-engine/features/util.ts +14 -14
- package/flagsmith-engine/identities/models.ts +1 -1
- package/flagsmith-engine/index.ts +1 -1
- package/flagsmith-engine/segments/evaluators.ts +2 -3
- package/flagsmith-engine/segments/models.ts +25 -15
- package/flagsmith-engine/utils/hashing/index.ts +3 -3
- package/flagsmith-engine/utils/index.ts +4 -2
- package/index.ts +19 -22
- package/package.json +1 -1
- package/release-please-config.json +62 -0
- package/sdk/analytics.ts +10 -6
- package/sdk/index.ts +91 -28
- package/sdk/models.ts +25 -0
- package/sdk/offline_handlers.ts +1 -1
- package/sdk/types.ts +17 -8
- package/sdk/utils.ts +21 -8
- package/tests/engine/e2e/engine.test.ts +2 -4
- package/tests/engine/unit/engine.test.ts +1 -6
- package/tests/engine/unit/features/models.test.ts +2 -2
- package/tests/engine/unit/identities/identities_builders.test.ts +1 -1
- package/tests/engine/unit/segments/segment_evaluators.test.ts +52 -23
- package/tests/engine/unit/segments/segments_model.test.ts +35 -37
- package/tests/engine/unit/utils/utils.test.ts +28 -30
- package/tests/sdk/analytics.test.ts +30 -26
- package/tests/sdk/flagsmith-cache.test.ts +84 -76
- package/tests/sdk/flagsmith-environment-flags.test.ts +121 -93
- package/tests/sdk/flagsmith-identity-flags.test.ts +155 -149
- package/tests/sdk/flagsmith.test.ts +202 -43
- package/tests/sdk/offline-handlers.test.ts +32 -32
- package/tests/sdk/polling.test.ts +0 -1
- package/tests/sdk/utils.ts +26 -18
- package/vitest.config.ts +10 -15
package/index.ts
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
AnalyticsProcessor,
|
|
3
|
+
AnalyticsProcessorOptions,
|
|
4
|
+
FlagsmithAPIError,
|
|
5
|
+
FlagsmithClientError,
|
|
6
|
+
EnvironmentDataPollingManager,
|
|
7
|
+
FlagsmithCache,
|
|
8
|
+
BaseFlag,
|
|
9
|
+
DefaultFlag,
|
|
10
|
+
Flags,
|
|
11
|
+
Flagsmith
|
|
11
12
|
} from './sdk/index.js';
|
|
12
13
|
|
|
13
|
-
export {
|
|
14
|
-
BaseOfflineHandler,
|
|
15
|
-
LocalFileHandler,
|
|
16
|
-
} from './sdk/offline_handlers.js';
|
|
14
|
+
export { BaseOfflineHandler, LocalFileHandler } from './sdk/offline_handlers.js';
|
|
17
15
|
|
|
18
|
-
export {
|
|
19
|
-
FlagsmithConfig
|
|
20
|
-
} from './sdk/types.js'
|
|
16
|
+
export { FlagsmithConfig, FlagsmithValue, TraitConfig } from './sdk/types.js';
|
|
21
17
|
|
|
22
18
|
export {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
EnvironmentModel,
|
|
20
|
+
FeatureModel,
|
|
21
|
+
FeatureStateModel,
|
|
22
|
+
IdentityModel,
|
|
23
|
+
TraitModel,
|
|
24
|
+
SegmentModel,
|
|
25
|
+
OrganisationModel
|
|
29
26
|
} from './flagsmith-engine/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.",
|
|
5
5
|
"main": "./build/cjs/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bootstrap-sha": "644c5c883ecbb3786507b50cea01903dc2e533bf",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"release-type": "node",
|
|
6
|
+
"changelog-path": "CHANGELOG.md",
|
|
7
|
+
"bump-minor-pre-major": false,
|
|
8
|
+
"bump-patch-for-minor-pre-major": false,
|
|
9
|
+
"draft": false,
|
|
10
|
+
"prerelease": false,
|
|
11
|
+
"include-component-in-tag": false
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
15
|
+
"changelog-sections": [
|
|
16
|
+
{
|
|
17
|
+
"type": "feat",
|
|
18
|
+
"hidden": false,
|
|
19
|
+
"section": "Features"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"hidden": false,
|
|
24
|
+
"section": "Bug Fixes"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "ci",
|
|
28
|
+
"hidden": false,
|
|
29
|
+
"section": "CI"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "docs",
|
|
33
|
+
"hidden": false,
|
|
34
|
+
"section": "Docs"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "deps",
|
|
38
|
+
"hidden": false,
|
|
39
|
+
"section": "Dependency Updates"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "perf",
|
|
43
|
+
"hidden": false,
|
|
44
|
+
"section": "Performance Improvements"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "refactor",
|
|
48
|
+
"hidden": false,
|
|
49
|
+
"section": "Refactoring"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "test",
|
|
53
|
+
"hidden": false,
|
|
54
|
+
"section": "Tests"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "chore",
|
|
58
|
+
"hidden": false,
|
|
59
|
+
"section": "Other"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
package/sdk/analytics.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { pino, Logger } from 'pino';
|
|
2
|
-
import { Fetch } from
|
|
3
|
-
import { FlagsmithConfig } from
|
|
2
|
+
import { Fetch } from './types.js';
|
|
3
|
+
import { FlagsmithConfig } from './types.js';
|
|
4
|
+
import { getUserAgent } from './utils.js';
|
|
4
5
|
|
|
5
6
|
export const ANALYTICS_ENDPOINT = './analytics/flags/';
|
|
6
7
|
|
|
7
8
|
/** Duration in seconds to wait before trying to flush collected data after {@link trackFeature} is called. **/
|
|
8
9
|
const ANALYTICS_TIMER = 10;
|
|
9
10
|
|
|
10
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 3000
|
|
11
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 3000;
|
|
11
12
|
|
|
12
13
|
export interface AnalyticsProcessorOptions {
|
|
13
14
|
/** URL of the Flagsmith analytics events API endpoint
|
|
@@ -20,7 +21,7 @@ export interface AnalyticsProcessorOptions {
|
|
|
20
21
|
requestTimeoutMs?: number;
|
|
21
22
|
logger?: Logger;
|
|
22
23
|
/** Custom {@link fetch} implementation to use for API requests. **/
|
|
23
|
-
fetch?: Fetch
|
|
24
|
+
fetch?: Fetch;
|
|
24
25
|
|
|
25
26
|
/** @deprecated Use {@link analyticsUrl} instead. **/
|
|
26
27
|
baseApiUrl?: string;
|
|
@@ -69,14 +70,17 @@ export class AnalyticsProcessor {
|
|
|
69
70
|
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
70
71
|
headers: {
|
|
71
72
|
'Content-Type': 'application/json',
|
|
72
|
-
'X-Environment-Key': this.environmentKey
|
|
73
|
+
'X-Environment-Key': this.environmentKey,
|
|
74
|
+
'User-Agent': getUserAgent()
|
|
73
75
|
}
|
|
74
76
|
});
|
|
75
77
|
await this.currentFlush;
|
|
76
78
|
} catch (error) {
|
|
77
79
|
// We don't want failing to write analytics to cause any exceptions in the main
|
|
78
80
|
// thread so we just swallow them here.
|
|
79
|
-
this.logger.warn(
|
|
81
|
+
this.logger.warn(
|
|
82
|
+
'Failed to post analytics to Flagsmith API. Not clearing data, will retry.'
|
|
83
|
+
);
|
|
80
84
|
return;
|
|
81
85
|
} finally {
|
|
82
86
|
this.currentFlush = undefined;
|
package/sdk/index.ts
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
import { Dispatcher } from 'undici-types';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getEnvironmentFeatureStates,
|
|
4
|
+
getIdentityFeatureStates
|
|
5
|
+
} from '../flagsmith-engine/index.js';
|
|
3
6
|
import { EnvironmentModel } from '../flagsmith-engine/index.js';
|
|
4
7
|
import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js';
|
|
5
8
|
import { IdentityModel } from '../flagsmith-engine/index.js';
|
|
6
9
|
import { TraitModel } from '../flagsmith-engine/index.js';
|
|
7
10
|
|
|
8
|
-
import {ANALYTICS_ENDPOINT, AnalyticsProcessor} from './analytics.js';
|
|
11
|
+
import { ANALYTICS_ENDPOINT, AnalyticsProcessor } from './analytics.js';
|
|
9
12
|
import { BaseOfflineHandler } from './offline_handlers.js';
|
|
10
13
|
import { FlagsmithAPIError } from './errors.js';
|
|
11
14
|
|
|
12
15
|
import { DefaultFlag, Flags } from './models.js';
|
|
13
16
|
import { EnvironmentDataPollingManager } from './polling_manager.js';
|
|
14
|
-
import { Deferred, generateIdentitiesData, retryFetch } from './utils.js';
|
|
17
|
+
import { Deferred, generateIdentitiesData, getUserAgent, retryFetch } from './utils.js';
|
|
15
18
|
import { SegmentModel } from '../flagsmith-engine/index.js';
|
|
16
19
|
import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js';
|
|
17
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
Fetch,
|
|
22
|
+
FlagsmithCache,
|
|
23
|
+
FlagsmithConfig,
|
|
24
|
+
FlagsmithTraitValue,
|
|
25
|
+
TraitConfig
|
|
26
|
+
} from './types.js';
|
|
18
27
|
import { pino, Logger } from 'pino';
|
|
19
28
|
|
|
20
29
|
export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js';
|
|
21
30
|
export { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
|
|
22
31
|
|
|
23
|
-
export { DefaultFlag, Flags } from './models.js';
|
|
32
|
+
export { BaseFlag, DefaultFlag, Flags } from './models.js';
|
|
24
33
|
export { EnvironmentDataPollingManager } from './polling_manager.js';
|
|
25
34
|
export { FlagsmithCache, FlagsmithConfig } from './types.js';
|
|
26
35
|
|
|
@@ -50,7 +59,7 @@ const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10;
|
|
|
50
59
|
* const bannerVariation: string = identityFlags.getFeatureValue('banner_flag')
|
|
51
60
|
*
|
|
52
61
|
* @see FlagsmithConfig
|
|
53
|
-
*/
|
|
62
|
+
*/
|
|
54
63
|
export class Flagsmith {
|
|
55
64
|
environmentKey?: string = undefined;
|
|
56
65
|
apiUrl?: string = undefined;
|
|
@@ -128,20 +137,23 @@ export class Flagsmith {
|
|
|
128
137
|
|
|
129
138
|
const apiUrl = data.apiUrl || DEFAULT_API_URL;
|
|
130
139
|
this.apiUrl = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
|
131
|
-
this.analyticsUrl =
|
|
140
|
+
this.analyticsUrl =
|
|
141
|
+
this.analyticsUrl || new URL(ANALYTICS_ENDPOINT, new Request(this.apiUrl).url).href;
|
|
132
142
|
this.environmentFlagsUrl = `${this.apiUrl}flags/`;
|
|
133
143
|
this.identitiesUrl = `${this.apiUrl}identities/`;
|
|
134
144
|
this.environmentUrl = `${this.apiUrl}environment-document/`;
|
|
135
145
|
|
|
136
146
|
if (this.enableLocalEvaluation) {
|
|
137
147
|
if (!this.environmentKey.startsWith('ser.')) {
|
|
138
|
-
throw new Error(
|
|
148
|
+
throw new Error(
|
|
149
|
+
'Using local evaluation requires a server-side environment key'
|
|
150
|
+
);
|
|
139
151
|
}
|
|
140
|
-
if (this.environmentRefreshIntervalSeconds > 0){
|
|
152
|
+
if (this.environmentRefreshIntervalSeconds > 0) {
|
|
141
153
|
this.environmentDataPollingManager = new EnvironmentDataPollingManager(
|
|
142
154
|
this,
|
|
143
155
|
this.environmentRefreshIntervalSeconds,
|
|
144
|
-
this.logger
|
|
156
|
+
this.logger
|
|
145
157
|
);
|
|
146
158
|
this.environmentDataPollingManager.start();
|
|
147
159
|
}
|
|
@@ -152,8 +164,8 @@ export class Flagsmith {
|
|
|
152
164
|
environmentKey: this.environmentKey,
|
|
153
165
|
analyticsUrl: this.analyticsUrl,
|
|
154
166
|
requestTimeoutMs: this.requestTimeoutMs,
|
|
155
|
-
logger: this.logger
|
|
156
|
-
})
|
|
167
|
+
logger: this.logger
|
|
168
|
+
});
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
}
|
|
@@ -174,7 +186,10 @@ export class Flagsmith {
|
|
|
174
186
|
return await this.getEnvironmentFlagsFromApi();
|
|
175
187
|
} catch (error) {
|
|
176
188
|
if (!this.defaultFlagHandler) {
|
|
177
|
-
throw new Error(
|
|
189
|
+
throw new Error(
|
|
190
|
+
'getEnvironmentFlags failed and no default flag handler was provided',
|
|
191
|
+
{ cause: error }
|
|
192
|
+
);
|
|
178
193
|
}
|
|
179
194
|
this.logger.error(error, 'getEnvironmentFlags failed');
|
|
180
195
|
return new Flags({
|
|
@@ -191,13 +206,13 @@ export class Flagsmith {
|
|
|
191
206
|
*
|
|
192
207
|
* @param {string} identifier a unique identifier for the identity in the current
|
|
193
208
|
environment, e.g. email address, username, uuid
|
|
194
|
-
* @param {{[key:string]:any |
|
|
209
|
+
* @param {{[key:string]:any | TraitConfig}} traits? a dictionary of traits to add / update on the identity in
|
|
195
210
|
Flagsmith, e.g. {"num_orders": 10} or {age: {value: 30, transient: true}}
|
|
196
211
|
* @returns Flags object holding all the flags for the given identity.
|
|
197
212
|
*/
|
|
198
213
|
async getIdentityFlags(
|
|
199
214
|
identifier: string,
|
|
200
|
-
traits?: { [key: string]: FlagsmithTraitValue |
|
|
215
|
+
traits?: { [key: string]: FlagsmithTraitValue | TraitConfig },
|
|
201
216
|
transient: boolean = false
|
|
202
217
|
): Promise<Flags> {
|
|
203
218
|
if (!identifier) {
|
|
@@ -216,7 +231,10 @@ export class Flagsmith {
|
|
|
216
231
|
return await this.getIdentityFlagsFromApi(identifier, traits, transient);
|
|
217
232
|
} catch (error) {
|
|
218
233
|
if (!this.defaultFlagHandler) {
|
|
219
|
-
throw new Error(
|
|
234
|
+
throw new Error(
|
|
235
|
+
'getIdentityFlags failed and no default flag handler was provided',
|
|
236
|
+
{ cause: error }
|
|
237
|
+
);
|
|
220
238
|
}
|
|
221
239
|
this.logger.error(error, 'getIdentityFlags failed');
|
|
222
240
|
return new Flags({
|
|
@@ -292,8 +310,8 @@ export class Flagsmith {
|
|
|
292
310
|
async updateEnvironment(): Promise<void> {
|
|
293
311
|
try {
|
|
294
312
|
if (this.environmentPromise) {
|
|
295
|
-
await this.environmentPromise
|
|
296
|
-
return
|
|
313
|
+
await this.environmentPromise;
|
|
314
|
+
return;
|
|
297
315
|
}
|
|
298
316
|
const environment = await this.fetchEnvironment();
|
|
299
317
|
this.onEnvironmentChange(null, environment);
|
|
@@ -311,12 +329,14 @@ export class Flagsmith {
|
|
|
311
329
|
url: string,
|
|
312
330
|
method: string,
|
|
313
331
|
body?: { [key: string]: any }
|
|
314
|
-
): Promise<any> {
|
|
332
|
+
): Promise<{ response: Response; data: any }> {
|
|
315
333
|
const headers: { [key: string]: any } = { 'Content-Type': 'application/json' };
|
|
316
334
|
if (this.environmentKey) {
|
|
317
335
|
headers['X-Environment-Key'] = this.environmentKey as string;
|
|
318
336
|
}
|
|
319
337
|
|
|
338
|
+
headers['User-Agent'] = getUserAgent();
|
|
339
|
+
|
|
320
340
|
if (this.customHeaders) {
|
|
321
341
|
for (const [k, v] of Object.entries(this.customHeaders)) {
|
|
322
342
|
headers[k] = v;
|
|
@@ -334,7 +354,7 @@ export class Flagsmith {
|
|
|
334
354
|
this.retries,
|
|
335
355
|
this.requestTimeoutMs,
|
|
336
356
|
this.requestRetryDelayMilliseconds,
|
|
337
|
-
this.customFetch
|
|
357
|
+
this.customFetch
|
|
338
358
|
);
|
|
339
359
|
|
|
340
360
|
if (data.status !== 200) {
|
|
@@ -343,7 +363,7 @@ export class Flagsmith {
|
|
|
343
363
|
);
|
|
344
364
|
}
|
|
345
365
|
|
|
346
|
-
return data.json();
|
|
366
|
+
return { response: data, data: await data.json() };
|
|
347
367
|
}
|
|
348
368
|
|
|
349
369
|
/**
|
|
@@ -373,8 +393,52 @@ export class Flagsmith {
|
|
|
373
393
|
if (!this.environmentUrl) {
|
|
374
394
|
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
375
395
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
396
|
+
const startTime = Date.now();
|
|
397
|
+
const documents: any[] = [];
|
|
398
|
+
let url = this.environmentUrl;
|
|
399
|
+
let loggedWarning = false;
|
|
400
|
+
|
|
401
|
+
while (true) {
|
|
402
|
+
try {
|
|
403
|
+
if (!loggedWarning) {
|
|
404
|
+
const elapsedMs = Date.now() - startTime;
|
|
405
|
+
if (elapsedMs > this.environmentRefreshIntervalSeconds * 1000) {
|
|
406
|
+
this.logger.warn(
|
|
407
|
+
`Environment document retrieval exceeded the polling interval of ${this.environmentRefreshIntervalSeconds} seconds.`
|
|
408
|
+
);
|
|
409
|
+
loggedWarning = true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const { response, data } = await this.getJSONResponse(url, 'GET');
|
|
414
|
+
|
|
415
|
+
documents.push(data);
|
|
416
|
+
|
|
417
|
+
const linkHeader = response.headers.get('link');
|
|
418
|
+
if (linkHeader) {
|
|
419
|
+
const nextMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/);
|
|
420
|
+
|
|
421
|
+
if (nextMatch) {
|
|
422
|
+
const relativeUrl = decodeURIComponent(nextMatch[1]);
|
|
423
|
+
url = new URL(relativeUrl, this.apiUrl).href;
|
|
424
|
+
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Compile the document
|
|
435
|
+
const compiledDocument = documents[0];
|
|
436
|
+
for (let i = 1; i < documents.length; i++) {
|
|
437
|
+
compiledDocument.identity_overrides = compiledDocument.identity_overrides || [];
|
|
438
|
+
compiledDocument.identity_overrides.push(...(documents[i].identity_overrides || []));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return buildEnvironmentModel(compiledDocument);
|
|
378
442
|
}
|
|
379
443
|
|
|
380
444
|
private async getEnvironmentFlagsFromDocument(): Promise<Flags> {
|
|
@@ -424,7 +488,7 @@ export class Flagsmith {
|
|
|
424
488
|
if (!this.environmentFlagsUrl) {
|
|
425
489
|
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
426
490
|
}
|
|
427
|
-
const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
|
|
491
|
+
const { data: apiFlags } = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
|
|
428
492
|
const flags = Flags.fromAPIFlags({
|
|
429
493
|
apiFlags: apiFlags,
|
|
430
494
|
analyticsProcessor: this.analyticsProcessor,
|
|
@@ -438,14 +502,14 @@ export class Flagsmith {
|
|
|
438
502
|
|
|
439
503
|
private async getIdentityFlagsFromApi(
|
|
440
504
|
identifier: string,
|
|
441
|
-
traits: { [key: string]: FlagsmithTraitValue |
|
|
505
|
+
traits: { [key: string]: FlagsmithTraitValue | TraitConfig },
|
|
442
506
|
transient: boolean = false
|
|
443
507
|
) {
|
|
444
508
|
if (!this.identitiesUrl) {
|
|
445
509
|
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
446
510
|
}
|
|
447
511
|
const data = generateIdentitiesData(identifier, traits, transient);
|
|
448
|
-
const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
|
|
512
|
+
const { data: jsonResponse } = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
|
|
449
513
|
const flags = Flags.fromAPIFlags({
|
|
450
514
|
apiFlags: jsonResponse['flags'],
|
|
451
515
|
analyticsProcessor: this.analyticsProcessor,
|
|
@@ -463,8 +527,7 @@ export class Flagsmith {
|
|
|
463
527
|
traits: { key: string; value: any }[]
|
|
464
528
|
) {
|
|
465
529
|
const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value));
|
|
466
|
-
let identityWithOverrides =
|
|
467
|
-
this.identitiesWithOverridesByIdentifier?.get(identifier);
|
|
530
|
+
let identityWithOverrides = this.identitiesWithOverridesByIdentifier?.get(identifier);
|
|
468
531
|
if (identityWithOverrides) {
|
|
469
532
|
identityWithOverrides.updateTraits(traitModels);
|
|
470
533
|
return identityWithOverrides;
|
package/sdk/models.ts
CHANGED
|
@@ -3,9 +3,21 @@ import { AnalyticsProcessor } from './analytics.js';
|
|
|
3
3
|
|
|
4
4
|
type FlagValue = string | number | boolean | undefined;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A Flagsmith feature. It has an enabled/disabled state, and an optional {@link FlagValue}.
|
|
8
|
+
*/
|
|
6
9
|
export class BaseFlag {
|
|
10
|
+
/**
|
|
11
|
+
* Indicates whether this feature is enabled.
|
|
12
|
+
*/
|
|
7
13
|
enabled: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* An optional {@link FlagValue} for this feature.
|
|
16
|
+
*/
|
|
8
17
|
value: FlagValue;
|
|
18
|
+
/**
|
|
19
|
+
* If true, the state for this feature was determined by a default flag handler. See {@link DefaultFlag}.
|
|
20
|
+
*/
|
|
9
21
|
isDefault: boolean;
|
|
10
22
|
|
|
11
23
|
constructor(value: FlagValue, enabled: boolean, isDefault: boolean) {
|
|
@@ -15,14 +27,27 @@ export class BaseFlag {
|
|
|
15
27
|
}
|
|
16
28
|
}
|
|
17
29
|
|
|
30
|
+
/**
|
|
31
|
+
* A {@link BaseFlag} returned by a default flag handler when flag evaluation fails.
|
|
32
|
+
* @see FlagsmithConfig#defaultFlagHandler
|
|
33
|
+
*/
|
|
18
34
|
export class DefaultFlag extends BaseFlag {
|
|
19
35
|
constructor(value: FlagValue, enabled: boolean) {
|
|
20
36
|
super(value, enabled, true);
|
|
21
37
|
}
|
|
22
38
|
}
|
|
23
39
|
|
|
40
|
+
/**
|
|
41
|
+
* A Flagsmith feature retrieved from a successful flag evaluation.
|
|
42
|
+
*/
|
|
24
43
|
export class Flag extends BaseFlag {
|
|
44
|
+
/**
|
|
45
|
+
* An identifier for this feature, unique in a single Flagsmith installation.
|
|
46
|
+
*/
|
|
25
47
|
featureId: number;
|
|
48
|
+
/**
|
|
49
|
+
* The programmatic name for this feature, unique per Flagsmith project.
|
|
50
|
+
*/
|
|
26
51
|
featureName: string;
|
|
27
52
|
|
|
28
53
|
constructor(params: {
|
package/sdk/offline_handlers.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js'
|
|
|
3
3
|
import { EnvironmentModel } from '../flagsmith-engine/environments/models.js';
|
|
4
4
|
|
|
5
5
|
export class BaseOfflineHandler {
|
|
6
|
-
getEnvironment()
|
|
6
|
+
getEnvironment(): EnvironmentModel {
|
|
7
7
|
throw new Error('Not implemented');
|
|
8
8
|
}
|
|
9
9
|
}
|
package/sdk/types.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { EnvironmentModel } from '../flagsmith-engine/index.js';
|
|
|
3
3
|
import { Dispatcher } from 'undici-types';
|
|
4
4
|
import { Logger } from 'pino';
|
|
5
5
|
import { BaseOfflineHandler } from './offline_handlers.js';
|
|
6
|
-
import { Flagsmith } from './index.js'
|
|
7
|
-
|
|
8
|
-
export type IFlagsmithValue<T = string | number | boolean | null> = T;
|
|
6
|
+
import { Flagsmith } from './index.js';
|
|
9
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A concrete type for the possible values of a feature.
|
|
10
|
+
*/
|
|
11
|
+
export type FlagsmithValue<T = string | number | boolean | null> = T;
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Stores and retrieves {@link Flags} from a cache.
|
|
@@ -26,7 +28,7 @@ export interface FlagsmithCache {
|
|
|
26
28
|
set(key: string, value: Flags): Promise<void>;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
export type Fetch = typeof fetch
|
|
31
|
+
export type Fetch = typeof fetch;
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* The configuration options for a {@link Flagsmith} client.
|
|
@@ -53,7 +55,7 @@ export interface FlagsmithConfig {
|
|
|
53
55
|
/**
|
|
54
56
|
* Custom headers to use in all HTTP requests.
|
|
55
57
|
*/
|
|
56
|
-
customHeaders?: HeadersInit
|
|
58
|
+
customHeaders?: HeadersInit;
|
|
57
59
|
/**
|
|
58
60
|
* The network request timeout duration, in seconds.
|
|
59
61
|
*
|
|
@@ -96,7 +98,7 @@ export interface FlagsmithConfig {
|
|
|
96
98
|
* const defaultHandler = () => new DefaultFlag(undefined, false)
|
|
97
99
|
*
|
|
98
100
|
* // Enable only VIP flags by default
|
|
99
|
-
* const vipDefaultHandler = (key: string) => new
|
|
101
|
+
* const vipDefaultHandler = (key: string) => new DefaultFlag(undefined, key.startsWith('vip_'))
|
|
100
102
|
*/
|
|
101
103
|
defaultFlagHandler?: (flagKey: string) => DefaultFlag;
|
|
102
104
|
cache?: FlagsmithCache;
|
|
@@ -117,9 +119,16 @@ export interface FlagsmithConfig {
|
|
|
117
119
|
offlineHandler?: BaseOfflineHandler;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Represents the configuration for a trait in Flagsmith.
|
|
124
|
+
*
|
|
125
|
+
* @property value The {@link FlagsmithTraitValue} for this trait.
|
|
126
|
+
* @property [transient] Indicates whether the trait should be persisted when used in a remote flag evaluation context.
|
|
127
|
+
* Defaults to false.
|
|
128
|
+
*/
|
|
129
|
+
export interface TraitConfig {
|
|
121
130
|
value: FlagsmithTraitValue;
|
|
122
131
|
transient?: boolean;
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
export declare type FlagsmithTraitValue =
|
|
134
|
+
export declare type FlagsmithTraitValue = FlagsmithValue;
|
package/sdk/utils.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {Fetch, FlagsmithTraitValue,
|
|
2
|
-
import {Dispatcher} from
|
|
1
|
+
import { Fetch, FlagsmithTraitValue, TraitConfig } from './types.js';
|
|
2
|
+
import { Dispatcher } from 'undici-types';
|
|
3
3
|
|
|
4
|
-
type Traits = { [key: string]:
|
|
4
|
+
type Traits = { [key: string]: TraitConfig | FlagsmithTraitValue };
|
|
5
|
+
|
|
6
|
+
const FLAGSMITH_USER_AGENT = 'flagsmith-nodejs-sdk';
|
|
7
|
+
const FLAGSMITH_UNKNOWN_VERSION = 'unknown';
|
|
5
8
|
|
|
6
9
|
export function isTraitConfig(
|
|
7
|
-
traitValue:
|
|
8
|
-
): traitValue is
|
|
10
|
+
traitValue: TraitConfig | FlagsmithTraitValue
|
|
11
|
+
): traitValue is TraitConfig {
|
|
9
12
|
return !!traitValue && typeof traitValue == 'object' && traitValue.value !== undefined;
|
|
10
13
|
}
|
|
11
14
|
|
|
@@ -15,12 +18,12 @@ export function generateIdentitiesData(identifier: string, traits: Traits, trans
|
|
|
15
18
|
return {
|
|
16
19
|
trait_key: key,
|
|
17
20
|
trait_value: value?.value,
|
|
18
|
-
transient: value?.transient
|
|
21
|
+
transient: value?.transient
|
|
19
22
|
};
|
|
20
23
|
} else {
|
|
21
24
|
return {
|
|
22
25
|
trait_key: key,
|
|
23
|
-
trait_value: value
|
|
26
|
+
trait_value: value
|
|
24
27
|
};
|
|
25
28
|
}
|
|
26
29
|
});
|
|
@@ -47,7 +50,7 @@ export const retryFetch = (
|
|
|
47
50
|
retries: number = 3,
|
|
48
51
|
timeoutMs: number = 10, // set an overall timeout for this function
|
|
49
52
|
retryDelayMs: number = 1000,
|
|
50
|
-
customFetch: Fetch
|
|
53
|
+
customFetch: Fetch
|
|
51
54
|
): Promise<Response> => {
|
|
52
55
|
const retryWrapper = async (n: number): Promise<Response> => {
|
|
53
56
|
try {
|
|
@@ -102,3 +105,13 @@ export class Deferred<T> {
|
|
|
102
105
|
this.rejectPromise(reason);
|
|
103
106
|
}
|
|
104
107
|
}
|
|
108
|
+
|
|
109
|
+
export function getUserAgent(): string {
|
|
110
|
+
try {
|
|
111
|
+
const packageJson = require('../package.json');
|
|
112
|
+
const version = packageJson?.version;
|
|
113
|
+
return version ? `${FLAGSMITH_USER_AGENT}/${version}` : FLAGSMITH_UNKNOWN_VERSION;
|
|
114
|
+
} catch {
|
|
115
|
+
return FLAGSMITH_UNKNOWN_VERSION;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -3,11 +3,9 @@ import { EnvironmentModel } from '../../../flagsmith-engine/environments/models.
|
|
|
3
3
|
import { buildEnvironmentModel } from '../../../flagsmith-engine/environments/util.js';
|
|
4
4
|
import { IdentityModel } from '../../../flagsmith-engine/identities/models.js';
|
|
5
5
|
import { buildIdentityModel } from '../../../flagsmith-engine/identities/util.js';
|
|
6
|
-
import * as testData from '../engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json'
|
|
6
|
+
import * as testData from '../engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json';
|
|
7
7
|
|
|
8
|
-
function extractTestCases(
|
|
9
|
-
data: any
|
|
10
|
-
): {
|
|
8
|
+
function extractTestCases(data: any): {
|
|
11
9
|
environment: EnvironmentModel;
|
|
12
10
|
identity: IdentityModel;
|
|
13
11
|
response: any;
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
environmentWithSegmentOverride,
|
|
13
13
|
feature1,
|
|
14
14
|
getEnvironmentFeatureStateForFeature,
|
|
15
|
-
getEnvironmentFeatureStateForFeatureByName,
|
|
16
15
|
identity,
|
|
17
16
|
identityInSegment,
|
|
18
17
|
segmentConditionProperty,
|
|
@@ -73,11 +72,7 @@ test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () =>
|
|
|
73
72
|
const env = environmentWithSegmentOverride();
|
|
74
73
|
env.project.hideDisabledFlags = true;
|
|
75
74
|
|
|
76
|
-
const featureStates = getIdentityFeatureStates(
|
|
77
|
-
env,
|
|
78
|
-
identityInSegment(),
|
|
79
|
-
[trait_models]
|
|
80
|
-
);
|
|
75
|
+
const featureStates = getIdentityFeatureStates(env, identityInSegment(), [trait_models]);
|
|
81
76
|
expect(featureStates.length).toBe(0);
|
|
82
77
|
});
|
|
83
78
|
|
|
@@ -66,11 +66,11 @@ test('test_feature_state_get_value_mv_values', () => {
|
|
|
66
66
|
const mvFeatureState = new FeatureStateModel(myFeature, true, 1);
|
|
67
67
|
mvFeatureState.multivariateFeatureStateValues = [
|
|
68
68
|
mvFeatureStateValue1,
|
|
69
|
-
mvFeatureStateValue2
|
|
69
|
+
mvFeatureStateValue2
|
|
70
70
|
];
|
|
71
71
|
|
|
72
72
|
mvFeatureState.setValue(mvFeatureControlValue);
|
|
73
73
|
|
|
74
|
-
expect(mvFeatureState.getValue(
|
|
74
|
+
expect(mvFeatureState.getValue('test')).toBe(mvFeatureValue2);
|
|
75
75
|
}
|
|
76
76
|
});
|
|
@@ -47,7 +47,7 @@ test('test_build_identity_model_from_dictionary_uses_identity_feature_list_for_i
|
|
|
47
47
|
id: 1,
|
|
48
48
|
identifier: 'test-identity',
|
|
49
49
|
environment_api_key: 'api-key',
|
|
50
|
-
created_date: '2021-08-22T06:25:23.406995Z'
|
|
50
|
+
created_date: '2021-08-22T06:25:23.406995Z'
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const identityModel = buildIdentityModel(identity_dict);
|