flagsmith-nodejs 6.1.0 → 7.0.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.
Files changed (117) hide show
  1. package/.github/workflows/conventional-commit.yml +29 -0
  2. package/.github/workflows/publish.yml +20 -17
  3. package/.github/workflows/pull_request.yaml +36 -33
  4. package/.github/workflows/release-please.yml +18 -0
  5. package/.gitmodules +1 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.prettierrc.cjs +9 -1
  9. package/.release-please-manifest.json +1 -0
  10. package/CHANGELOG.md +592 -0
  11. package/CODEOWNERS +1 -0
  12. package/README.md +0 -2
  13. package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
  14. package/build/cjs/flagsmith-engine/environments/models.js +3 -1
  15. package/build/cjs/flagsmith-engine/environments/util.js +1 -1
  16. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  17. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
  18. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  19. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
  20. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  21. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
  22. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  23. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
  24. package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
  25. package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
  26. package/build/cjs/flagsmith-engine/features/models.js +1 -1
  27. package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
  28. package/build/cjs/flagsmith-engine/features/types.js +9 -0
  29. package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
  30. package/build/cjs/flagsmith-engine/features/util.js +5 -1
  31. package/build/cjs/flagsmith-engine/index.d.ts +61 -9
  32. package/build/cjs/flagsmith-engine/index.js +176 -56
  33. package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
  34. package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
  35. package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
  36. package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
  37. package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
  38. package/build/cjs/flagsmith-engine/segments/models.js +115 -13
  39. package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  40. package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
  41. package/build/cjs/sdk/analytics.js +3 -1
  42. package/build/cjs/sdk/index.d.ts +1 -3
  43. package/build/cjs/sdk/index.js +63 -24
  44. package/build/cjs/sdk/models.d.ts +8 -1
  45. package/build/cjs/sdk/models.js +29 -1
  46. package/build/cjs/sdk/utils.d.ts +1 -0
  47. package/build/cjs/sdk/utils.js +14 -1
  48. package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
  49. package/build/esm/flagsmith-engine/environments/models.js +3 -1
  50. package/build/esm/flagsmith-engine/environments/util.js +1 -1
  51. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  52. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
  53. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  54. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
  55. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  56. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
  57. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  58. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
  59. package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
  60. package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
  61. package/build/esm/flagsmith-engine/features/models.js +2 -2
  62. package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
  63. package/build/esm/flagsmith-engine/features/types.js +6 -0
  64. package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
  65. package/build/esm/flagsmith-engine/features/util.js +3 -0
  66. package/build/esm/flagsmith-engine/index.d.ts +61 -9
  67. package/build/esm/flagsmith-engine/index.js +161 -43
  68. package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
  69. package/build/esm/flagsmith-engine/segments/constants.js +1 -0
  70. package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
  71. package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
  72. package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
  73. package/build/esm/flagsmith-engine/segments/models.js +115 -13
  74. package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  75. package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
  76. package/build/esm/sdk/analytics.js +3 -1
  77. package/build/esm/sdk/index.d.ts +1 -3
  78. package/build/esm/sdk/index.js +63 -24
  79. package/build/esm/sdk/models.d.ts +8 -1
  80. package/build/esm/sdk/models.js +29 -1
  81. package/build/esm/sdk/utils.d.ts +1 -0
  82. package/build/esm/sdk/utils.js +12 -0
  83. package/flagsmith-engine/environments/models.ts +3 -1
  84. package/flagsmith-engine/environments/util.ts +2 -1
  85. package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
  86. package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
  87. package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
  88. package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
  89. package/flagsmith-engine/evaluation/models.ts +96 -0
  90. package/flagsmith-engine/features/models.ts +3 -2
  91. package/flagsmith-engine/features/types.ts +5 -0
  92. package/flagsmith-engine/features/util.ts +4 -0
  93. package/flagsmith-engine/index.ts +229 -72
  94. package/flagsmith-engine/segments/constants.ts +1 -0
  95. package/flagsmith-engine/segments/evaluators.ts +178 -62
  96. package/flagsmith-engine/segments/models.ts +171 -23
  97. package/flagsmith-engine/utils/hashing/index.ts +2 -2
  98. package/package.json +13 -2
  99. package/release-please-config.json +62 -0
  100. package/sdk/analytics.ts +3 -1
  101. package/sdk/index.ts +89 -30
  102. package/sdk/models.ts +44 -2
  103. package/sdk/utils.ts +13 -0
  104. package/tests/engine/e2e/engine.test.ts +43 -38
  105. package/tests/engine/unit/engine.test.ts +306 -60
  106. package/tests/engine/unit/mappers.test.ts +353 -0
  107. package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
  108. package/tests/engine/unit/segments/segments_model.test.ts +85 -0
  109. package/tests/engine/unit/utils/utils.test.ts +7 -7
  110. package/tests/engine/unit/utils.ts +1 -1
  111. package/tests/sdk/analytics.test.ts +6 -1
  112. package/tests/sdk/data/environment.json +1 -0
  113. package/tests/sdk/flagsmith-environment-flags.test.ts +28 -0
  114. package/tests/sdk/flagsmith-identity-flags.test.ts +11 -2
  115. package/tests/sdk/flagsmith.test.ts +190 -3
  116. package/tests/sdk/offline-handlers.test.ts +3 -1
  117. package/vitest.config.esm.ts +34 -0
@@ -1,76 +1,192 @@
1
- import { EnvironmentModel } from '../environments/models.js';
2
- import { IdentityModel } from '../identities/models.js';
3
- import { TraitModel } from '../identities/traits/models.js';
4
- import { getHashedPercentateForObjIds } from '../utils/hashing/index.js';
5
- import { PERCENTAGE_SPLIT, IS_SET, IS_NOT_SET } from './constants.js';
6
- import { SegmentConditionModel, SegmentModel, SegmentRuleModel } from './models.js';
7
-
8
- export function getIdentitySegments(
9
- environment: EnvironmentModel,
10
- identity: IdentityModel,
11
- overrideTraits?: TraitModel[]
12
- ): SegmentModel[] {
13
- return environment.project.segments.filter(segment =>
14
- evaluateIdentityInSegment(identity, segment, overrideTraits)
15
- );
1
+ import * as jsonpathModule from 'jsonpath';
2
+ import {
3
+ GenericEvaluationContext,
4
+ InSegmentCondition,
5
+ SegmentCondition,
6
+ SegmentContext,
7
+ SegmentRule
8
+ } from '../evaluation/models.js';
9
+ import { getHashedPercentageForObjIds } from '../utils/hashing/index.js';
10
+ import { SegmentConditionModel } from './models.js';
11
+ import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js';
12
+
13
+ // Handle ESM/CJS interop - jsonpath exports default in ESM
14
+ const jsonpath = (jsonpathModule as any).default || jsonpathModule;
15
+
16
+ /**
17
+ * Returns all segments that the identity belongs to based on segment rules evaluation.
18
+ *
19
+ * An identity belongs to a segment if it matches ALL of the segment's rules.
20
+ * If the context has no identity or segments, returns an empty array.
21
+ *
22
+ * @param context - Evaluation context containing identity and segment definitions
23
+ * @returns Array of segments that the identity matches
24
+ */
25
+ export function getIdentitySegments(context: GenericEvaluationContext): SegmentContext[] {
26
+ if (!context.identity || !context.segments) return [];
27
+
28
+ return Object.values(context.segments).filter(segment => {
29
+ if (segment.rules.length === 0) return false;
30
+ return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context));
31
+ });
16
32
  }
17
33
 
18
- export function evaluateIdentityInSegment(
19
- identity: IdentityModel,
20
- segment: SegmentModel,
21
- overrideTraits?: TraitModel[]
34
+ /**
35
+ * Evaluates whether a segment condition matches the identity's traits or context values.
36
+ *
37
+ * Handles different types of conditions:
38
+ * - PERCENTAGE_SPLIT: Deterministic percentage-based bucketing using identity key
39
+ * - IS_SET/IS_NOT_SET: Checks for trait existence
40
+ * - Standard operators: EQUAL, NOT_EQUAL, etc. via SegmentConditionModel
41
+ * - JSONPath expressions: $.identity.identifier, $.environment.name, etc.
42
+ *
43
+ * @param condition - The condition to evaluate (property, operator, value)
44
+ * @param segmentKey - Key of the segment (used for percentage split hashing)
45
+ * @param context - Evaluation context containing identity, traits, and environment
46
+ * @returns true if the condition matches
47
+ */
48
+ export function traitsMatchSegmentCondition(
49
+ condition: SegmentCondition | InSegmentCondition,
50
+ segmentKey: string,
51
+ context?: GenericEvaluationContext
22
52
  ): boolean {
23
- return (
24
- segment.rules.length > 0 &&
25
- segment.rules.filter(rule =>
26
- traitsMatchSegmentRule(
27
- overrideTraits || identity.identityTraits,
28
- rule,
29
- segment.id,
30
- identity.djangoID || identity.compositeKey
31
- )
32
- ).length === segment.rules.length
33
- );
53
+ if (condition.operator === PERCENTAGE_SPLIT) {
54
+ let splitKey: string | undefined;
55
+
56
+ if (!condition.property) {
57
+ splitKey = context?.identity?.key;
58
+ } else {
59
+ splitKey = getContextValue(condition.property, context);
60
+ }
61
+
62
+ if (!splitKey) {
63
+ return false;
64
+ }
65
+ const hashedPercentage = getHashedPercentageForObjIds([segmentKey, splitKey]);
66
+ return hashedPercentage <= parseFloat(String(condition.value));
67
+ }
68
+ if (!condition.property) {
69
+ return false;
70
+ }
71
+
72
+ const traitValue = getTraitValue(condition.property, context);
73
+
74
+ if (condition.operator === IS_SET) {
75
+ return traitValue !== undefined && traitValue !== null;
76
+ }
77
+ if (condition.operator === IS_NOT_SET) {
78
+ return traitValue === undefined || traitValue === null;
79
+ }
80
+
81
+ if (traitValue !== undefined && traitValue !== null) {
82
+ const segmentCondition = new SegmentConditionModel(
83
+ condition.operator,
84
+ condition.value as string,
85
+ condition.property
86
+ );
87
+ return segmentCondition.matchesTraitValue(traitValue);
88
+ }
89
+
90
+ return false;
34
91
  }
35
92
 
36
93
  function traitsMatchSegmentRule(
37
- identityTraits: TraitModel[],
38
- rule: SegmentRuleModel,
39
- segmentId: number | string,
40
- identityId: number | string
94
+ rule: SegmentRule,
95
+ segmentKey: string,
96
+ context?: GenericEvaluationContext
97
+ ): boolean {
98
+ const matchesConditions = evaluateConditions(rule, segmentKey, context);
99
+ const matchesSubRules = evaluateSubRules(rule, segmentKey, context);
100
+
101
+ return matchesConditions && matchesSubRules;
102
+ }
103
+
104
+ function evaluateConditions(
105
+ rule: SegmentRule,
106
+ segmentKey: string,
107
+ context?: GenericEvaluationContext
41
108
  ): boolean {
42
- const matchesConditions =
43
- rule.conditions.length > 0
44
- ? rule.matchingFunction()(
45
- rule.conditions.map(condition =>
46
- traitsMatchSegmentCondition(identityTraits, condition, segmentId, identityId)
47
- )
48
- )
49
- : true;
50
- return (
51
- matchesConditions &&
52
- rule.rules.filter(rule =>
53
- traitsMatchSegmentRule(identityTraits, rule, segmentId, identityId)
54
- ).length === rule.rules.length
109
+ if (!rule.conditions || rule.conditions.length === 0) return true;
110
+
111
+ const conditionResults = rule.conditions.map((condition: SegmentCondition) =>
112
+ traitsMatchSegmentCondition(condition, segmentKey, context)
55
113
  );
114
+
115
+ return evaluateRuleConditions(rule.type, conditionResults);
56
116
  }
57
117
 
58
- export function traitsMatchSegmentCondition(
59
- identityTraits: TraitModel[],
60
- condition: SegmentConditionModel,
61
- segmentId: number | string,
62
- identityId: number | string
118
+ function evaluateSubRules(
119
+ rule: SegmentRule,
120
+ segmentKey: string,
121
+ context?: GenericEvaluationContext
63
122
  ): boolean {
64
- if (condition.operator == PERCENTAGE_SPLIT) {
65
- var hashedPercentage = getHashedPercentateForObjIds([segmentId, identityId]);
66
- return hashedPercentage <= parseFloat(String(condition.value));
123
+ if (!rule.rules || rule.rules.length === 0) return true;
124
+
125
+ return rule.rules.every((subRule: SegmentRule) =>
126
+ traitsMatchSegmentRule(subRule, segmentKey, context)
127
+ );
128
+ }
129
+
130
+ function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]): boolean {
131
+ switch (ruleType) {
132
+ case 'ALL':
133
+ return conditionResults.length === 0 || conditionResults.every(result => result);
134
+ case 'ANY':
135
+ return conditionResults.length > 0 && conditionResults.some(result => result);
136
+ case 'NONE':
137
+ return conditionResults.length === 0 || conditionResults.every(result => !result);
138
+ default:
139
+ return false;
67
140
  }
68
- const traits = identityTraits.filter(t => t.traitKey === condition.property_);
69
- const trait = traits.length > 0 ? traits[0] : undefined;
70
- if (condition.operator === IS_SET) {
71
- return !!trait;
72
- } else if (condition.operator === IS_NOT_SET) {
73
- return trait == undefined;
141
+ }
142
+
143
+ function getTraitValue(property: string, context?: GenericEvaluationContext): any {
144
+ if (property.startsWith('$.')) {
145
+ const contextValue = getContextValue(property, context);
146
+ if (contextValue !== undefined && isPrimitive(contextValue)) {
147
+ return contextValue;
148
+ }
74
149
  }
75
- return trait ? condition.matchesTraitValue(trait.traitValue) : false;
150
+ const traits = context?.identity?.traits || {};
151
+
152
+ return traits[property];
153
+ }
154
+
155
+ function isPrimitive(value: any): boolean {
156
+ if (value === null || value === undefined) {
157
+ return true;
158
+ }
159
+
160
+ // Objects and arrays are non-primitive
161
+ return typeof value !== 'object';
162
+ }
163
+
164
+ /**
165
+ * Evaluates JSONPath expressions against the evaluation context.
166
+ *
167
+ * Supports accessing nested context values using JSONPath syntax.
168
+ * Commonly used paths:
169
+ * - $.identity.identifier - User's unique identifier
170
+ * - $.identity.key - User's internal key
171
+ * - $.environment.name - Environment name
172
+ * - $.environment.key - Environment key
173
+ *
174
+ * @param jsonPath - JSONPath expression starting with '$.'
175
+ * @param context - Evaluation context to query against
176
+ * @returns The resolved value, or undefined if path doesn't exist or is invalid
177
+ */
178
+ export function getContextValue(jsonPath: string, context?: GenericEvaluationContext): any {
179
+ if (!context || !jsonPath?.startsWith('$.')) return undefined;
180
+
181
+ try {
182
+ const normalizedPath = normalizeJsonPath(jsonPath);
183
+ const results = jsonpath.query(context, normalizedPath);
184
+ return results.length > 0 ? results[0] : undefined;
185
+ } catch (error) {
186
+ return undefined;
187
+ }
188
+ }
189
+
190
+ function normalizeJsonPath(jsonPath: string): string {
191
+ return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']");
76
192
  }
@@ -1,6 +1,11 @@
1
1
  import * as semver from 'semver';
2
2
 
3
- import { FeatureStateModel } from '../features/models.js';
3
+ import {
4
+ FeatureModel,
5
+ FeatureStateModel,
6
+ MultivariateFeatureOptionModel,
7
+ MultivariateFeatureStateValueModel
8
+ } from '../features/models.js';
4
9
  import { getCastingFunction as getCastingFunction } from '../utils/index.js';
5
10
  import {
6
11
  ALL_RULE,
@@ -13,6 +18,12 @@ import {
13
18
  CONDITION_OPERATORS
14
19
  } from './constants.js';
15
20
  import { isSemver } from './util.js';
21
+ import {
22
+ EvaluationContext,
23
+ Overrides
24
+ } from '../evaluation/evaluationContext/evaluationContext.types.js';
25
+ import { CONSTANTS } from '../features/constants.js';
26
+ import { EvaluationResultSegments, SegmentSource } from '../evaluation/models.js';
16
27
 
17
28
  export const all = (iterable: Array<any>) => iterable.filter(e => !!e).length === iterable.length;
18
29
  export const any = (iterable: Array<any>) => iterable.filter(e => !!e).length > 0;
@@ -26,22 +37,45 @@ export const matchingFunctions = {
26
37
  [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
27
38
  thisValue >= otherValue,
28
39
  [CONDITION_OPERATORS.NOT_EQUAL]: (thisValue: any, otherValue: any) => thisValue != otherValue,
29
- [CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) =>
30
- !!otherValue && otherValue.includes(thisValue)
40
+ [CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) => {
41
+ try {
42
+ return !!otherValue && otherValue.includes(thisValue);
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ };
48
+
49
+ // Semver library throws an error if the version is invalid, in this case, we want to catch and return false
50
+ const safeSemverCompare = (
51
+ semverMatchingFunction: (conditionValue: any, traitValue: any) => boolean
52
+ ) => {
53
+ return (conditionValue: any, traitValue: any) => {
54
+ try {
55
+ return semverMatchingFunction(conditionValue, traitValue);
56
+ } catch {
57
+ return false;
58
+ }
59
+ };
31
60
  };
32
61
 
33
62
  export const semverMatchingFunction = {
34
63
  ...matchingFunctions,
35
- [CONDITION_OPERATORS.EQUAL]: (thisValue: any, otherValue: any) =>
36
- semver.eq(thisValue, otherValue),
37
- [CONDITION_OPERATORS.GREATER_THAN]: (thisValue: any, otherValue: any) =>
38
- semver.gt(otherValue, thisValue),
39
- [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
40
- semver.gte(otherValue, thisValue),
41
- [CONDITION_OPERATORS.LESS_THAN]: (thisValue: any, otherValue: any) =>
42
- semver.gt(thisValue, otherValue),
43
- [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
44
- semver.gte(thisValue, otherValue)
64
+ [CONDITION_OPERATORS.EQUAL]: safeSemverCompare((conditionValue, traitValue) =>
65
+ semver.eq(traitValue, conditionValue)
66
+ ),
67
+ [CONDITION_OPERATORS.GREATER_THAN]: safeSemverCompare((conditionValue, traitValue) =>
68
+ semver.gt(traitValue, conditionValue)
69
+ ),
70
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) =>
71
+ semver.gte(traitValue, conditionValue)
72
+ ),
73
+ [CONDITION_OPERATORS.LESS_THAN]: safeSemverCompare((conditionValue, traitValue) =>
74
+ semver.lt(traitValue, conditionValue)
75
+ ),
76
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) =>
77
+ semver.lte(traitValue, conditionValue)
78
+ )
45
79
  };
46
80
 
47
81
  export const getMatchingFunctions = (semver: boolean) =>
@@ -56,17 +90,17 @@ export class SegmentConditionModel {
56
90
  };
57
91
 
58
92
  operator: string;
59
- value: string | null | undefined;
60
- property_: string | null | undefined;
93
+ value: string | null | undefined | string[];
94
+ property: string | null | undefined;
61
95
 
62
96
  constructor(
63
97
  operator: string,
64
- value?: string | null | undefined,
98
+ value?: string | null | undefined | string[],
65
99
  property?: string | null | undefined
66
100
  ) {
67
101
  this.operator = operator;
68
102
  this.value = value;
69
- this.property_ = property;
103
+ this.property = property;
70
104
  }
71
105
 
72
106
  matchesTraitValue(traitValue: any) {
@@ -79,17 +113,52 @@ export class SegmentConditionModel {
79
113
  );
80
114
  },
81
115
  evaluateRegex: (traitValue: any) => {
82
- return !!this.value && !!traitValue?.toString().match(new RegExp(this.value));
116
+ try {
117
+ if (!this.value) {
118
+ return false;
119
+ }
120
+ const regex = new RegExp(this.value?.toString());
121
+ return !!traitValue?.toString().match(regex);
122
+ } catch {
123
+ return false;
124
+ }
83
125
  },
84
126
  evaluateModulo: (traitValue: any) => {
85
- if (isNaN(parseFloat(traitValue)) || !this.value) {
127
+ const parsedTraitValue = parseFloat(traitValue);
128
+ if (isNaN(parsedTraitValue) || !this.value) {
86
129
  return false;
87
130
  }
88
- const parts = this.value.split('|');
89
- const [divisor, reminder] = [parseFloat(parts[0]), parseFloat(parts[1])];
90
- return traitValue % divisor === reminder;
131
+
132
+ const parts = this.value.toString().split('|');
133
+ if (parts.length !== 2) {
134
+ return false;
135
+ }
136
+
137
+ const divisor = parseFloat(parts[0]);
138
+ const remainder = parseFloat(parts[1]);
139
+
140
+ if (isNaN(divisor) || isNaN(remainder) || divisor === 0) {
141
+ return false;
142
+ }
143
+
144
+ return parsedTraitValue % divisor === remainder;
91
145
  },
92
- evaluateIn: (traitValue: any) => {
146
+ evaluateIn: (traitValue: string[] | string) => {
147
+ if (!traitValue || typeof traitValue === 'boolean') {
148
+ return false;
149
+ }
150
+ if (Array.isArray(this.value)) {
151
+ return this.value.includes(traitValue.toString());
152
+ }
153
+
154
+ if (typeof this.value === 'string') {
155
+ try {
156
+ const parsed = JSON.parse(this.value);
157
+ if (Array.isArray(parsed)) {
158
+ return parsed.includes(traitValue.toString());
159
+ }
160
+ } catch {}
161
+ }
93
162
  return this.value?.split(',').includes(traitValue.toString());
94
163
  }
95
164
  };
@@ -144,4 +213,83 @@ export class SegmentModel {
144
213
  this.id = id;
145
214
  this.name = name;
146
215
  }
216
+
217
+ static fromSegmentResult(
218
+ segmentResults: EvaluationResultSegments,
219
+ evaluationContext: EvaluationContext
220
+ ): SegmentModel[] {
221
+ const segmentModels: SegmentModel[] = [];
222
+ if (!evaluationContext.segments) {
223
+ return [];
224
+ }
225
+
226
+ for (const segmentResult of segmentResults) {
227
+ if (segmentResult.metadata?.source === SegmentSource.IDENTITY_OVERRIDE) {
228
+ continue;
229
+ }
230
+ const segmentMetadataId = segmentResult.metadata?.id;
231
+ if (!segmentMetadataId) {
232
+ continue;
233
+ }
234
+ const segmentContext = evaluationContext.segments[segmentMetadataId.toString()];
235
+ if (segmentContext) {
236
+ const segment = new SegmentModel(segmentMetadataId, segmentContext.name);
237
+ segment.rules = segmentContext.rules.map(rule => new SegmentRuleModel(rule.type));
238
+ segment.featureStates = SegmentModel.createFeatureStatesFromOverrides(
239
+ segmentContext.overrides || []
240
+ );
241
+ segmentModels.push(segment);
242
+ }
243
+ }
244
+
245
+ return segmentModels;
246
+ }
247
+
248
+ private static createFeatureStatesFromOverrides(overrides: Overrides): FeatureStateModel[] {
249
+ if (!overrides) return [];
250
+ return overrides
251
+ .filter(override => {
252
+ const overrideMetadataId = override?.metadata?.id;
253
+ return typeof overrideMetadataId === 'number';
254
+ })
255
+ .map(override => {
256
+ const overrideMetadataId = override.metadata!.id as number;
257
+ const feature = new FeatureModel(
258
+ overrideMetadataId,
259
+ override.name,
260
+ override.variants?.length && override.variants.length > 0
261
+ ? CONSTANTS.MULTIVARIATE
262
+ : CONSTANTS.STANDARD
263
+ );
264
+
265
+ const featureState = new FeatureStateModel(
266
+ feature,
267
+ override.enabled,
268
+ override.priority || 0
269
+ );
270
+
271
+ if (override.value !== undefined) {
272
+ featureState.setValue(override.value);
273
+ }
274
+
275
+ if (override.variants && override.variants.length > 0) {
276
+ featureState.multivariateFeatureStateValues = this.createMultivariateValues(
277
+ override.variants
278
+ );
279
+ }
280
+
281
+ return featureState;
282
+ });
283
+ }
284
+
285
+ private static createMultivariateValues(variants: any[]): MultivariateFeatureStateValueModel[] {
286
+ return variants.map(
287
+ variant =>
288
+ new MultivariateFeatureStateValueModel(
289
+ new MultivariateFeatureOptionModel(variant.value, variant.id as number),
290
+ variant.weight as number,
291
+ variant.id as number
292
+ )
293
+ );
294
+ }
147
295
  }
@@ -14,7 +14,7 @@ const makeRepeated = (arr: Array<any>, repeats: number) =>
14
14
  * @param {} iterations=1 num times to include each id in the generated string to hash
15
15
  * @returns number number between 0 (inclusive) and 100 (exclusive)
16
16
  */
17
- export function getHashedPercentateForObjIds(objectIds: Array<any>, iterations = 1): number {
17
+ export function getHashedPercentageForObjIds(objectIds: Array<any>, iterations = 1): number {
18
18
  let toHash = makeRepeated(objectIds, iterations).join(',');
19
19
  const hashedValue = md5(toHash);
20
20
  const hashedInt = BigInt('0x' + hashedValue);
@@ -24,7 +24,7 @@ export function getHashedPercentateForObjIds(objectIds: Array<any>, iterations =
24
24
  /* istanbul ignore next */
25
25
  if (value === 100) {
26
26
  /* istanbul ignore next */
27
- return getHashedPercentateForObjIds(objectIds, iterations + 1);
27
+ return getHashedPercentageForObjIds(objectIds, iterations + 1);
28
28
  }
29
29
 
30
30
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagsmith-nodejs",
3
- "version": "6.1.0",
3
+ "version": "7.0.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/cjs/index.js",
6
6
  "type": "module",
@@ -51,26 +51,37 @@
51
51
  "scripts": {
52
52
  "lint": "prettier --write .",
53
53
  "test": "vitest --coverage --run",
54
+ "test:esm-build": "npm run build && ESM_BUILD=true vitest --config vitest.config.esm.ts --run",
54
55
  "test:watch": "vitest",
55
56
  "test:debug": "vitest --inspect-brk --no-file-parallelism --coverage",
56
57
  "prebuild": "rm -rf ./build",
57
58
  "build": "tsc -b tsconfig.cjs.json tsconfig.esm.json && echo '{\"type\": \"commonjs\"}'> build/cjs/package.json",
58
59
  "deploy": "npm i && npm run build && npm publish",
59
60
  "deploy:beta": "npm i && npm run build && npm publish --tag beta",
60
- "prepare": "husky install"
61
+ "prepare": "husky install",
62
+ "generate-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-result.json -o flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts && rm evaluation-result.json",
63
+ "generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json",
64
+ "generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types"
61
65
  },
62
66
  "dependencies": {
67
+ "jsonpath": "^1.1.1",
63
68
  "pino": "^8.8.0",
64
69
  "semver": "^7.3.7",
65
70
  "undici-types": "^6.19.8"
66
71
  },
67
72
  "devDependencies": {
73
+ "@types/jest": "^30.0.0",
74
+ "@types/jsonpath": "^0.2.4",
68
75
  "@types/node": "^20.16.10",
69
76
  "@types/semver": "^7.3.9",
70
77
  "@types/uuid": "^8.3.4",
71
78
  "@vitest/coverage-v8": "^2.1.2",
72
79
  "esbuild": "^0.25.0",
73
80
  "husky": "^7.0.4",
81
+ "install": "^0.13.0",
82
+ "json-schema-to-typescript": "^15.0.4",
83
+ "jsonc-parser": "^3.3.1",
84
+ "npm": "^11.6.1",
74
85
  "prettier": "^2.2.1",
75
86
  "typescript": "^4.9.5",
76
87
  "undici": "^6.19.8",
@@ -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,6 +1,7 @@
1
1
  import { pino, Logger } from 'pino';
2
2
  import { Fetch } from './types.js';
3
3
  import { FlagsmithConfig } from './types.js';
4
+ import { getUserAgent } from './utils.js';
4
5
 
5
6
  export const ANALYTICS_ENDPOINT = './analytics/flags/';
6
7
 
@@ -69,7 +70,8 @@ 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;