@depup/launchdarkly-node-server-sdk 7.0.4-depup.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.
Files changed (107) hide show
  1. package/.babelrc +16 -0
  2. package/.circleci/config.yml +89 -0
  3. package/.eslintignore +5 -0
  4. package/.eslintrc.yaml +114 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. package/.github/pull_request_template.md +21 -0
  9. package/.github/workflows/stale.yml +8 -0
  10. package/.hound.yml +33 -0
  11. package/.ldrelease/config.yml +28 -0
  12. package/.prettierrc +6 -0
  13. package/CHANGELOG.md +603 -0
  14. package/CODEOWNERS +2 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/LICENSE.txt +13 -0
  17. package/README.md +36 -0
  18. package/SECURITY.md +5 -0
  19. package/attribute_reference.js +217 -0
  20. package/big_segments.js +117 -0
  21. package/caching_store_wrapper.js +240 -0
  22. package/changes.json +30 -0
  23. package/configuration.js +235 -0
  24. package/context.js +98 -0
  25. package/context_filter.js +137 -0
  26. package/contract-tests/README.md +7 -0
  27. package/contract-tests/index.js +109 -0
  28. package/contract-tests/log.js +23 -0
  29. package/contract-tests/package.json +15 -0
  30. package/contract-tests/sdkClientEntity.js +110 -0
  31. package/contract-tests/testharness-suppressions.txt +2 -0
  32. package/diagnostic_events.js +151 -0
  33. package/docs/typedoc.js +10 -0
  34. package/errors.js +26 -0
  35. package/evaluator.js +822 -0
  36. package/event_factory.js +121 -0
  37. package/event_processor.js +320 -0
  38. package/event_summarizer.js +101 -0
  39. package/feature_store.js +120 -0
  40. package/feature_store_event_wrapper.js +258 -0
  41. package/file_data_source.js +192 -0
  42. package/flags_state.js +46 -0
  43. package/index.d.ts +2426 -0
  44. package/index.js +452 -0
  45. package/integrations.js +7 -0
  46. package/interfaces.js +2 -0
  47. package/loggers.js +125 -0
  48. package/messages.js +31 -0
  49. package/operators.js +106 -0
  50. package/package.json +105 -0
  51. package/polling.js +70 -0
  52. package/requestor.js +62 -0
  53. package/scripts/better-audit.sh +76 -0
  54. package/sharedtest/big_segment_store_tests.js +86 -0
  55. package/sharedtest/feature_store_tests.js +177 -0
  56. package/sharedtest/persistent_feature_store_tests.js +183 -0
  57. package/sharedtest/store_tests.js +7 -0
  58. package/streaming.js +179 -0
  59. package/test/LDClient-big-segments-test.js +92 -0
  60. package/test/LDClient-end-to-end-test.js +218 -0
  61. package/test/LDClient-evaluation-all-flags-test.js +226 -0
  62. package/test/LDClient-evaluation-test.js +204 -0
  63. package/test/LDClient-events-test.js +502 -0
  64. package/test/LDClient-listeners-test.js +180 -0
  65. package/test/LDClient-test.js +96 -0
  66. package/test/LDClient-tls-test.js +110 -0
  67. package/test/attribute_reference-test.js +494 -0
  68. package/test/big_segments-test.js +182 -0
  69. package/test/caching_store_wrapper-test.js +434 -0
  70. package/test/configuration-test.js +249 -0
  71. package/test/context-test.js +93 -0
  72. package/test/context_filter-test.js +424 -0
  73. package/test/diagnostic_events-test.js +152 -0
  74. package/test/evaluator-big-segments-test.js +301 -0
  75. package/test/evaluator-bucketing-test.js +333 -0
  76. package/test/evaluator-clause-test.js +277 -0
  77. package/test/evaluator-flag-test.js +452 -0
  78. package/test/evaluator-pre-conditions-test.js +105 -0
  79. package/test/evaluator-rule-test.js +131 -0
  80. package/test/evaluator-segment-match-test.js +310 -0
  81. package/test/evaluator_helpers.js +106 -0
  82. package/test/event_processor-test.js +680 -0
  83. package/test/event_summarizer-test.js +146 -0
  84. package/test/feature_store-test.js +42 -0
  85. package/test/feature_store_event_wrapper-test.js +182 -0
  86. package/test/feature_store_test_base.js +60 -0
  87. package/test/file_data_source-test.js +255 -0
  88. package/test/loggers-test.js +126 -0
  89. package/test/operators-test.js +102 -0
  90. package/test/polling-test.js +158 -0
  91. package/test/requestor-test.js +60 -0
  92. package/test/store_tests_big_segments-test.js +61 -0
  93. package/test/streaming-test.js +323 -0
  94. package/test/stubs.js +107 -0
  95. package/test/test_data-test.js +341 -0
  96. package/test/update_queue-test.js +61 -0
  97. package/test-types.ts +210 -0
  98. package/test_data.js +323 -0
  99. package/tsconfig.json +14 -0
  100. package/update_queue.js +28 -0
  101. package/utils/__tests__/httpUtils-test.js +39 -0
  102. package/utils/__tests__/wrapPromiseCallback-test.js +33 -0
  103. package/utils/asyncUtils.js +32 -0
  104. package/utils/httpUtils.js +105 -0
  105. package/utils/stringifyAttrs.js +14 -0
  106. package/utils/wrapPromiseCallback.js +36 -0
  107. package/versioned_data_kind.js +34 -0
@@ -0,0 +1,301 @@
1
+ const { Evaluator, makeBigSegmentRef } = require('../evaluator');
2
+ const {
3
+ basicUser,
4
+ eventFactory,
5
+ asyncEvaluate,
6
+ makeFlagWithSegmentMatch,
7
+ makeClauseThatMatchesUser,
8
+ prepareQueries,
9
+ makeSegmentMatchClause,
10
+ basicSingleKindUser,
11
+ basicMultiKindUser,
12
+ makeBooleanFlagWithRules,
13
+ } = require('./evaluator_helpers');
14
+
15
+ // Tests of flag evaluation involving Big Segments.
16
+
17
+ describe.each([undefined, 'user'])('Evaluator - Big Segments user contexts', (unboundedContextKind) => {
18
+ it('segment is not matched if there is no way to query it', async () => {
19
+ const segment = {
20
+ key: 'test',
21
+ included: [ basicUser.key ], // included should be ignored for a big segment
22
+ version: 1,
23
+ unbounded: true,
24
+ generation: 1,
25
+ unboundedContextKind
26
+ };
27
+ const e = Evaluator(prepareQueries({ segments: [segment] }));
28
+ const flag = makeFlagWithSegmentMatch(segment);
29
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, basicUser, eventFactory);
30
+ expect(detail.value).toBe(false);
31
+ expect(detail.reason.bigSegmentsStatus).toEqual('NOT_CONFIGURED');
32
+ });
33
+
34
+ it('segment with no generation is not matched', async () => {
35
+ const segment = {
36
+ key: 'test',
37
+ included: [ basicUser.key ], // included should be ignored for a big segment
38
+ version: 1,
39
+ unbounded: true,
40
+ };
41
+ const e = Evaluator(prepareQueries({ segments: [segment] }));
42
+ const flag = makeFlagWithSegmentMatch(segment);
43
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, basicUser, eventFactory);
44
+ expect(detail.value).toBe(false);
45
+ expect(detail.reason.bigSegmentsStatus).toEqual('NOT_CONFIGURED');
46
+ });
47
+
48
+ it.each([basicUser, basicSingleKindUser, basicMultiKindUser])
49
+ ('matched with include', async (context) => {
50
+ const segment = {
51
+ key: 'test',
52
+ version: 1,
53
+ unbounded: true,
54
+ generation: 2,
55
+ };
56
+ const membership = { [makeBigSegmentRef(segment)]: true };
57
+ const e = Evaluator(prepareQueries({ segments: [segment], bigSegments: { [basicUser.key]: membership } }));
58
+ const flag = makeFlagWithSegmentMatch(segment);
59
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, context, eventFactory);
60
+ expect(detail.value).toBe(true);
61
+ expect(detail.reason.bigSegmentsStatus).toEqual('HEALTHY');
62
+ });
63
+
64
+ it.each([basicUser, basicSingleKindUser, basicMultiKindUser])
65
+ ('matched with rule', async (context) => {
66
+ const segment = {
67
+ key: 'test',
68
+ version: 1,
69
+ unbounded: true,
70
+ generation: 2,
71
+ rules: [
72
+ { clauses: [makeClauseThatMatchesUser(basicUser)] },
73
+ ]
74
+ };
75
+ const membership = {};
76
+ const e = Evaluator(prepareQueries({ segments: [segment], bigSegments: { [basicUser.key]: membership } }));
77
+ const flag = makeFlagWithSegmentMatch(segment);
78
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, context, eventFactory);
79
+ expect(detail.value).toBe(true);
80
+ expect(detail.reason.bigSegmentsStatus).toEqual('HEALTHY');
81
+ });
82
+
83
+ it.each([basicUser, basicSingleKindUser, basicMultiKindUser])
84
+ ('unmatched by exclude regardless of rule', async (context) => {
85
+ const segment = {
86
+ key: 'test',
87
+ version: 1,
88
+ unbounded: true,
89
+ generation: 2,
90
+ rules: [
91
+ { clauses: [makeClauseThatMatchesUser(basicUser)] },
92
+ ]
93
+ };
94
+ const membership = { [makeBigSegmentRef(segment)]: false };
95
+ const e = Evaluator(prepareQueries({ segments: [segment], bigSegments: { [basicUser.key]: membership } }));
96
+ const flag = makeFlagWithSegmentMatch(segment);
97
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, context, eventFactory);
98
+ expect(detail.value).toBe(false);
99
+ expect(detail.reason.bigSegmentsStatus).toEqual('HEALTHY');
100
+ });
101
+
102
+ it('status is returned from provider', async () => {
103
+ const segment = {
104
+ key: 'test',
105
+ version: 1,
106
+ unbounded: true,
107
+ generation: 2,
108
+ };
109
+ const membership = { [makeBigSegmentRef(segment)]: true };
110
+ const queries = prepareQueries({ segments: [segment] });
111
+ queries.getBigSegmentsMembership = (key, cb) => {
112
+ cb([ membership, 'STALE' ]);
113
+ };
114
+ const e = Evaluator(queries);
115
+ const flag = makeFlagWithSegmentMatch(segment);
116
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, basicUser, eventFactory);
117
+ expect(detail.value).toBe(true);
118
+ expect(detail.reason.bigSegmentsStatus).toEqual('STALE');
119
+ });
120
+
121
+ it.each([basicUser, basicSingleKindUser, basicMultiKindUser])
122
+ ('queries state only once per user even if flag references multiple segments', async (context) => {
123
+ const segment1 = {
124
+ key: 'segmentkey1',
125
+ version: 1,
126
+ unbounded: true,
127
+ generation: 2,
128
+ };
129
+ const segment2 = {
130
+ key: 'segmentkey2',
131
+ version: 1,
132
+ unbounded: true,
133
+ generation: 3,
134
+ };
135
+ const flag = {
136
+ key: "key",
137
+ on: "true",
138
+ fallthrough: { variation: 0 },
139
+ variations: [ false, true ],
140
+ rules: [
141
+ { variation: 1, clauses: [ makeSegmentMatchClause(segment1) ]},
142
+ { variation: 1, clauses: [ makeSegmentMatchClause(segment2) ]},
143
+ ],
144
+ }
145
+
146
+ const membership = { [makeBigSegmentRef(segment2)]: true };
147
+ // The membership deliberately does not include segment1, because we want the first rule to be
148
+ // a non-match so that it will continue on and check segment2 as well.
149
+
150
+ const queries = prepareQueries({ segments: [segment1, segment2] });
151
+ let userQueryCount = 0;
152
+ queries.getBigSegmentsMembership = (key, cb) => {
153
+ userQueryCount++;
154
+ cb([ membership, 'HEALTHY' ]);
155
+ };
156
+
157
+ const e = Evaluator(queries);
158
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, context, eventFactory);
159
+ expect(detail.value).toBe(true);
160
+ expect(detail.reason.bigSegmentsStatus).toEqual('HEALTHY');
161
+
162
+ expect(userQueryCount).toEqual(1);
163
+ });
164
+ });
165
+
166
+ describe('Evaluator - Big Segments non-user', () => {
167
+ const targetKey = 'targetKey';
168
+ const targetContextKind = 'org';
169
+
170
+ const singleKindContext = {
171
+ kind: targetContextKind,
172
+ key: targetKey
173
+ };
174
+ const multiKindContext = {
175
+ kind: 'multi',
176
+ };
177
+ multiKindContext[targetContextKind] = {
178
+ key: targetKey
179
+ };
180
+
181
+ it.each([singleKindContext, multiKindContext])
182
+ ('matched with include for matching unboundedContextKind', async (context) => {
183
+ const segment = {
184
+ key: 'test',
185
+ version: 1,
186
+ unbounded: true,
187
+ generation: 2,
188
+ unboundedContextKind: 'org'
189
+ };
190
+ const membership = { [makeBigSegmentRef(segment)]: true };
191
+ const e = Evaluator(prepareQueries({ segments: [segment], bigSegments: { [singleKindContext.key]: membership } }));
192
+ const flag = makeFlagWithSegmentMatch(segment);
193
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, context, eventFactory);
194
+ expect(detail.value).toBe(true);
195
+ expect(detail.reason.bigSegmentsStatus).toEqual('HEALTHY');
196
+ });
197
+
198
+ it.each([singleKindContext, multiKindContext])
199
+ ('not matched with include for unboundedContextKind which does not match', async (context) => {
200
+ const segment = {
201
+ key: 'test',
202
+ version: 1,
203
+ unbounded: true,
204
+ generation: 2,
205
+ unboundedContextKind: 'party'
206
+ };
207
+ const membership = { [makeBigSegmentRef(segment)]: true };
208
+ const e = Evaluator(prepareQueries({ segments: [segment], bigSegments: { [singleKindContext.key]: membership } }));
209
+ const flag = makeFlagWithSegmentMatch(segment);
210
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, context, eventFactory);
211
+ expect(detail.value).toBe(false);
212
+ expect(detail.reason.bigSegmentsStatus).toBeUndefined();
213
+ });
214
+
215
+ it('cached membership by key', async () => {
216
+ const segment = {
217
+ key: 'bigSegment1',
218
+ version: 1,
219
+ unbounded: true,
220
+ generation: 2,
221
+ unboundedContextKind: 'party'
222
+ };
223
+ const segment2 = {
224
+ key: 'bigSegment2',
225
+ version: 1,
226
+ unbounded: true,
227
+ generation: 2,
228
+ unboundedContextKind: 'org'
229
+ };
230
+
231
+ const context = {
232
+ kind: 'multi',
233
+ party: {key: 'partyKey'},
234
+ org: {key: 'orgKey'},
235
+ };
236
+ const membership = { [makeBigSegmentRef(segment)]: true };
237
+ const membership2 = { [makeBigSegmentRef(segment2)]: true };
238
+ const e = Evaluator(prepareQueries({ segments: [segment, segment2], bigSegments: { [context.party.key]: membership, [context.org.key]: membership2 } }));
239
+ const flag = makeBooleanFlagWithRules([{clauses: [
240
+ makeSegmentMatchClause(segment),
241
+ makeSegmentMatchClause(segment2)
242
+ ], variation: 1}])
243
+
244
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, context, eventFactory);
245
+ expect(detail.value).toBe(true);
246
+ expect(detail.reason.bigSegmentsStatus).toEqual('HEALTHY');
247
+ });
248
+
249
+ it.each([
250
+ ['HEALTYH', 'STALE', 'STALE'],
251
+ ['HEALTYH', 'STORE_ERROR', 'STORE_ERROR'],
252
+ ['HEALTYH', 'NOT_CONFIGURED', 'NOT_CONFIGURED'],
253
+ ['STALE', 'HEALTYH', 'STALE'],
254
+ ['STALE', 'STORE_ERROR', 'STORE_ERROR'],
255
+ ['STALE', 'NOT_CONFIGURED', 'NOT_CONFIGURED'],
256
+ ['STORE_ERROR', 'HEALTYH', 'STORE_ERROR'],
257
+ ['STORE_ERROR', 'STALE', 'STORE_ERROR'],
258
+ ['STORE_ERROR', 'NOT_CONFIGURED', 'NOT_CONFIGURED'],
259
+ ['NOT_CONFIGURED', 'HEALTYH', 'NOT_CONFIGURED'],
260
+ ['NOT_CONFIGURED', 'STALE', 'NOT_CONFIGURED'],
261
+ ['NOT_CONFIGURED', 'STORE_ERROR', 'NOT_CONFIGURED']
262
+ ])
263
+ ('worst status is returned given multiple queries with different status', async (status1, status2, result) => {
264
+ const segment = {
265
+ key: 'bigSegment1',
266
+ version: 1,
267
+ unbounded: true,
268
+ generation: 2,
269
+ unboundedContextKind: 'party'
270
+ };
271
+ const segment2 = {
272
+ key: 'bigSegment2',
273
+ version: 1,
274
+ unbounded: true,
275
+ generation: 2,
276
+ unboundedContextKind: 'org'
277
+ };
278
+
279
+ const context = {
280
+ kind: 'multi',
281
+ party: {key: 'partyKey'},
282
+ org: {key: 'orgKey'},
283
+ };
284
+ const membership1 = { [makeBigSegmentRef(segment)]: true };
285
+ const membership2 = { [makeBigSegmentRef(segment2)]: true };
286
+ const queries = prepareQueries({ segments: [segment, segment2] });
287
+ const memberships = {[context.party.key]: [membership1, status1], [context.org.key]: [membership2, status2]};
288
+ queries.getBigSegmentsMembership = (key, cb) => {
289
+ cb(memberships[key]);
290
+ };
291
+
292
+ const e = Evaluator(queries);
293
+ const flag = makeBooleanFlagWithRules([{clauses: [
294
+ makeSegmentMatchClause(segment),
295
+ makeSegmentMatchClause(segment2)
296
+ ], variation: 1}])
297
+ const [ err, detail, events ] = await asyncEvaluate(e, flag, context, eventFactory);
298
+ expect(detail.value).toBe(true);
299
+ expect(detail.reason.bigSegmentsStatus).toEqual(result);
300
+ });
301
+ });
@@ -0,0 +1,333 @@
1
+ const { isExportDeclaration } = require('typescript');
2
+ const { Evaluator, bucketContext } = require('../evaluator');
3
+ const {
4
+ eventFactory,
5
+ asyncEvaluate,
6
+ } = require('./evaluator_helpers');
7
+
8
+ describe('rollout', () => {
9
+ it('selects bucket', async () => {
10
+ const user = { key: 'userkey' };
11
+ const flagKey = 'flagkey';
12
+ const salt = 'salt';
13
+
14
+ // First verify that with our test inputs, the bucket value will be greater than zero and less than 100000,
15
+ // so we can construct a rollout whose second bucket just barely contains that value
16
+ const bucketValue = Math.floor(bucketContext(user, flagKey, 'key', salt, null, 'user')[0] * 100000);
17
+ expect(bucketValue).toBeGreaterThan(0);
18
+ expect(bucketValue).toBeLessThan(100000);
19
+
20
+ const badVariationA = 0, matchedVariation = 1, badVariationB = 2;
21
+ const rollout = {
22
+ variations: [
23
+ { variation: badVariationA, weight: bucketValue }, // end of bucket range is not inclusive, so it will *not* match the target value
24
+ { variation: matchedVariation, weight: 1 }, // size of this bucket is 1, so it only matches that specific value
25
+ { variation: badVariationB, weight: 100000 - (bucketValue + 1) }
26
+ ]
27
+ };
28
+ const flag = {
29
+ key: flagKey,
30
+ salt: salt,
31
+ on: true,
32
+ fallthrough: { rollout: rollout },
33
+ variations: [null, null, null]
34
+ };
35
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, user, eventFactory);
36
+ expect(err).toEqual(null);
37
+ expect(detail.variationIndex).toEqual(matchedVariation);
38
+ });
39
+
40
+ it('does not use the secondary key', async () => {
41
+ const userWithSecondary = { key: 'userkey', secondary: 'secondary' };
42
+ const userWithoutSecondary = { key: 'userkey' };
43
+ const flagKey = 'flagkey';
44
+ const salt = 'salt';
45
+
46
+ // The secondary attribute is no longer used, so we want to make sure a user with and without the attribute bucket
47
+ // the same.
48
+ const bucketValueWithSecondary = Math.floor(bucketContext(userWithSecondary, flagKey, 'key', salt, null, 'user')[0] * 100000);
49
+ const bucketValueWithoutSecondary = Math.floor(bucketContext(userWithoutSecondary, flagKey, 'key', salt, null, 'user')[0] * 100000);
50
+
51
+ expect(bucketValueWithSecondary).toEqual(bucketValueWithoutSecondary);
52
+ });
53
+
54
+ it('handles an invalid bucketBy', async () => {
55
+ const user = { key: 'userkey' };
56
+ const flagKey = 'flagkey';
57
+ const salt = 'salt';
58
+
59
+ const rollout = {
60
+ contextKind: 'user',
61
+ bucketBy: '//',
62
+ variations: [
63
+ { variation: 0, weight: 10000 },
64
+ ]
65
+ };
66
+ const flag = {
67
+ key: flagKey,
68
+ salt: salt,
69
+ on: true,
70
+ fallthrough: { rollout: rollout },
71
+ variations: [null, null, null]
72
+ };
73
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, user, eventFactory);
74
+ expect(err).toBeDefined();
75
+ expect(detail.reason).toEqual({ kind: 'ERROR', errorKind: 'MALFORMED_FLAG' });
76
+ expect(detail.variationIndex).toEqual(null);
77
+ });
78
+
79
+ it('selects bucket for a single kind user context', async () => {
80
+ const context = { kind: 'user', key: 'userkey' };
81
+ const flagKey = 'flagkey';
82
+ const salt = 'salt';
83
+
84
+ const [bucket, hadContext] = bucketContext(context, flagKey, 'key', salt, null, 'user');
85
+ const bucketValue = Math.floor(bucket * 100000);
86
+ expect(bucketValue).toBeGreaterThan(0);
87
+ expect(bucketValue).toBeLessThan(100000);
88
+ expect(hadContext).toEqual(true);
89
+
90
+ const badVariationA = 0, matchedVariation = 1, badVariationB = 2;
91
+ const rollout = {
92
+ variations: [
93
+ { variation: badVariationA, weight: bucketValue }, // end of bucket range is not inclusive, so it will *not* match the target value
94
+ { variation: matchedVariation, weight: 1 }, // size of this bucket is 1, so it only matches that specific value
95
+ { variation: badVariationB, weight: 100000 - (bucketValue + 1) }
96
+ ]
97
+ };
98
+ const flag = {
99
+ key: flagKey,
100
+ salt: salt,
101
+ on: true,
102
+ fallthrough: { rollout: rollout },
103
+ variations: [null, null, null]
104
+ };
105
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, context, eventFactory);
106
+ expect(err).toEqual(null);
107
+ expect(detail.variationIndex).toEqual(matchedVariation);
108
+ });
109
+
110
+ it('Uses the first bucket when the context does not contain the context kind of the rollout', async () => {
111
+ const context = { kind: 'org', key: 'orgKey' };
112
+ const flagKey = 'flagkey';
113
+ const salt = 'salt';
114
+
115
+ const [bucket, hadContext] = bucketContext(context, flagKey, 'key', salt, null, 'user')
116
+ const bucketValue = Math.floor(bucket * 100000);
117
+ expect(bucketValue).toEqual(0);
118
+ expect(hadContext).toEqual(false);
119
+
120
+ const rollout = {
121
+ contextKind: 'user',
122
+ variations: [
123
+ { variation: 0, weight: 1 }, // end of bucket range is not inclusive, so it will *not* match the target value
124
+ { variation: 1, weight: 1 }, // size of this bucket is 1, so it only matches that specific value
125
+ { variation: 2, weight: 100000 - (1 + 1) }
126
+ ]
127
+ };
128
+ const flag = {
129
+ key: flagKey,
130
+ salt: salt,
131
+ on: true,
132
+ fallthrough: { rollout: rollout },
133
+ variations: [null, null, null]
134
+ };
135
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, context, eventFactory);
136
+ expect(err).toEqual(null);
137
+ expect(detail.variationIndex).toEqual(0);
138
+ });
139
+
140
+ it('Produces a non-zero bucket for a multi-kind context which contains the desired context kind', async () => {
141
+ const context = { kind: 'org', key: 'orgKey' };
142
+ const flagKey = 'flagkey';
143
+ const salt = 'salt';
144
+
145
+ const [bucket, hadContext] = bucketContext(context, flagKey, 'key', salt, null, 'org');
146
+ const bucketValue = Math.floor(bucket * 100000);
147
+ expect(bucketValue).toBeGreaterThan(0);
148
+ expect(bucketValue).toBeLessThan(100000);
149
+ expect(hadContext).toEqual(true);
150
+
151
+ const badVariationA = 0, matchedVariation = 1, badVariationB = 2;
152
+ const rollout = {
153
+ contextKind: 'org',
154
+ variations: [
155
+ { variation: badVariationA, weight: bucketValue }, // end of bucket range is not inclusive, so it will *not* match the target value
156
+ { variation: matchedVariation, weight: 1 }, // size of this bucket is 1, so it only matches that specific value
157
+ { variation: badVariationB, weight: 100000 - (bucketValue + 1) }
158
+ ]
159
+ };
160
+ const flag = {
161
+ key: flagKey,
162
+ salt: salt,
163
+ on: true,
164
+ fallthrough: { rollout: rollout },
165
+ variations: [null, null, null]
166
+ };
167
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, context, eventFactory);
168
+ expect(err).toEqual(null);
169
+ expect(detail.variationIndex).toEqual(matchedVariation);
170
+ });
171
+
172
+ it('uses last bucket if bucket value is equal to total weight', async () => {
173
+ const user = { key: 'userkey' };
174
+ const flagKey = 'flagkey';
175
+ const salt = 'salt';
176
+
177
+ // We'll construct a list of variations that stops right at the target bucket value
178
+ const bucketValue = Math.floor(bucketContext(user, flagKey, 'key', salt)[0] * 100000);
179
+
180
+ const rollout = {
181
+ variations: [{ variation: 0, weight: bucketValue }]
182
+ };
183
+ const flag = {
184
+ key: flagKey,
185
+ salt: salt,
186
+ on: true,
187
+ fallthrough: { rollout: rollout },
188
+ variations: [null, null, null]
189
+ };
190
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, user, eventFactory);
191
+ expect(err).toEqual(null);
192
+ expect(detail.variationIndex).toEqual(0);
193
+ });
194
+
195
+ describe('with seed', () => {
196
+ const seed = 61;
197
+ const flagKey = 'flagkey';
198
+ const salt = 'salt';
199
+ const rollout = {
200
+ kind: 'experiment',
201
+ seed,
202
+ variations: [
203
+ { variation: 0, weight: 10000 },
204
+ { variation: 1, weight: 20000 },
205
+ { variation: 0, weight: 70000, untracked: true },
206
+ ],
207
+ };
208
+ const flag = {
209
+ key: flagKey,
210
+ salt: salt,
211
+ on: true,
212
+ fallthrough: { rollout: rollout },
213
+ variations: [null, null, null],
214
+ };
215
+
216
+ it('buckets user into first variant of the experiment', async () => {
217
+ const user = { key: 'userKeyA' };
218
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, user, eventFactory);
219
+ expect(err).toEqual(null);
220
+ expect(detail.variationIndex).toEqual(0);
221
+ expect(detail.reason.inExperiment).toBe(true);
222
+ });
223
+
224
+ it('inExperiment is not set when the context kind is not present', async () => {
225
+ const user = { kind: 'org', key: 'userKeyA' };
226
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, user, eventFactory);
227
+ expect(err).toEqual(null);
228
+ expect(detail.variationIndex).toEqual(0);
229
+ expect(detail.reason.inExperiment).toBeUndefined();
230
+ });
231
+
232
+ it('uses seed to bucket user into second variant of the experiment', async () => {
233
+ const user = { key: 'userKeyB' };
234
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, user, eventFactory);
235
+ expect(err).toEqual(null);
236
+ expect(detail.variationIndex).toEqual(1);
237
+ expect(detail.reason.inExperiment).toBe(true);
238
+ });
239
+
240
+ it('buckets user outside of the experiment', async () => {
241
+ const user = { key: 'userKeyC' };
242
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), flag, user, eventFactory);
243
+ expect(err).toEqual(null);
244
+ expect(detail.variationIndex).toEqual(0);
245
+ expect(detail.reason.inExperiment).toBe(undefined);
246
+ });
247
+
248
+ it('does not use bucketBy for experiments', async () => {
249
+ const user = { key: 'userKeyA', kind: 'user', mimic: 'userKeyC' };
250
+ const bucketByFlag = JSON.parse(JSON.stringify(flag));
251
+ bucketByFlag.fallthrough.rollout.bucketBy = "mimic";
252
+ const [err, detail, events] = await asyncEvaluate(Evaluator(), bucketByFlag, user, eventFactory);
253
+ expect(err).toEqual(null);
254
+ expect(detail.variationIndex).toEqual(0);
255
+ expect(detail.reason.inExperiment).toBe(true);
256
+ });
257
+ });
258
+ });
259
+
260
+ describe('bucketContext', () => {
261
+ it('gets expected bucket values for specific keys', () => {
262
+ let user = { key: 'userKeyA' };
263
+ let [bucket] = bucketContext(user, 'hashKey', 'key', 'saltyA', null, 'user');
264
+ expect(bucket).toBeCloseTo(0.42157587, 7);
265
+
266
+ user = { key: 'userKeyB' };
267
+ [bucket] = bucketContext(user, 'hashKey', 'key', 'saltyA', null, 'user');
268
+ expect(bucket).toBeCloseTo(0.6708485, 7);
269
+
270
+ user = { key: 'userKeyC' };
271
+ [bucket] = bucketContext(user, 'hashKey', 'key', 'saltyA', null, 'user');
272
+ expect(bucket).toBeCloseTo(0.10343106, 7);
273
+ });
274
+
275
+ it('can bucket by int value (equivalent to string)', () => {
276
+ const user = {
277
+ key: 'userKey',
278
+ custom: {
279
+ intAttr: 33333,
280
+ stringAttr: '33333'
281
+ }
282
+ };
283
+ const [bucket] = bucketContext(user, 'hashKey', 'intAttr', 'saltyA', null, 'user');
284
+ const [bucket2] = bucketContext(user, 'hashKey', 'stringAttr', 'saltyA', null, 'user');
285
+ expect(bucket).toBeCloseTo(0.54771423, 7);
286
+ expect(bucket2).toBe(bucket);
287
+ });
288
+
289
+ it('cannot bucket by float value', () => {
290
+ const user = {
291
+ key: 'userKey',
292
+ custom: {
293
+ floatAttr: 33.5
294
+ }
295
+ };
296
+ const [bucket] = bucketContext(user, 'hashKey', 'floatAttr', 'saltyA', null, 'user');
297
+ expect(bucket).toBe(0);
298
+ });
299
+ });
300
+
301
+ describe('when seed is present', () => {
302
+ const seed = 61;
303
+ it('gets expected bucket values for specific keys', () => {
304
+ let user = { key: 'userKeyA' };
305
+ let [bucket] = bucketContext(user, 'hashKey', 'key', 'saltyA', seed, 'user');
306
+ expect(bucket).toBeCloseTo(0.09801207, 7);
307
+
308
+ user = { key: 'userKeyB' };
309
+ [bucket] = bucketContext(user, 'hashKey', 'key', 'saltyA', seed, 'user');
310
+ expect(bucket).toBeCloseTo(0.14483777, 7);
311
+
312
+ user = { key: 'userKeyC' };
313
+ [bucket] = bucketContext(user, 'hashKey', 'key', 'saltyA', seed, 'user');
314
+ expect(bucket).toBeCloseTo(0.9242641, 7);
315
+ });
316
+
317
+ it('should not generate a different bucket when hashKey or salt are changed', () => {
318
+ let user = { key: 'userKeyA' };
319
+ let [bucket] = bucketContext(user, 'hashKey', 'key', 'saltyA', seed, 'user');
320
+ let [bucketDifferentHashKey] = bucketContext(user, 'otherHashKey', 'key', 'saltyA', seed, 'user');
321
+ let [bucketDifferentSalt] = bucketContext(user, 'hashKey', 'key', 'otherSaltyA', seed, 'user');
322
+
323
+ expect(bucketDifferentHashKey).toBeCloseTo(bucket, 7);
324
+ expect(bucketDifferentSalt).toBeCloseTo(bucket, 7);
325
+ });
326
+
327
+ it('should generate a new bucket if the seed changes', () => {
328
+ const otherSeed = 60;
329
+ const user = { key: 'userKeyA' };
330
+ const [bucket] = bucketContext(user, 'hashKey', 'key', 'saltyA', otherSeed, 'user');
331
+ expect(bucket).toBeCloseTo(0.7008816, 7);
332
+ });
333
+ });