flagsmith-nodejs 3.3.0 → 3.3.2
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/build/flagsmith-engine/segments/models.js +6 -3
- package/build/sdk/analytics.d.ts +1 -0
- package/build/sdk/analytics.js +16 -12
- package/flagsmith-engine/segments/models.ts +5 -3
- package/package.json +1 -1
- package/sdk/analytics.ts +7 -3
- package/tests/engine/unit/segments/segments_model.test.ts +5 -2
- package/tests/sdk/analytics.test.ts +18 -0
|
@@ -52,7 +52,7 @@ exports.matchingFunctions = (_a = {},
|
|
|
52
52
|
},
|
|
53
53
|
_a[constants_1.CONDITION_OPERATORS.NOT_EQUAL] = function (thisValue, otherValue) { return thisValue != otherValue; },
|
|
54
54
|
_a[constants_1.CONDITION_OPERATORS.CONTAINS] = function (thisValue, otherValue) {
|
|
55
|
-
return otherValue.includes(thisValue);
|
|
55
|
+
return !!otherValue && otherValue.includes(thisValue);
|
|
56
56
|
},
|
|
57
57
|
_a);
|
|
58
58
|
exports.semverMatchingFunction = __assign(__assign({}, exports.matchingFunctions), (_b = {}, _b[constants_1.CONDITION_OPERATORS.EQUAL] = function (thisValue, otherValue) { return semver_1.default.eq(thisValue, otherValue); }, _b[constants_1.CONDITION_OPERATORS.GREATER_THAN] = function (thisValue, otherValue) { return semver_1.default.gt(otherValue, thisValue); }, _b[constants_1.CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE] = function (thisValue, otherValue) {
|
|
@@ -79,10 +79,13 @@ var SegmentConditionModel = /** @class */ (function () {
|
|
|
79
79
|
var _this = this;
|
|
80
80
|
var evaluators = {
|
|
81
81
|
evaluateNotContains: function (traitValue) {
|
|
82
|
-
|
|
82
|
+
var _a;
|
|
83
|
+
return typeof traitValue == "string" &&
|
|
84
|
+
!!_this.value &&
|
|
85
|
+
!traitValue.includes((_a = _this.value) === null || _a === void 0 ? void 0 : _a.toString());
|
|
83
86
|
},
|
|
84
87
|
evaluateRegex: function (traitValue) {
|
|
85
|
-
return !!_this.value && !!traitValue.match(new RegExp(_this.value));
|
|
88
|
+
return !!_this.value && !!(traitValue === null || traitValue === void 0 ? void 0 : traitValue.toString().match(new RegExp(_this.value)));
|
|
86
89
|
},
|
|
87
90
|
evaluateModulo: function (traitValue) {
|
|
88
91
|
if (isNaN(parseFloat(traitValue)) || !_this.value) {
|
package/build/sdk/analytics.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare class AnalyticsProcessor {
|
|
|
8
8
|
};
|
|
9
9
|
private requestTimeoutMs;
|
|
10
10
|
private logger;
|
|
11
|
+
private currentFlush;
|
|
11
12
|
/**
|
|
12
13
|
* AnalyticsProcessor is used to track how often individual Flags are evaluated within
|
|
13
14
|
* the Flagsmith SDK. Docs: https://docs.flagsmith.com/advanced-use/flag-analytics.
|
package/build/sdk/analytics.js
CHANGED
|
@@ -73,24 +73,25 @@ var AnalyticsProcessor = /** @class */ (function () {
|
|
|
73
73
|
return __generator(this, function (_a) {
|
|
74
74
|
switch (_a.label) {
|
|
75
75
|
case 0:
|
|
76
|
-
if (!Object.keys(this.analyticsData).length) {
|
|
76
|
+
if (this.currentFlush || !Object.keys(this.analyticsData).length) {
|
|
77
77
|
return [2 /*return*/];
|
|
78
78
|
}
|
|
79
79
|
_a.label = 1;
|
|
80
80
|
case 1:
|
|
81
|
-
_a.trys.push([1, 3, ,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
_a.trys.push([1, 3, 4, 5]);
|
|
82
|
+
this.currentFlush = (0, node_fetch_1.default)(this.analyticsEndpoint, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
body: JSON.stringify(this.analyticsData),
|
|
85
|
+
timeout: this.requestTimeoutMs,
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
'X-Environment-Key': this.environmentKey
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return [4 /*yield*/, this.currentFlush];
|
|
91
92
|
case 2:
|
|
92
93
|
_a.sent();
|
|
93
|
-
return [3 /*break*/,
|
|
94
|
+
return [3 /*break*/, 5];
|
|
94
95
|
case 3:
|
|
95
96
|
error_1 = _a.sent();
|
|
96
97
|
// We don't want failing to write analytics to cause any exceptions in the main
|
|
@@ -98,6 +99,9 @@ var AnalyticsProcessor = /** @class */ (function () {
|
|
|
98
99
|
this.logger.warn('Failed to post analytics to Flagsmith API. Not clearing data, will retry.');
|
|
99
100
|
return [2 /*return*/];
|
|
100
101
|
case 4:
|
|
102
|
+
this.currentFlush = undefined;
|
|
103
|
+
return [7 /*endfinally*/];
|
|
104
|
+
case 5:
|
|
101
105
|
this.analyticsData = {};
|
|
102
106
|
this.lastFlushed = Date.now();
|
|
103
107
|
return [2 /*return*/];
|
|
@@ -27,7 +27,7 @@ export const matchingFunctions = {
|
|
|
27
27
|
thisValue >= otherValue,
|
|
28
28
|
[CONDITION_OPERATORS.NOT_EQUAL]: (thisValue: any, otherValue: any) => thisValue != otherValue,
|
|
29
29
|
[CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) =>
|
|
30
|
-
otherValue.includes(thisValue),
|
|
30
|
+
!!otherValue && otherValue.includes(thisValue),
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export const semverMatchingFunction = {
|
|
@@ -64,10 +64,12 @@ export class SegmentConditionModel {
|
|
|
64
64
|
matchesTraitValue(traitValue: any) {
|
|
65
65
|
const evaluators: { [key: string]: CallableFunction } = {
|
|
66
66
|
evaluateNotContains: (traitValue: any) => {
|
|
67
|
-
return
|
|
67
|
+
return typeof traitValue == "string" &&
|
|
68
|
+
!!this.value &&
|
|
69
|
+
!traitValue.includes(this.value?.toString());
|
|
68
70
|
},
|
|
69
71
|
evaluateRegex: (traitValue: any) => {
|
|
70
|
-
return !!this.value && !!traitValue.match(new RegExp(this.value));
|
|
72
|
+
return !!this.value && !!traitValue?.toString().match(new RegExp(this.value));
|
|
71
73
|
},
|
|
72
74
|
evaluateModulo: (traitValue: any) => {
|
|
73
75
|
if (isNaN(parseFloat(traitValue)) || !this.value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
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/index.js",
|
|
6
6
|
"repository": {
|
package/sdk/analytics.ts
CHANGED
|
@@ -13,6 +13,7 @@ export class AnalyticsProcessor {
|
|
|
13
13
|
analyticsData: { [key: string]: any };
|
|
14
14
|
private requestTimeoutMs: number = 3000;
|
|
15
15
|
private logger: Logger;
|
|
16
|
+
private currentFlush: ReturnType<typeof fetch> | undefined;
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* AnalyticsProcessor is used to track how often individual Flags are evaluated within
|
|
@@ -35,12 +36,12 @@ export class AnalyticsProcessor {
|
|
|
35
36
|
* Sends all the collected data to the api asynchronously and resets the timer
|
|
36
37
|
*/
|
|
37
38
|
async flush() {
|
|
38
|
-
if (!Object.keys(this.analyticsData).length) {
|
|
39
|
+
if (this.currentFlush || !Object.keys(this.analyticsData).length) {
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
try {
|
|
43
|
-
|
|
44
|
+
this.currentFlush = fetch(this.analyticsEndpoint, {
|
|
44
45
|
method: 'POST',
|
|
45
46
|
body: JSON.stringify(this.analyticsData),
|
|
46
47
|
timeout: this.requestTimeoutMs,
|
|
@@ -48,12 +49,15 @@ export class AnalyticsProcessor {
|
|
|
48
49
|
'Content-Type': 'application/json',
|
|
49
50
|
'X-Environment-Key': this.environmentKey
|
|
50
51
|
}
|
|
51
|
-
})
|
|
52
|
+
});
|
|
53
|
+
await this.currentFlush;
|
|
52
54
|
} catch (error) {
|
|
53
55
|
// We don't want failing to write analytics to cause any exceptions in the main
|
|
54
56
|
// thread so we just swallow them here.
|
|
55
57
|
this.logger.warn('Failed to post analytics to Flagsmith API. Not clearing data, will retry.')
|
|
56
58
|
return;
|
|
59
|
+
} finally {
|
|
60
|
+
this.currentFlush = undefined;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
this.analyticsData = {};
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
SegmentRuleModel
|
|
12
12
|
} from '../../../../flagsmith-engine/segments/models';
|
|
13
13
|
|
|
14
|
-
const conditionMatchCases: [string, string | number | boolean, string, boolean][] = [
|
|
14
|
+
const conditionMatchCases: [string, string | number | boolean | null, string, boolean][] = [
|
|
15
15
|
[CONDITION_OPERATORS.EQUAL, 'bar', 'bar', true],
|
|
16
16
|
[CONDITION_OPERATORS.EQUAL, 'bar', 'baz', false],
|
|
17
17
|
[CONDITION_OPERATORS.EQUAL, 1, '1', true],
|
|
@@ -57,11 +57,14 @@ const conditionMatchCases: [string, string | number | boolean, string, boolean][
|
|
|
57
57
|
[CONDITION_OPERATORS.CONTAINS, 'bar', 'b', true],
|
|
58
58
|
[CONDITION_OPERATORS.CONTAINS, 'bar', 'bar', true],
|
|
59
59
|
[CONDITION_OPERATORS.CONTAINS, 'bar', 'baz', false],
|
|
60
|
+
[CONDITION_OPERATORS.CONTAINS, null, 'foo', false],
|
|
60
61
|
[CONDITION_OPERATORS.NOT_CONTAINS, 'bar', 'b', false],
|
|
61
62
|
[CONDITION_OPERATORS.NOT_CONTAINS, 'bar', 'bar', false],
|
|
62
63
|
[CONDITION_OPERATORS.NOT_CONTAINS, 'bar', 'baz', true],
|
|
64
|
+
[CONDITION_OPERATORS.NOT_CONTAINS, null, 'foo', false],
|
|
63
65
|
[CONDITION_OPERATORS.REGEX, 'foo', '[a-z]+', true],
|
|
64
66
|
[CONDITION_OPERATORS.REGEX, 'FOO', '[a-z]+', false],
|
|
67
|
+
[CONDITION_OPERATORS.REGEX, null, '[a-z]+', false],
|
|
65
68
|
[CONDITION_OPERATORS.EQUAL, "1.0.0", "1.0.0:semver", true],
|
|
66
69
|
[CONDITION_OPERATORS.EQUAL, "1.0.0", "1.0.0:semver", true],
|
|
67
70
|
[CONDITION_OPERATORS.EQUAL, "1.0.0", "1.0.1:semver", false],
|
|
@@ -133,4 +136,4 @@ test('test_segment_rule_matching_function', () => {
|
|
|
133
136
|
for (const testCase of testCases) {
|
|
134
137
|
expect(new SegmentRuleModel(testCase[0]).matchingFunction()).toBe(testCase[1]);
|
|
135
138
|
}
|
|
136
|
-
});
|
|
139
|
+
});
|
|
@@ -72,3 +72,21 @@ test('errors in fetch sending analytics data are swallowed', async () => {
|
|
|
72
72
|
// we expect that fetch was called but the exception was handled
|
|
73
73
|
expect(fetch).toHaveBeenCalled();
|
|
74
74
|
})
|
|
75
|
+
|
|
76
|
+
test('analytics is only flushed once even if requested concurrently', async () => {
|
|
77
|
+
const processor = analyticsProcessor();
|
|
78
|
+
processor.trackFeature('myFeature');
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
fetch.mockImplementation(() => {
|
|
81
|
+
return new Promise((resolve, _) => {
|
|
82
|
+
setTimeout(resolve, 1000)
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
const flushes = Promise.all([
|
|
86
|
+
processor.flush(),
|
|
87
|
+
processor.flush(),
|
|
88
|
+
])
|
|
89
|
+
jest.runOnlyPendingTimers();
|
|
90
|
+
await flushes;
|
|
91
|
+
expect(fetch).toHaveBeenCalledTimes(1)
|
|
92
|
+
})
|