posthog-node 2.0.2 → 2.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/CHANGELOG.md +11 -1
- package/lib/index.cjs.js +183 -32
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +22 -0
- package/lib/index.esm.js +182 -31
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +2 -0
- package/lib/posthog-core/src/types.d.ts +8 -1
- package/lib/posthog-node/src/feature-flags.d.ts +5 -3
- package/lib/posthog-node/src/fetch.d.ts +2 -0
- package/lib/posthog-node/src/posthog-node.d.ts +2 -1
- package/lib/posthog-node/src/types.d.ts +1 -0
- package/package.json +4 -4
- package/src/feature-flags.ts +84 -22
- package/src/fetch.ts +20 -0
- package/src/posthog-node.ts +6 -3
- package/src/types.ts +1 -0
- package/test/feature-flags.spec.ts +442 -115
- package/test/posthog-node.spec.ts +41 -40
|
@@ -2,57 +2,47 @@
|
|
|
2
2
|
// Uncomment below line while developing to not compile code everytime
|
|
3
3
|
import { PostHog as PostHog } from '../src/posthog-node'
|
|
4
4
|
import { matchProperty, InconclusiveMatchError } from '../src/feature-flags'
|
|
5
|
-
jest.mock('
|
|
6
|
-
import
|
|
5
|
+
jest.mock('../src/fetch')
|
|
6
|
+
import { fetch } from '../src/fetch'
|
|
7
7
|
|
|
8
8
|
jest.spyOn(global.console, 'debug').mockImplementation()
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
export const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}) as any
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return Promise.resolve({
|
|
26
|
-
statusCode: 401,
|
|
27
|
-
body: {
|
|
28
|
-
text: () => Promise.resolve('ok'),
|
|
29
|
-
json: () =>
|
|
30
|
-
Promise.resolve({
|
|
31
|
-
statusCode: 'ok',
|
|
32
|
-
}),
|
|
33
|
-
},
|
|
34
|
-
}) as any
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const decideImplementation =
|
|
38
|
-
(flags: any, decideStatus: number = 200) =>
|
|
39
|
-
(url: any): Promise<any> => {
|
|
10
|
+
const mockedFetch = jest.mocked(fetch, true)
|
|
11
|
+
|
|
12
|
+
export const apiImplementation = ({
|
|
13
|
+
localFlags,
|
|
14
|
+
decideFlags,
|
|
15
|
+
decideStatus = 200,
|
|
16
|
+
}: {
|
|
17
|
+
localFlags?: any
|
|
18
|
+
decideFlags?: any
|
|
19
|
+
decideStatus?: number
|
|
20
|
+
}) => {
|
|
21
|
+
return (url: any): Promise<any> => {
|
|
40
22
|
if ((url as any).includes('/decide/')) {
|
|
41
23
|
return Promise.resolve({
|
|
42
24
|
status: decideStatus,
|
|
43
25
|
text: () => Promise.resolve('ok'),
|
|
44
26
|
json: () => {
|
|
45
27
|
if (decideStatus !== 200) {
|
|
46
|
-
return Promise.resolve(
|
|
28
|
+
return Promise.resolve(decideFlags)
|
|
47
29
|
} else {
|
|
48
30
|
return Promise.resolve({
|
|
49
|
-
featureFlags:
|
|
31
|
+
featureFlags: decideFlags,
|
|
50
32
|
})
|
|
51
33
|
}
|
|
52
34
|
},
|
|
53
35
|
}) as any
|
|
54
36
|
}
|
|
55
37
|
|
|
38
|
+
if ((url as any).includes('api/feature_flag/local_evaluation?token=TEST_API_KEY')) {
|
|
39
|
+
return Promise.resolve({
|
|
40
|
+
status: 200,
|
|
41
|
+
text: () => Promise.resolve('ok'),
|
|
42
|
+
json: () => Promise.resolve(localFlags),
|
|
43
|
+
}) as any
|
|
44
|
+
}
|
|
45
|
+
|
|
56
46
|
return Promise.resolve({
|
|
57
47
|
status: 400,
|
|
58
48
|
text: () => Promise.resolve('ok'),
|
|
@@ -62,6 +52,13 @@ export const decideImplementation =
|
|
|
62
52
|
}),
|
|
63
53
|
}) as any
|
|
64
54
|
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const anyLocalEvalCall = [
|
|
58
|
+
'http://example.com/api/feature_flag/local_evaluation?token=TEST_API_KEY',
|
|
59
|
+
expect.any(Object),
|
|
60
|
+
]
|
|
61
|
+
export const anyDecideCall = ['http://example.com/decide/?v=2', expect.any(Object)]
|
|
65
62
|
|
|
66
63
|
describe('local evaluation', () => {
|
|
67
64
|
let posthog: PostHog
|
|
@@ -100,7 +97,7 @@ describe('local evaluation', () => {
|
|
|
100
97
|
},
|
|
101
98
|
],
|
|
102
99
|
}
|
|
103
|
-
|
|
100
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
|
|
104
101
|
|
|
105
102
|
posthog = new PostHog('TEST_API_KEY', {
|
|
106
103
|
host: 'http://example.com',
|
|
@@ -113,7 +110,7 @@ describe('local evaluation', () => {
|
|
|
113
110
|
expect(
|
|
114
111
|
await posthog.getFeatureFlag('person-flag', 'some-distinct-id', { personProperties: { region: 'Canada' } })
|
|
115
112
|
).toEqual(false)
|
|
116
|
-
expect(
|
|
113
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
117
114
|
})
|
|
118
115
|
|
|
119
116
|
it('evaluates group properties', async () => {
|
|
@@ -146,7 +143,7 @@ describe('local evaluation', () => {
|
|
|
146
143
|
],
|
|
147
144
|
group_type_mapping: { '0': 'company', '1': 'project' },
|
|
148
145
|
}
|
|
149
|
-
|
|
146
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
|
|
150
147
|
|
|
151
148
|
posthog = new PostHog('TEST_API_KEY', {
|
|
152
149
|
host: 'http://example.com',
|
|
@@ -189,9 +186,9 @@ describe('local evaluation', () => {
|
|
|
189
186
|
})
|
|
190
187
|
).toEqual(false)
|
|
191
188
|
|
|
192
|
-
expect(
|
|
189
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
193
190
|
// decide not called
|
|
194
|
-
expect(
|
|
191
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
195
192
|
})
|
|
196
193
|
|
|
197
194
|
it('evaluates group properties and falls back to decide when group_type_mappings not present', async () => {
|
|
@@ -224,9 +221,9 @@ describe('local evaluation', () => {
|
|
|
224
221
|
],
|
|
225
222
|
// "group_type_mapping": {"0": "company", "1": "project"}
|
|
226
223
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
mockedFetch.mockImplementation(
|
|
225
|
+
apiImplementation({ localFlags: flags, decideFlags: { 'group-flag': 'decide-fallback-value' } })
|
|
226
|
+
)
|
|
230
227
|
|
|
231
228
|
posthog = new PostHog('TEST_API_KEY', {
|
|
232
229
|
host: 'http://example.com',
|
|
@@ -297,9 +294,9 @@ describe('local evaluation', () => {
|
|
|
297
294
|
},
|
|
298
295
|
],
|
|
299
296
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
297
|
+
mockedFetch.mockImplementation(
|
|
298
|
+
apiImplementation({ localFlags: flags, decideFlags: { 'complex-flag': 'decide-fallback-value' } })
|
|
299
|
+
)
|
|
303
300
|
|
|
304
301
|
posthog = new PostHog('TEST_API_KEY', {
|
|
305
302
|
host: 'http://example.com',
|
|
@@ -311,7 +308,7 @@ describe('local evaluation', () => {
|
|
|
311
308
|
personProperties: { region: 'USA', name: 'Aloha' },
|
|
312
309
|
})
|
|
313
310
|
).toEqual(true)
|
|
314
|
-
expect(
|
|
311
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
315
312
|
|
|
316
313
|
// # this distinctIDs hash is < rollout %
|
|
317
314
|
expect(
|
|
@@ -319,7 +316,7 @@ describe('local evaluation', () => {
|
|
|
319
316
|
personProperties: { region: 'USA', email: 'a@b.com' },
|
|
320
317
|
})
|
|
321
318
|
).toEqual(true)
|
|
322
|
-
expect(
|
|
319
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
323
320
|
|
|
324
321
|
// # will fall back on `/decide`, as all properties present for second group, but that group resolves to false
|
|
325
322
|
expect(
|
|
@@ -327,7 +324,7 @@ describe('local evaluation', () => {
|
|
|
327
324
|
personProperties: { region: 'USA', email: 'a@b.com' },
|
|
328
325
|
})
|
|
329
326
|
).toEqual('decide-fallback-value')
|
|
330
|
-
expect(
|
|
327
|
+
expect(mockedFetch).toHaveBeenCalledWith(
|
|
331
328
|
'http://example.com/decide/?v=2',
|
|
332
329
|
expect.objectContaining({
|
|
333
330
|
body: JSON.stringify({
|
|
@@ -339,13 +336,13 @@ describe('local evaluation', () => {
|
|
|
339
336
|
}),
|
|
340
337
|
})
|
|
341
338
|
)
|
|
342
|
-
|
|
339
|
+
mockedFetch.mockClear()
|
|
343
340
|
|
|
344
341
|
// # same as above
|
|
345
342
|
expect(
|
|
346
343
|
await posthog.getFeatureFlag('complex-flag', 'some-distinct-id', { personProperties: { doesnt_matter: '1' } })
|
|
347
344
|
).toEqual('decide-fallback-value')
|
|
348
|
-
expect(
|
|
345
|
+
expect(mockedFetch).toHaveBeenCalledWith(
|
|
349
346
|
'http://example.com/decide/?v=2',
|
|
350
347
|
expect.objectContaining({
|
|
351
348
|
body: JSON.stringify({
|
|
@@ -357,13 +354,13 @@ describe('local evaluation', () => {
|
|
|
357
354
|
}),
|
|
358
355
|
})
|
|
359
356
|
)
|
|
360
|
-
|
|
357
|
+
mockedFetch.mockClear()
|
|
361
358
|
|
|
362
359
|
expect(
|
|
363
360
|
await posthog.getFeatureFlag('complex-flag', 'some-distinct-id', { personProperties: { region: 'USA' } })
|
|
364
361
|
).toEqual('decide-fallback-value')
|
|
365
|
-
expect(
|
|
366
|
-
|
|
362
|
+
expect(mockedFetch).toHaveBeenCalledTimes(1) // TODO: Check this
|
|
363
|
+
mockedFetch.mockClear()
|
|
367
364
|
|
|
368
365
|
// # won't need to fallback when all values are present, and resolves to False
|
|
369
366
|
expect(
|
|
@@ -371,7 +368,7 @@ describe('local evaluation', () => {
|
|
|
371
368
|
personProperties: { region: 'USA', email: 'a@b.com', name: 'X', doesnt_matter: '1' },
|
|
372
369
|
})
|
|
373
370
|
).toEqual(false)
|
|
374
|
-
expect(
|
|
371
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
375
372
|
})
|
|
376
373
|
|
|
377
374
|
it('falls back to decide', async () => {
|
|
@@ -414,10 +411,11 @@ describe('local evaluation', () => {
|
|
|
414
411
|
},
|
|
415
412
|
],
|
|
416
413
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
414
|
+
mockedFetch.mockImplementation(
|
|
415
|
+
apiImplementation({
|
|
416
|
+
localFlags: flags,
|
|
417
|
+
decideFlags: { 'beta-feature': 'alakazam', 'beta-feature2': 'alakazam2' },
|
|
418
|
+
})
|
|
421
419
|
)
|
|
422
420
|
|
|
423
421
|
posthog = new PostHog('TEST_API_KEY', {
|
|
@@ -427,12 +425,12 @@ describe('local evaluation', () => {
|
|
|
427
425
|
|
|
428
426
|
// # beta-feature fallbacks to decide because property type is unknown
|
|
429
427
|
expect(await posthog.getFeatureFlag('beta-feature', 'some-distinct-id')).toEqual('alakazam')
|
|
430
|
-
expect(
|
|
431
|
-
|
|
428
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
|
|
429
|
+
mockedFetch.mockClear()
|
|
432
430
|
|
|
433
431
|
// # beta-feature2 fallbacks to decide because region property not given with call
|
|
434
432
|
expect(await posthog.getFeatureFlag('beta-feature2', 'some-distinct-id')).toEqual('alakazam2')
|
|
435
|
-
expect(
|
|
433
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
|
|
436
434
|
})
|
|
437
435
|
|
|
438
436
|
it('dont fall back to decide when local evaluation is set', async () => {
|
|
@@ -475,10 +473,11 @@ describe('local evaluation', () => {
|
|
|
475
473
|
},
|
|
476
474
|
],
|
|
477
475
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
476
|
+
mockedFetch.mockImplementation(
|
|
477
|
+
apiImplementation({
|
|
478
|
+
localFlags: flags,
|
|
479
|
+
decideFlags: { 'beta-feature': 'alakazam', 'beta-feature2': 'alakazam2' },
|
|
480
|
+
})
|
|
482
481
|
)
|
|
483
482
|
|
|
484
483
|
posthog = new PostHog('TEST_API_KEY', {
|
|
@@ -494,7 +493,7 @@ describe('local evaluation', () => {
|
|
|
494
493
|
expect(await posthog.isFeatureEnabled('beta-feature', 'some-distinct-id', { onlyEvaluateLocally: true })).toEqual(
|
|
495
494
|
undefined
|
|
496
495
|
)
|
|
497
|
-
expect(
|
|
496
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
498
497
|
|
|
499
498
|
// # beta-feature2 should fallback to decide because region property not given with call
|
|
500
499
|
// # but doesn't because only_evaluate_locally is true
|
|
@@ -504,7 +503,7 @@ describe('local evaluation', () => {
|
|
|
504
503
|
expect(await posthog.isFeatureEnabled('beta-feature2', 'some-distinct-id', { onlyEvaluateLocally: true })).toEqual(
|
|
505
504
|
undefined
|
|
506
505
|
)
|
|
507
|
-
expect(
|
|
506
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
508
507
|
})
|
|
509
508
|
|
|
510
509
|
it("doesn't return undefined when flag is evaluated successfully", async () => {
|
|
@@ -527,9 +526,7 @@ describe('local evaluation', () => {
|
|
|
527
526
|
},
|
|
528
527
|
],
|
|
529
528
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
mockedUndici.fetch.mockImplementation(decideImplementation({}))
|
|
529
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags, decideFlags: {} }))
|
|
533
530
|
|
|
534
531
|
posthog = new PostHog('TEST_API_KEY', {
|
|
535
532
|
host: 'http://example.com',
|
|
@@ -539,12 +536,12 @@ describe('local evaluation', () => {
|
|
|
539
536
|
// # beta-feature resolves to False
|
|
540
537
|
expect(await posthog.getFeatureFlag('beta-feature', 'some-distinct-id')).toEqual(false)
|
|
541
538
|
expect(await posthog.isFeatureEnabled('beta-feature', 'some-distinct-id')).toEqual(false)
|
|
542
|
-
expect(
|
|
539
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
543
540
|
|
|
544
541
|
// # beta-feature2 falls back to decide, and whatever decide returns is the value
|
|
545
542
|
expect(await posthog.getFeatureFlag('beta-feature2', 'some-distinct-id')).toEqual(false)
|
|
546
543
|
expect(await posthog.isFeatureEnabled('beta-feature2', 'some-distinct-id')).toEqual(false)
|
|
547
|
-
expect(
|
|
544
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
|
|
548
545
|
})
|
|
549
546
|
|
|
550
547
|
it('returns undefined when decide errors out', async () => {
|
|
@@ -567,9 +564,9 @@ describe('local evaluation', () => {
|
|
|
567
564
|
},
|
|
568
565
|
],
|
|
569
566
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
567
|
+
mockedFetch.mockImplementation(
|
|
568
|
+
apiImplementation({ localFlags: flags, decideFlags: { error: 'went wrong' }, decideStatus: 400 })
|
|
569
|
+
)
|
|
573
570
|
|
|
574
571
|
posthog = new PostHog('TEST_API_KEY', {
|
|
575
572
|
host: 'http://example.com',
|
|
@@ -579,7 +576,7 @@ describe('local evaluation', () => {
|
|
|
579
576
|
// # beta-feature2 falls back to decide, which on error returns undefined
|
|
580
577
|
expect(await posthog.getFeatureFlag('beta-feature2', 'some-distinct-id')).toEqual(undefined)
|
|
581
578
|
expect(await posthog.isFeatureEnabled('beta-feature2', 'some-distinct-id')).toEqual(undefined)
|
|
582
|
-
expect(
|
|
579
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
|
|
583
580
|
})
|
|
584
581
|
|
|
585
582
|
it('experience continuity flags are not evaluated locally', async () => {
|
|
@@ -603,9 +600,9 @@ describe('local evaluation', () => {
|
|
|
603
600
|
},
|
|
604
601
|
],
|
|
605
602
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
603
|
+
mockedFetch.mockImplementation(
|
|
604
|
+
apiImplementation({ localFlags: flags, decideFlags: { 'beta-feature': 'decide-fallback-value' } })
|
|
605
|
+
)
|
|
609
606
|
|
|
610
607
|
posthog = new PostHog('TEST_API_KEY', {
|
|
611
608
|
host: 'http://example.com',
|
|
@@ -614,7 +611,7 @@ describe('local evaluation', () => {
|
|
|
614
611
|
|
|
615
612
|
// # beta-feature2 falls back to decide, which on error returns default
|
|
616
613
|
expect(await posthog.getFeatureFlag('beta-feature', 'some-distinct-id')).toEqual('decide-fallback-value')
|
|
617
|
-
expect(
|
|
614
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
|
|
618
615
|
})
|
|
619
616
|
|
|
620
617
|
it('get all flags with fallback', async () => {
|
|
@@ -668,10 +665,11 @@ describe('local evaluation', () => {
|
|
|
668
665
|
},
|
|
669
666
|
],
|
|
670
667
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
668
|
+
mockedFetch.mockImplementation(
|
|
669
|
+
apiImplementation({
|
|
670
|
+
localFlags: flags,
|
|
671
|
+
decideFlags: { 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' },
|
|
672
|
+
})
|
|
675
673
|
)
|
|
676
674
|
|
|
677
675
|
posthog = new PostHog('TEST_API_KEY', {
|
|
@@ -685,8 +683,8 @@ describe('local evaluation', () => {
|
|
|
685
683
|
'beta-feature2': 'variant-2',
|
|
686
684
|
'disabled-feature': false,
|
|
687
685
|
})
|
|
688
|
-
expect(
|
|
689
|
-
|
|
686
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
|
|
687
|
+
mockedFetch.mockClear()
|
|
690
688
|
})
|
|
691
689
|
|
|
692
690
|
it('get all flags with fallback but only_locally_evaluated set', async () => {
|
|
@@ -740,10 +738,11 @@ describe('local evaluation', () => {
|
|
|
740
738
|
},
|
|
741
739
|
],
|
|
742
740
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
741
|
+
mockedFetch.mockImplementation(
|
|
742
|
+
apiImplementation({
|
|
743
|
+
localFlags: flags,
|
|
744
|
+
decideFlags: { 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' },
|
|
745
|
+
})
|
|
747
746
|
)
|
|
748
747
|
|
|
749
748
|
posthog = new PostHog('TEST_API_KEY', {
|
|
@@ -756,17 +755,18 @@ describe('local evaluation', () => {
|
|
|
756
755
|
'beta-feature': true,
|
|
757
756
|
'disabled-feature': false,
|
|
758
757
|
})
|
|
759
|
-
expect(
|
|
758
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
760
759
|
})
|
|
761
760
|
|
|
762
761
|
it('get all flags with fallback, with no local flags', async () => {
|
|
763
762
|
const flags = {
|
|
764
763
|
flags: [],
|
|
765
764
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
765
|
+
mockedFetch.mockImplementation(
|
|
766
|
+
apiImplementation({
|
|
767
|
+
localFlags: flags,
|
|
768
|
+
decideFlags: { 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' },
|
|
769
|
+
})
|
|
770
770
|
)
|
|
771
771
|
|
|
772
772
|
posthog = new PostHog('TEST_API_KEY', {
|
|
@@ -778,8 +778,8 @@ describe('local evaluation', () => {
|
|
|
778
778
|
'beta-feature': 'variant-1',
|
|
779
779
|
'beta-feature2': 'variant-2',
|
|
780
780
|
})
|
|
781
|
-
expect(
|
|
782
|
-
|
|
781
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
|
|
782
|
+
mockedFetch.mockClear()
|
|
783
783
|
})
|
|
784
784
|
|
|
785
785
|
it('get all flags with no fallback', async () => {
|
|
@@ -818,10 +818,11 @@ describe('local evaluation', () => {
|
|
|
818
818
|
},
|
|
819
819
|
],
|
|
820
820
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
821
|
+
mockedFetch.mockImplementation(
|
|
822
|
+
apiImplementation({
|
|
823
|
+
localFlags: flags,
|
|
824
|
+
decideFlags: { 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' },
|
|
825
|
+
})
|
|
825
826
|
)
|
|
826
827
|
|
|
827
828
|
posthog = new PostHog('TEST_API_KEY', {
|
|
@@ -830,7 +831,7 @@ describe('local evaluation', () => {
|
|
|
830
831
|
})
|
|
831
832
|
|
|
832
833
|
expect(await posthog.getAllFlags('distinct-id')).toEqual({ 'beta-feature': true, 'disabled-feature': false })
|
|
833
|
-
expect(
|
|
834
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
834
835
|
})
|
|
835
836
|
|
|
836
837
|
it('computes inactive flags locally as well', async () => {
|
|
@@ -869,10 +870,11 @@ describe('local evaluation', () => {
|
|
|
869
870
|
},
|
|
870
871
|
],
|
|
871
872
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
873
|
+
mockedFetch.mockImplementation(
|
|
874
|
+
apiImplementation({
|
|
875
|
+
localFlags: flags,
|
|
876
|
+
decideFlags: { 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' },
|
|
877
|
+
})
|
|
876
878
|
)
|
|
877
879
|
|
|
878
880
|
posthog = new PostHog('TEST_API_KEY', {
|
|
@@ -881,7 +883,7 @@ describe('local evaluation', () => {
|
|
|
881
883
|
})
|
|
882
884
|
|
|
883
885
|
expect(await posthog.getAllFlags('distinct-id')).toEqual({ 'beta-feature': true, 'disabled-feature': false })
|
|
884
|
-
expect(
|
|
886
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
885
887
|
|
|
886
888
|
// # Now, after a poll interval, flag 1 is inactive, and flag 2 rollout is set to 100%.
|
|
887
889
|
const flags2 = {
|
|
@@ -919,13 +921,306 @@ describe('local evaluation', () => {
|
|
|
919
921
|
},
|
|
920
922
|
],
|
|
921
923
|
}
|
|
922
|
-
|
|
924
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags2 }))
|
|
923
925
|
|
|
924
926
|
// # force reload to simulate poll interval
|
|
925
927
|
await posthog.reloadFeatureFlags()
|
|
926
928
|
|
|
927
929
|
expect(await posthog.getAllFlags('distinct-id')).toEqual({ 'beta-feature': false, 'disabled-feature': true })
|
|
928
|
-
expect(
|
|
930
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
it('gets feature flag with variant overrides', async () => {
|
|
934
|
+
const flags = {
|
|
935
|
+
flags: [
|
|
936
|
+
{
|
|
937
|
+
id: 1,
|
|
938
|
+
name: 'Beta Feature',
|
|
939
|
+
key: 'beta-feature',
|
|
940
|
+
is_simple_flag: true,
|
|
941
|
+
active: true,
|
|
942
|
+
filters: {
|
|
943
|
+
groups: [
|
|
944
|
+
{
|
|
945
|
+
properties: [
|
|
946
|
+
{
|
|
947
|
+
key: 'email',
|
|
948
|
+
operator: 'exact',
|
|
949
|
+
value: 'test@posthog.com',
|
|
950
|
+
type: 'person',
|
|
951
|
+
},
|
|
952
|
+
],
|
|
953
|
+
rollout_percentage: 100,
|
|
954
|
+
variant: 'second-variant',
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
rollout_percentage: 50,
|
|
958
|
+
variant: 'first-variant',
|
|
959
|
+
},
|
|
960
|
+
],
|
|
961
|
+
multivariate: {
|
|
962
|
+
variants: [
|
|
963
|
+
{
|
|
964
|
+
key: 'first-variant',
|
|
965
|
+
name: 'First Variant',
|
|
966
|
+
rollout_percentage: 50,
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
key: 'second-variant',
|
|
970
|
+
name: 'Second Variant',
|
|
971
|
+
rollout_percentage: 25,
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
key: 'third-variant',
|
|
975
|
+
name: 'Third Variant',
|
|
976
|
+
rollout_percentage: 25,
|
|
977
|
+
},
|
|
978
|
+
],
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
],
|
|
983
|
+
}
|
|
984
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
|
|
985
|
+
|
|
986
|
+
posthog = new PostHog('TEST_API_KEY', {
|
|
987
|
+
host: 'http://example.com',
|
|
988
|
+
personalApiKey: 'TEST_PERSONAL_API_KEY',
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
expect(
|
|
992
|
+
await posthog.getFeatureFlag('beta-feature', 'test_id', { personProperties: { email: 'test@posthog.com' } })
|
|
993
|
+
).toEqual('second-variant')
|
|
994
|
+
expect(await posthog.getFeatureFlag('beta-feature', 'example_id')).toEqual('first-variant')
|
|
995
|
+
|
|
996
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
997
|
+
// decide not called
|
|
998
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
it('gets feature flag with clashing variant overrides', async () => {
|
|
1002
|
+
const flags = {
|
|
1003
|
+
flags: [
|
|
1004
|
+
{
|
|
1005
|
+
id: 1,
|
|
1006
|
+
name: 'Beta Feature',
|
|
1007
|
+
key: 'beta-feature',
|
|
1008
|
+
is_simple_flag: true,
|
|
1009
|
+
active: true,
|
|
1010
|
+
filters: {
|
|
1011
|
+
groups: [
|
|
1012
|
+
{
|
|
1013
|
+
properties: [
|
|
1014
|
+
{
|
|
1015
|
+
key: 'email',
|
|
1016
|
+
operator: 'exact',
|
|
1017
|
+
value: 'test@posthog.com',
|
|
1018
|
+
type: 'person',
|
|
1019
|
+
},
|
|
1020
|
+
],
|
|
1021
|
+
rollout_percentage: 100,
|
|
1022
|
+
variant: 'second-variant',
|
|
1023
|
+
},
|
|
1024
|
+
// # since second-variant comes first in the list, it will be the one that gets picked
|
|
1025
|
+
{
|
|
1026
|
+
properties: [
|
|
1027
|
+
{
|
|
1028
|
+
key: 'email',
|
|
1029
|
+
operator: 'exact',
|
|
1030
|
+
value: 'test@posthog.com',
|
|
1031
|
+
type: 'person',
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
rollout_percentage: 100,
|
|
1035
|
+
variant: 'first-variant',
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
rollout_percentage: 50,
|
|
1039
|
+
variant: 'first-variant',
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
multivariate: {
|
|
1043
|
+
variants: [
|
|
1044
|
+
{
|
|
1045
|
+
key: 'first-variant',
|
|
1046
|
+
name: 'First Variant',
|
|
1047
|
+
rollout_percentage: 50,
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
key: 'second-variant',
|
|
1051
|
+
name: 'Second Variant',
|
|
1052
|
+
rollout_percentage: 25,
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
key: 'third-variant',
|
|
1056
|
+
name: 'Third Variant',
|
|
1057
|
+
rollout_percentage: 25,
|
|
1058
|
+
},
|
|
1059
|
+
],
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
],
|
|
1064
|
+
}
|
|
1065
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
|
|
1066
|
+
|
|
1067
|
+
posthog = new PostHog('TEST_API_KEY', {
|
|
1068
|
+
host: 'http://example.com',
|
|
1069
|
+
personalApiKey: 'TEST_PERSONAL_API_KEY',
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
expect(
|
|
1073
|
+
await posthog.getFeatureFlag('beta-feature', 'test_id', { personProperties: { email: 'test@posthog.com' } })
|
|
1074
|
+
).toEqual('second-variant')
|
|
1075
|
+
expect(
|
|
1076
|
+
await posthog.getFeatureFlag('beta-feature', 'example_id', { personProperties: { email: 'test@posthog.com' } })
|
|
1077
|
+
).toEqual('second-variant')
|
|
1078
|
+
expect(await posthog.getFeatureFlag('beta-feature', 'example_id')).toEqual('first-variant')
|
|
1079
|
+
|
|
1080
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
1081
|
+
// decide not called
|
|
1082
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
it('gets feature flag with invalid variant overrides', async () => {
|
|
1086
|
+
const flags = {
|
|
1087
|
+
flags: [
|
|
1088
|
+
{
|
|
1089
|
+
id: 1,
|
|
1090
|
+
name: 'Beta Feature',
|
|
1091
|
+
key: 'beta-feature',
|
|
1092
|
+
is_simple_flag: true,
|
|
1093
|
+
active: true,
|
|
1094
|
+
filters: {
|
|
1095
|
+
groups: [
|
|
1096
|
+
{
|
|
1097
|
+
properties: [
|
|
1098
|
+
{
|
|
1099
|
+
key: 'email',
|
|
1100
|
+
operator: 'exact',
|
|
1101
|
+
value: 'test@posthog.com',
|
|
1102
|
+
type: 'person',
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
rollout_percentage: 100,
|
|
1106
|
+
variant: 'second???',
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
rollout_percentage: 50,
|
|
1110
|
+
variant: 'first???',
|
|
1111
|
+
},
|
|
1112
|
+
],
|
|
1113
|
+
multivariate: {
|
|
1114
|
+
variants: [
|
|
1115
|
+
{
|
|
1116
|
+
key: 'first-variant',
|
|
1117
|
+
name: 'First Variant',
|
|
1118
|
+
rollout_percentage: 50,
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
key: 'second-variant',
|
|
1122
|
+
name: 'Second Variant',
|
|
1123
|
+
rollout_percentage: 25,
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
key: 'third-variant',
|
|
1127
|
+
name: 'Third Variant',
|
|
1128
|
+
rollout_percentage: 25,
|
|
1129
|
+
},
|
|
1130
|
+
],
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
],
|
|
1135
|
+
}
|
|
1136
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
|
|
1137
|
+
|
|
1138
|
+
posthog = new PostHog('TEST_API_KEY', {
|
|
1139
|
+
host: 'http://example.com',
|
|
1140
|
+
personalApiKey: 'TEST_PERSONAL_API_KEY',
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
expect(
|
|
1144
|
+
await posthog.getFeatureFlag('beta-feature', 'test_id', { personProperties: { email: 'test@posthog.com' } })
|
|
1145
|
+
).toEqual('third-variant')
|
|
1146
|
+
expect(await posthog.getFeatureFlag('beta-feature', 'example_id')).toEqual('second-variant')
|
|
1147
|
+
|
|
1148
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
1149
|
+
// decide not called
|
|
1150
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
it('gets feature flag with multiple variant overrides', async () => {
|
|
1154
|
+
const flags = {
|
|
1155
|
+
flags: [
|
|
1156
|
+
{
|
|
1157
|
+
id: 1,
|
|
1158
|
+
name: 'Beta Feature',
|
|
1159
|
+
key: 'beta-feature',
|
|
1160
|
+
is_simple_flag: true,
|
|
1161
|
+
active: true,
|
|
1162
|
+
filters: {
|
|
1163
|
+
groups: [
|
|
1164
|
+
{
|
|
1165
|
+
rollout_percentage: 100,
|
|
1166
|
+
// # The override applies even if the first condition matches all and gives everyone their default group
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
properties: [
|
|
1170
|
+
{
|
|
1171
|
+
key: 'email',
|
|
1172
|
+
operator: 'exact',
|
|
1173
|
+
value: 'test@posthog.com',
|
|
1174
|
+
type: 'person',
|
|
1175
|
+
},
|
|
1176
|
+
],
|
|
1177
|
+
rollout_percentage: 100,
|
|
1178
|
+
variant: 'second-variant',
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
rollout_percentage: 50,
|
|
1182
|
+
variant: 'third-variant',
|
|
1183
|
+
},
|
|
1184
|
+
],
|
|
1185
|
+
multivariate: {
|
|
1186
|
+
variants: [
|
|
1187
|
+
{
|
|
1188
|
+
key: 'first-variant',
|
|
1189
|
+
name: 'First Variant',
|
|
1190
|
+
rollout_percentage: 50,
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
key: 'second-variant',
|
|
1194
|
+
name: 'Second Variant',
|
|
1195
|
+
rollout_percentage: 25,
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
key: 'third-variant',
|
|
1199
|
+
name: 'Third Variant',
|
|
1200
|
+
rollout_percentage: 25,
|
|
1201
|
+
},
|
|
1202
|
+
],
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
],
|
|
1207
|
+
}
|
|
1208
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
|
|
1209
|
+
|
|
1210
|
+
posthog = new PostHog('TEST_API_KEY', {
|
|
1211
|
+
host: 'http://example.com',
|
|
1212
|
+
personalApiKey: 'TEST_PERSONAL_API_KEY',
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
expect(
|
|
1216
|
+
await posthog.getFeatureFlag('beta-feature', 'test_id', { personProperties: { email: 'test@posthog.com' } })
|
|
1217
|
+
).toEqual('second-variant')
|
|
1218
|
+
expect(await posthog.getFeatureFlag('beta-feature', 'example_id')).toEqual('third-variant')
|
|
1219
|
+
expect(await posthog.getFeatureFlag('beta-feature', 'another_id')).toEqual('second-variant')
|
|
1220
|
+
|
|
1221
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
1222
|
+
// decide not called
|
|
1223
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
929
1224
|
})
|
|
930
1225
|
})
|
|
931
1226
|
|
|
@@ -1096,6 +1391,42 @@ describe('match properties', () => {
|
|
|
1096
1391
|
expect(matchProperty(property_d, { key: '44' })).toBe(false)
|
|
1097
1392
|
expect(matchProperty(property_d, { key: 44 })).toBe(false)
|
|
1098
1393
|
})
|
|
1394
|
+
|
|
1395
|
+
it('with date operators', () => {
|
|
1396
|
+
// is date before
|
|
1397
|
+
const property_a = { key: 'key', value: '2022-05-01', operator: 'is_date_before' }
|
|
1398
|
+
expect(matchProperty(property_a, { key: '2022-03-01' })).toBe(true)
|
|
1399
|
+
expect(matchProperty(property_a, { key: '2022-04-30' })).toBe(true)
|
|
1400
|
+
expect(matchProperty(property_a, { key: new Date(2022, 3, 30) })).toBe(true)
|
|
1401
|
+
expect(matchProperty(property_a, { key: new Date(2022, 3, 30, 1, 2, 3) })).toBe(true)
|
|
1402
|
+
expect(matchProperty(property_a, { key: new Date('2022-04-30T00:00:00+02:00') })).toBe(true) // europe/madrid
|
|
1403
|
+
expect(matchProperty(property_a, { key: new Date('2022-04-30') })).toBe(true)
|
|
1404
|
+
expect(matchProperty(property_a, { key: '2022-05-30' })).toBe(false)
|
|
1405
|
+
|
|
1406
|
+
// is date after
|
|
1407
|
+
const property_b = { key: 'key', value: '2022-05-01', operator: 'is_date_after' }
|
|
1408
|
+
expect(matchProperty(property_b, { key: '2022-05-02' })).toBe(true)
|
|
1409
|
+
expect(matchProperty(property_b, { key: '2022-05-30' })).toBe(true)
|
|
1410
|
+
expect(matchProperty(property_b, { key: new Date(2022, 4, 30) })).toBe(true)
|
|
1411
|
+
expect(matchProperty(property_b, { key: new Date('2022-05-30') })).toBe(true)
|
|
1412
|
+
expect(matchProperty(property_b, { key: '2022-04-30' })).toBe(false)
|
|
1413
|
+
|
|
1414
|
+
// can't be an invalid number or invalid string
|
|
1415
|
+
expect(() => matchProperty(property_a, { key: parseInt('62802180000012345') })).toThrow(InconclusiveMatchError)
|
|
1416
|
+
expect(() => matchProperty(property_a, { key: 'abcdef' })).toThrow(InconclusiveMatchError)
|
|
1417
|
+
// invalid flag property
|
|
1418
|
+
const property_c = { key: 'key', value: 'abcd123', operator: 'is_date_before' }
|
|
1419
|
+
expect(() => matchProperty(property_c, { key: '2022-05-30' })).toThrow(InconclusiveMatchError)
|
|
1420
|
+
|
|
1421
|
+
// Timezone
|
|
1422
|
+
const property_d = { key: 'key', value: '2022-04-05 12:34:12 +01:00', operator: 'is_date_before' }
|
|
1423
|
+
expect(matchProperty(property_d, { key: '2022-05-30' })).toBe(false)
|
|
1424
|
+
|
|
1425
|
+
expect(matchProperty(property_d, { key: '2022-03-30' })).toBe(true)
|
|
1426
|
+
expect(matchProperty(property_d, { key: '2022-04-05 12:34:11+01:00' })).toBe(true)
|
|
1427
|
+
expect(matchProperty(property_d, { key: '2022-04-05 11:34:11 +00:00' })).toBe(true)
|
|
1428
|
+
expect(matchProperty(property_d, { key: '2022-04-05 11:34:13 +00:00' })).toBe(false)
|
|
1429
|
+
})
|
|
1099
1430
|
})
|
|
1100
1431
|
|
|
1101
1432
|
describe('consistency tests', () => {
|
|
@@ -1127,9 +1458,7 @@ describe('consistency tests', () => {
|
|
|
1127
1458
|
],
|
|
1128
1459
|
}
|
|
1129
1460
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
mockedUndici.fetch.mockImplementation(decideImplementation({}, 400))
|
|
1461
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags, decideFlags: {}, decideStatus: 400 }))
|
|
1133
1462
|
|
|
1134
1463
|
posthog = new PostHog('TEST_API_KEY', {
|
|
1135
1464
|
host: 'http://example.com',
|
|
@@ -2171,9 +2500,7 @@ describe('consistency tests', () => {
|
|
|
2171
2500
|
],
|
|
2172
2501
|
}
|
|
2173
2502
|
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
mockedUndici.fetch.mockImplementation(decideImplementation({}, 400))
|
|
2503
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags, decideFlags: {}, decideStatus: 400 }))
|
|
2177
2504
|
|
|
2178
2505
|
posthog = new PostHog('TEST_API_KEY', {
|
|
2179
2506
|
host: 'http://example.com',
|