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.
@@ -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('undici')
6
- import undici from 'undici'
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 mockedUndici = jest.mocked(undici, true)
11
-
12
- export const localEvaluationImplementation =
13
- (flags: any) =>
14
- (url: any): Promise<any> => {
15
- if ((url as any).includes('api/feature_flag/local_evaluation?token=TEST_API_KEY')) {
16
- return Promise.resolve({
17
- statusCode: 200,
18
- body: {
19
- text: () => Promise.resolve('ok'),
20
- json: () => Promise.resolve(flags),
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(flags)
28
+ return Promise.resolve(decideFlags)
47
29
  } else {
48
30
  return Promise.resolve({
49
- featureFlags: flags,
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
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(mockedUndici.request).toHaveBeenCalled()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
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(mockedUndici.request).toHaveBeenCalled()
189
+ expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
193
190
  // decide not called
194
- expect(mockedUndici.fetch).not.toHaveBeenCalled()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
228
-
229
- mockedUndici.fetch.mockImplementation(decideImplementation({ 'group-flag': 'decide-fallback-value' }))
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
301
-
302
- mockedUndici.fetch.mockImplementation(decideImplementation({ 'complex-flag': 'decide-fallback-value' }))
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(mockedUndici.fetch).not.toHaveBeenCalled()
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(mockedUndici.fetch).not.toHaveBeenCalled()
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(mockedUndici.fetch).toHaveBeenCalledWith(
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
- mockedUndici.fetch.mockClear()
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(mockedUndici.fetch).toHaveBeenCalledWith(
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
- mockedUndici.fetch.mockClear()
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(mockedUndici.fetch).toHaveBeenCalledTimes(1)
366
- mockedUndici.fetch.mockClear()
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(mockedUndici.fetch).not.toHaveBeenCalled()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
418
-
419
- mockedUndici.fetch.mockImplementation(
420
- decideImplementation({ 'beta-feature': 'alakazam', 'beta-feature2': 'alakazam2' })
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(mockedUndici.fetch).toHaveBeenCalledTimes(1)
431
- mockedUndici.fetch.mockClear()
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(mockedUndici.fetch).toHaveBeenCalledTimes(1)
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
479
-
480
- mockedUndici.fetch.mockImplementation(
481
- decideImplementation({ 'beta-feature': 'alakazam', 'beta-feature2': 'alakazam2' })
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(mockedUndici.fetch).not.toHaveBeenCalled()
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(mockedUndici.fetch).not.toHaveBeenCalled()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
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(mockedUndici.fetch).not.toHaveBeenCalled()
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(mockedUndici.fetch).toHaveBeenCalledTimes(2)
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
571
-
572
- mockedUndici.fetch.mockImplementation(decideImplementation({ error: 'went wrong' }, 400))
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(mockedUndici.fetch).toHaveBeenCalledTimes(2)
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
607
-
608
- mockedUndici.fetch.mockImplementation(decideImplementation({ 'beta-feature': 'decide-fallback-value' }))
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(mockedUndici.fetch).toHaveBeenCalledTimes(1)
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
672
-
673
- mockedUndici.fetch.mockImplementation(
674
- decideImplementation({ 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' })
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(mockedUndici.fetch).toHaveBeenCalledTimes(1)
689
- mockedUndici.fetch.mockClear()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
744
-
745
- mockedUndici.fetch.mockImplementation(
746
- decideImplementation({ 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' })
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(mockedUndici.fetch).not.toHaveBeenCalled()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
767
-
768
- mockedUndici.fetch.mockImplementation(
769
- decideImplementation({ 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' })
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(mockedUndici.fetch).toHaveBeenCalledTimes(1)
782
- mockedUndici.fetch.mockClear()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
822
-
823
- mockedUndici.fetch.mockImplementation(
824
- decideImplementation({ 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' })
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(mockedUndici.fetch).not.toHaveBeenCalled()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
873
-
874
- mockedUndici.fetch.mockImplementation(
875
- decideImplementation({ 'beta-feature': 'variant-1', 'beta-feature2': 'variant-2' })
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(mockedUndici.fetch).not.toHaveBeenCalled()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags2))
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(mockedUndici.fetch).not.toHaveBeenCalled()
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
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
- mockedUndici.request.mockImplementation(localEvaluationImplementation(flags))
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',