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.
@@ -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
- return !traitValue.includes(_this.value);
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) {
@@ -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.
@@ -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, , 4]);
82
- return [4 /*yield*/, (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
- })];
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*/, 4];
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 !traitValue.includes(this.value);
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.0",
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
- await fetch(this.analyticsEndpoint, {
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
+ })