@posthog/core 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1106 +0,0 @@
1
- import { PostHogPersistedProperty, PostHogV2FlagsResponse } from '@/types'
2
- import { normalizeFlagsResponse } from '@/featureFlagUtils'
3
- import {
4
- parseBody,
5
- waitForPromises,
6
- createTestClient,
7
- PostHogCoreTestClient,
8
- PostHogCoreTestClientMocks,
9
- } from '@/testing'
10
-
11
- describe('PostHog Feature Flags v4', () => {
12
- let posthog: PostHogCoreTestClient
13
- let mocks: PostHogCoreTestClientMocks
14
-
15
- jest.useFakeTimers()
16
- jest.setSystemTime(new Date('2022-01-01'))
17
-
18
- const createMockFeatureFlags = (): Partial<PostHogV2FlagsResponse['flags']> => ({
19
- 'feature-1': {
20
- key: 'feature-1',
21
- enabled: true,
22
- variant: undefined,
23
- reason: {
24
- code: 'matched_condition',
25
- description: 'matched condition set 1',
26
- condition_index: 0,
27
- },
28
- metadata: {
29
- id: 1,
30
- version: 1,
31
- description: 'feature-1',
32
- payload: '{"color":"blue"}',
33
- },
34
- },
35
- 'feature-2': {
36
- key: 'feature-2',
37
- enabled: true,
38
- variant: undefined,
39
- reason: {
40
- code: 'matched_condition',
41
- description: 'matched condition set 2',
42
- condition_index: 1,
43
- },
44
- metadata: {
45
- id: 2,
46
- version: 42,
47
- description: 'feature-2',
48
- payload: undefined,
49
- },
50
- },
51
- 'feature-variant': {
52
- key: 'feature-variant',
53
- enabled: true,
54
- variant: 'variant',
55
- reason: {
56
- code: 'matched_condition',
57
- description: 'matched condition set 3',
58
- condition_index: 2,
59
- },
60
- metadata: {
61
- id: 3,
62
- version: 1,
63
- description: 'feature-variant',
64
- payload: '[5]',
65
- },
66
- },
67
- 'json-payload': {
68
- key: 'json-payload',
69
- enabled: true,
70
- variant: undefined,
71
- reason: {
72
- code: 'matched_condition',
73
- description: 'matched condition set 4',
74
- condition_index: 4,
75
- },
76
- metadata: {
77
- id: 4,
78
- version: 1,
79
- description: 'json-payload',
80
- payload: '{"a":"payload"}',
81
- },
82
- },
83
- })
84
-
85
- const expectedFeatureFlagResponses = {
86
- 'feature-1': true,
87
- 'feature-2': true,
88
- 'feature-variant': 'variant',
89
- 'json-payload': true,
90
- }
91
-
92
- const errorAPIResponse = Promise.resolve({
93
- status: 400,
94
- text: () => Promise.resolve('error'),
95
- json: () =>
96
- Promise.resolve({
97
- status: 'error',
98
- }),
99
- })
100
-
101
- beforeEach(() => {
102
- ;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 1 }, (_mocks) => {
103
- _mocks.fetch.mockImplementation((url) => {
104
- if (url.includes('/flags/?v=2&config=true')) {
105
- return Promise.resolve({
106
- status: 200,
107
- text: () => Promise.resolve('ok'),
108
- json: () =>
109
- Promise.resolve({
110
- flags: createMockFeatureFlags(),
111
- requestId: '0152a345-295f-4fba-adac-2e6ea9c91082',
112
- }),
113
- })
114
- }
115
-
116
- return Promise.resolve({
117
- status: 200,
118
- text: () => Promise.resolve('ok'),
119
- json: () =>
120
- Promise.resolve({
121
- status: 'ok',
122
- }),
123
- })
124
- })
125
- })
126
- })
127
-
128
- describe('featureflags', () => {
129
- it('getFeatureFlags should return undefined if not loaded', () => {
130
- expect(posthog.getFeatureFlags()).toEqual(undefined)
131
- })
132
-
133
- it('getFeatureFlagPayloads should return undefined if not loaded', () => {
134
- expect(posthog.getFeatureFlagPayloads()).toEqual(undefined)
135
- })
136
-
137
- it('getFeatureFlag should return undefined if not loaded', () => {
138
- expect(posthog.getFeatureFlag('my-flag')).toEqual(undefined)
139
- expect(posthog.getFeatureFlag('feature-1')).toEqual(undefined)
140
- })
141
-
142
- it('getFeatureFlagPayload should return undefined if not loaded', () => {
143
- expect(posthog.getFeatureFlagPayload('my-flag')).toEqual(undefined)
144
- })
145
-
146
- it('isFeatureEnabled should return undefined if not loaded', () => {
147
- expect(posthog.isFeatureEnabled('my-flag')).toEqual(undefined)
148
- expect(posthog.isFeatureEnabled('feature-1')).toEqual(undefined)
149
- })
150
-
151
- it('should load persisted feature flags', () => {
152
- const flagsResponse = { flags: createMockFeatureFlags() } as PostHogV2FlagsResponse
153
- const normalizedFeatureFlags = normalizeFlagsResponse(flagsResponse)
154
- posthog.setPersistedProperty(PostHogPersistedProperty.FeatureFlagDetails, normalizedFeatureFlags)
155
- expect(posthog.getFeatureFlags()).toEqual(expectedFeatureFlagResponses)
156
- })
157
-
158
- it('should only call fetch once if already calling', async () => {
159
- expect(mocks.fetch).toHaveBeenCalledTimes(0)
160
- posthog.reloadFeatureFlagsAsync()
161
- posthog.reloadFeatureFlagsAsync()
162
- const flags = await posthog.reloadFeatureFlagsAsync()
163
- expect(mocks.fetch).toHaveBeenCalledTimes(1)
164
- expect(flags).toEqual(expectedFeatureFlagResponses)
165
- })
166
-
167
- it('should emit featureflags event when flags are loaded', async () => {
168
- const receivedFlags: any[] = []
169
- const unsubscribe = posthog.onFeatureFlags((flags) => {
170
- receivedFlags.push(flags)
171
- })
172
-
173
- await posthog.reloadFeatureFlagsAsync()
174
- unsubscribe()
175
-
176
- expect(receivedFlags).toEqual([expectedFeatureFlagResponses])
177
- })
178
-
179
- describe('when loaded', () => {
180
- beforeEach(() => {
181
- // The core doesn't reload flags by default (this is handled differently by web and RN)
182
- posthog.reloadFeatureFlags()
183
- })
184
-
185
- it('should return the value of a flag', async () => {
186
- expect(posthog.getFeatureFlag('feature-1')).toEqual(true)
187
- expect(posthog.getFeatureFlag('feature-variant')).toEqual('variant')
188
- expect(posthog.getFeatureFlag('feature-missing')).toEqual(false)
189
- })
190
-
191
- it.each([
192
- ['feature-variant', [5]],
193
- ['feature-1', { color: 'blue' }],
194
- ['feature-2', null],
195
- ])('should return correct payload for flag %s', (flagKey, expectedPayload) => {
196
- expect(posthog.getFeatureFlagPayload(flagKey)).toEqual(expectedPayload)
197
- })
198
-
199
- describe('when errored out', () => {
200
- beforeEach(() => {
201
- ;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 1 }, (_mocks) => {
202
- _mocks.fetch.mockImplementation((url) => {
203
- if (url.includes('/flags/')) {
204
- return Promise.resolve({
205
- status: 400,
206
- text: () => Promise.resolve('ok'),
207
- json: () =>
208
- Promise.resolve({
209
- error: 'went wrong',
210
- }),
211
- })
212
- }
213
-
214
- return errorAPIResponse
215
- })
216
- })
217
-
218
- posthog.reloadFeatureFlags()
219
- })
220
-
221
- it('should return undefined', async () => {
222
- expect(mocks.fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2&config=true', {
223
- body: JSON.stringify({
224
- token: 'TEST_API_KEY',
225
- distinct_id: posthog.getDistinctId(),
226
- groups: {},
227
- person_properties: {},
228
- group_properties: {},
229
- $anon_distinct_id: posthog.getAnonymousId(),
230
- }),
231
- method: 'POST',
232
- headers: {
233
- 'Content-Type': 'application/json',
234
- 'User-Agent': 'posthog-core-tests',
235
- },
236
- signal: expect.anything(),
237
- })
238
-
239
- expect(posthog.getFeatureFlag('feature-1')).toEqual(undefined)
240
- expect(posthog.getFeatureFlag('feature-variant')).toEqual(undefined)
241
- expect(posthog.getFeatureFlag('feature-missing')).toEqual(undefined)
242
-
243
- expect(posthog.isFeatureEnabled('feature-1')).toEqual(undefined)
244
- expect(posthog.isFeatureEnabled('feature-variant')).toEqual(undefined)
245
- expect(posthog.isFeatureEnabled('feature-missing')).toEqual(undefined)
246
-
247
- expect(posthog.getFeatureFlagPayloads()).toEqual(undefined)
248
- expect(posthog.getFeatureFlagPayload('feature-1')).toEqual(undefined)
249
- })
250
- })
251
-
252
- describe('when subsequent flags calls return partial results', () => {
253
- beforeEach(() => {
254
- ;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 1 }, (_mocks) => {
255
- _mocks.fetch
256
- .mockImplementationOnce((url) => {
257
- if (url.includes('/flags/?v=2&config=true')) {
258
- return Promise.resolve({
259
- status: 200,
260
- text: () => Promise.resolve('ok'),
261
- json: () =>
262
- Promise.resolve({
263
- flags: createMockFeatureFlags(),
264
- }),
265
- })
266
- }
267
- return errorAPIResponse
268
- })
269
- .mockImplementationOnce((url) => {
270
- if (url.includes('/flags/?v=2&config=true')) {
271
- return Promise.resolve({
272
- status: 200,
273
- text: () => Promise.resolve('ok'),
274
- json: () =>
275
- Promise.resolve({
276
- flags: {
277
- 'x-flag': {
278
- key: 'x-flag',
279
- enabled: true,
280
- variant: 'x-value',
281
- reason: {
282
- code: 'matched_condition',
283
- description: 'matched condition set 5',
284
- condition_index: 0,
285
- },
286
- metadata: {
287
- id: 5,
288
- version: 1,
289
- description: 'x-flag',
290
- payload: '{"x":"value"}',
291
- },
292
- },
293
- 'feature-1': {
294
- key: 'feature-1',
295
- enabled: false,
296
- variant: undefined,
297
- reason: {
298
- code: 'matched_condition',
299
- description: 'matched condition set 6',
300
- condition_index: 0,
301
- },
302
- metadata: {
303
- id: 6,
304
- version: 1,
305
- description: 'feature-1',
306
- payload: '{"color":"blue"}',
307
- },
308
- },
309
- },
310
- errorsWhileComputingFlags: true,
311
- }),
312
- })
313
- }
314
-
315
- return errorAPIResponse
316
- })
317
- .mockImplementation(() => {
318
- return errorAPIResponse
319
- })
320
- })
321
-
322
- posthog.reloadFeatureFlags()
323
- })
324
-
325
- it('should return combined results', async () => {
326
- expect(mocks.fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2&config=true', {
327
- body: JSON.stringify({
328
- token: 'TEST_API_KEY',
329
- distinct_id: posthog.getDistinctId(),
330
- groups: {},
331
- person_properties: {},
332
- group_properties: {},
333
- $anon_distinct_id: posthog.getAnonymousId(),
334
- }),
335
- method: 'POST',
336
- headers: {
337
- 'Content-Type': 'application/json',
338
- 'User-Agent': 'posthog-core-tests',
339
- },
340
- signal: expect.anything(),
341
- })
342
-
343
- expect(posthog.getFeatureFlags()).toEqual({
344
- 'feature-1': true,
345
- 'feature-2': true,
346
- 'json-payload': true,
347
- 'feature-variant': 'variant',
348
- })
349
-
350
- // now second call to feature flags
351
- await posthog.reloadFeatureFlagsAsync()
352
-
353
- expect(mocks.fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2&config=true', {
354
- body: JSON.stringify({
355
- token: 'TEST_API_KEY',
356
- distinct_id: posthog.getDistinctId(),
357
- groups: {},
358
- person_properties: {},
359
- group_properties: {},
360
- $anon_distinct_id: posthog.getAnonymousId(),
361
- }),
362
- method: 'POST',
363
- headers: {
364
- 'Content-Type': 'application/json',
365
- 'User-Agent': 'posthog-core-tests',
366
- },
367
- signal: expect.anything(),
368
- })
369
-
370
- expect(posthog.getFeatureFlags()).toEqual({
371
- 'feature-1': false,
372
- 'feature-2': true,
373
- 'json-payload': true,
374
- 'feature-variant': 'variant',
375
- 'x-flag': 'x-value',
376
- })
377
-
378
- expect(posthog.getFeatureFlag('feature-1')).toEqual(false)
379
- expect(posthog.getFeatureFlag('feature-variant')).toEqual('variant')
380
- expect(posthog.getFeatureFlag('feature-missing')).toEqual(false)
381
- expect(posthog.getFeatureFlag('x-flag')).toEqual('x-value')
382
-
383
- expect(posthog.isFeatureEnabled('feature-1')).toEqual(false)
384
- expect(posthog.isFeatureEnabled('feature-variant')).toEqual(true)
385
- expect(posthog.isFeatureEnabled('feature-missing')).toEqual(false)
386
- expect(posthog.isFeatureEnabled('x-flag')).toEqual(true)
387
- })
388
- })
389
-
390
- describe('when subsequent flags calls return results without errors', () => {
391
- beforeEach(() => {
392
- ;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 1 }, (_mocks) => {
393
- _mocks.fetch
394
- .mockImplementationOnce((url) => {
395
- if (url.includes('/flags/?v=2&config=true')) {
396
- return Promise.resolve({
397
- status: 200,
398
- text: () => Promise.resolve('ok'),
399
- json: () =>
400
- Promise.resolve({
401
- flags: createMockFeatureFlags(),
402
- requestId: '18043bf7-9cf6-44cd-b959-9662ee20d371',
403
- }),
404
- })
405
- }
406
- return errorAPIResponse
407
- })
408
- .mockImplementationOnce((url) => {
409
- if (url.includes('/flags/?v=2&config=true')) {
410
- return Promise.resolve({
411
- status: 200,
412
- text: () => Promise.resolve('ok'),
413
- json: () =>
414
- Promise.resolve({
415
- flags: {
416
- 'x-flag': {
417
- key: 'x-flag',
418
- enabled: true,
419
- variant: 'x-value',
420
- reason: {
421
- code: 'matched_condition',
422
- description: 'matched condition set 5',
423
- condition_index: 0,
424
- },
425
- metadata: {
426
- id: 5,
427
- version: 1,
428
- description: 'x-flag',
429
- payload: '{"x":"value"}',
430
- },
431
- },
432
- 'feature-1': {
433
- key: 'feature-1',
434
- enabled: false,
435
- variant: undefined,
436
- reason: {
437
- code: 'matched_condition',
438
- description: 'matched condition set 6',
439
- condition_index: 0,
440
- },
441
- metadata: {
442
- id: 6,
443
- version: 1,
444
- description: 'feature-1',
445
- payload: '{"color":"blue"}',
446
- },
447
- },
448
- },
449
- errorsWhileComputingFlags: false,
450
- requestId: 'bccd3c21-38e6-4499-a804-89f77ddcd1fc',
451
- }),
452
- })
453
- }
454
-
455
- return errorAPIResponse
456
- })
457
- .mockImplementation(() => {
458
- return errorAPIResponse
459
- })
460
- })
461
-
462
- posthog.reloadFeatureFlags()
463
- })
464
-
465
- it('should return only latest results', async () => {
466
- expect(mocks.fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2&config=true', {
467
- body: JSON.stringify({
468
- token: 'TEST_API_KEY',
469
- distinct_id: posthog.getDistinctId(),
470
- groups: {},
471
- person_properties: {},
472
- group_properties: {},
473
- $anon_distinct_id: posthog.getAnonymousId(),
474
- }),
475
- method: 'POST',
476
- headers: {
477
- 'Content-Type': 'application/json',
478
- 'User-Agent': 'posthog-core-tests',
479
- },
480
- signal: expect.anything(),
481
- })
482
-
483
- expect(posthog.getFeatureFlags()).toEqual({
484
- 'feature-1': true,
485
- 'feature-2': true,
486
- 'json-payload': true,
487
- 'feature-variant': 'variant',
488
- })
489
-
490
- // now second call to feature flags
491
- await posthog.reloadFeatureFlagsAsync()
492
-
493
- expect(mocks.fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2&config=true', {
494
- body: JSON.stringify({
495
- token: 'TEST_API_KEY',
496
- distinct_id: posthog.getDistinctId(),
497
- groups: {},
498
- person_properties: {},
499
- group_properties: {},
500
- $anon_distinct_id: posthog.getAnonymousId(),
501
- }),
502
- method: 'POST',
503
- headers: {
504
- 'Content-Type': 'application/json',
505
- 'User-Agent': 'posthog-core-tests',
506
- },
507
- signal: expect.anything(),
508
- })
509
-
510
- expect(posthog.getFeatureFlags()).toEqual({
511
- 'feature-1': false,
512
- 'x-flag': 'x-value',
513
- })
514
-
515
- expect(posthog.getFeatureFlag('feature-1')).toEqual(false)
516
- expect(posthog.getFeatureFlag('feature-variant')).toEqual(false)
517
- expect(posthog.getFeatureFlag('feature-missing')).toEqual(false)
518
- expect(posthog.getFeatureFlag('x-flag')).toEqual('x-value')
519
-
520
- expect(posthog.isFeatureEnabled('feature-1')).toEqual(false)
521
- expect(posthog.isFeatureEnabled('feature-variant')).toEqual(false)
522
- expect(posthog.isFeatureEnabled('feature-missing')).toEqual(false)
523
- expect(posthog.isFeatureEnabled('x-flag')).toEqual(true)
524
- })
525
- })
526
-
527
- it('should return the boolean value of a flag', async () => {
528
- expect(posthog.isFeatureEnabled('feature-1')).toEqual(true)
529
- expect(posthog.isFeatureEnabled('feature-variant')).toEqual(true)
530
- expect(posthog.isFeatureEnabled('feature-missing')).toEqual(false)
531
- })
532
-
533
- it('should reload if groups are set', async () => {
534
- posthog.group('my-group', 'is-great')
535
- await waitForPromises()
536
- expect(mocks.fetch).toHaveBeenCalledTimes(2)
537
- expect(JSON.parse((mocks.fetch.mock.calls[1][1].body as string) || '')).toMatchObject({
538
- groups: { 'my-group': 'is-great' },
539
- })
540
- })
541
-
542
- it.each([
543
- {
544
- key: 'feature-1',
545
- expected_response: true,
546
- expected_id: 1,
547
- expected_version: 1,
548
- expected_reason: 'matched condition set 1',
549
- },
550
- {
551
- key: 'feature-2',
552
- expected_response: true,
553
- expected_id: 2,
554
- expected_version: 42,
555
- expected_reason: 'matched condition set 2',
556
- },
557
- {
558
- key: 'feature-variant',
559
- expected_response: 'variant',
560
- expected_id: 3,
561
- expected_version: 1,
562
- expected_reason: 'matched condition set 3',
563
- },
564
- {
565
- key: 'json-payload',
566
- expected_response: true,
567
- expected_id: 4,
568
- expected_version: 1,
569
- expected_reason: 'matched condition set 4',
570
- },
571
- ])(
572
- 'should capture feature_flag_called when called for %s',
573
- async ({ key, expected_response, expected_id, expected_version, expected_reason }) => {
574
- expect(posthog.getFeatureFlag(key)).toEqual(expected_response)
575
- await waitForPromises()
576
- expect(mocks.fetch).toHaveBeenCalledTimes(2)
577
-
578
- expect(parseBody(mocks.fetch.mock.calls[1])).toMatchObject({
579
- batch: [
580
- {
581
- event: '$feature_flag_called',
582
- distinct_id: posthog.getDistinctId(),
583
- properties: {
584
- $feature_flag: key,
585
- $feature_flag_response: expected_response,
586
- $feature_flag_id: expected_id,
587
- $feature_flag_version: expected_version,
588
- $feature_flag_reason: expected_reason,
589
- '$feature/feature-1': true,
590
- $used_bootstrap_value: false,
591
- $feature_flag_request_id: '0152a345-295f-4fba-adac-2e6ea9c91082',
592
- },
593
- type: 'capture',
594
- },
595
- ],
596
- })
597
-
598
- // Only tracked once
599
- expect(posthog.getFeatureFlag('feature-1')).toEqual(true)
600
- expect(mocks.fetch).toHaveBeenCalledTimes(2)
601
- }
602
- )
603
-
604
- it('should capture $feature_flag_called again if new flags', async () => {
605
- expect(posthog.getFeatureFlag('feature-1')).toEqual(true)
606
- await waitForPromises()
607
- expect(mocks.fetch).toHaveBeenCalledTimes(2)
608
-
609
- expect(parseBody(mocks.fetch.mock.calls[1])).toMatchObject({
610
- batch: [
611
- {
612
- event: '$feature_flag_called',
613
- distinct_id: posthog.getDistinctId(),
614
- properties: {
615
- $feature_flag: 'feature-1',
616
- $feature_flag_response: true,
617
- '$feature/feature-1': true,
618
- $used_bootstrap_value: false,
619
- $feature_flag_request_id: '0152a345-295f-4fba-adac-2e6ea9c91082',
620
- },
621
- type: 'capture',
622
- },
623
- ],
624
- })
625
-
626
- await posthog.reloadFeatureFlagsAsync()
627
- posthog.getFeatureFlag('feature-1')
628
-
629
- await waitForPromises()
630
- expect(mocks.fetch).toHaveBeenCalledTimes(4)
631
-
632
- expect(parseBody(mocks.fetch.mock.calls[3])).toMatchObject({
633
- batch: [
634
- {
635
- event: '$feature_flag_called',
636
- distinct_id: posthog.getDistinctId(),
637
- properties: {
638
- $feature_flag: 'feature-1',
639
- $feature_flag_response: true,
640
- '$feature/feature-1': true,
641
- $used_bootstrap_value: false,
642
- },
643
- type: 'capture',
644
- },
645
- ],
646
- })
647
- })
648
-
649
- it('should capture $feature_flag_called when called, but not add all cached flags', async () => {
650
- expect(posthog.getFeatureFlag('feature-1')).toEqual(true)
651
- await waitForPromises()
652
- expect(mocks.fetch).toHaveBeenCalledTimes(2)
653
-
654
- expect(parseBody(mocks.fetch.mock.calls[1])).toMatchObject({
655
- batch: [
656
- {
657
- event: '$feature_flag_called',
658
- distinct_id: posthog.getDistinctId(),
659
- properties: {
660
- $feature_flag: 'feature-1',
661
- $feature_flag_response: true,
662
- '$feature/feature-1': true,
663
- $used_bootstrap_value: false,
664
- },
665
- type: 'capture',
666
- },
667
- ],
668
- })
669
-
670
- // Only tracked once
671
- expect(posthog.getFeatureFlag('feature-1')).toEqual(true)
672
- expect(mocks.fetch).toHaveBeenCalledTimes(2)
673
- })
674
-
675
- it('should persist feature flags', () => {
676
- const expectedFeatureFlags = {
677
- flags: createMockFeatureFlags(),
678
- requestId: '0152a345-295f-4fba-adac-2e6ea9c91082',
679
- }
680
- const normalizedFeatureFlags = normalizeFlagsResponse(expectedFeatureFlags as PostHogV2FlagsResponse)
681
- expect(posthog.getPersistedProperty(PostHogPersistedProperty.FeatureFlagDetails)).toEqual(
682
- normalizedFeatureFlags
683
- )
684
- })
685
-
686
- it('should include feature flags in subsequent captures', async () => {
687
- posthog.capture('test-event', { foo: 'bar' })
688
-
689
- await waitForPromises()
690
-
691
- expect(parseBody(mocks.fetch.mock.calls[1])).toMatchObject({
692
- batch: [
693
- {
694
- event: 'test-event',
695
- distinct_id: posthog.getDistinctId(),
696
- properties: {
697
- $active_feature_flags: ['feature-1', 'feature-2', 'feature-variant', 'json-payload'],
698
- '$feature/feature-1': true,
699
- '$feature/feature-2': true,
700
- '$feature/json-payload': true,
701
- '$feature/feature-variant': 'variant',
702
- },
703
- type: 'capture',
704
- },
705
- ],
706
- })
707
- })
708
-
709
- it('should override flags', () => {
710
- posthog.overrideFeatureFlag({
711
- 'feature-2': false,
712
- 'feature-variant': 'control',
713
- })
714
-
715
- const received = posthog.getFeatureFlags()
716
-
717
- expect(received).toEqual({
718
- 'json-payload': true,
719
- 'feature-1': true,
720
- 'feature-variant': 'control',
721
- })
722
- })
723
- })
724
-
725
- describe('when quota limited', () => {
726
- beforeEach(() => {
727
- ;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 1 }, (_mocks) => {
728
- _mocks.fetch.mockImplementation((url) => {
729
- if (url.includes('/flags/')) {
730
- return Promise.resolve({
731
- status: 200,
732
- text: () => Promise.resolve('ok'),
733
- json: () =>
734
- Promise.resolve({
735
- quotaLimited: ['feature_flags'],
736
- flags: {},
737
- }),
738
- })
739
- }
740
- return errorAPIResponse
741
- })
742
- })
743
-
744
- posthog.reloadFeatureFlags()
745
- })
746
-
747
- it('should unset all flags when feature_flags is quota limited', async () => {
748
- // First verify the fetch was called correctly
749
- expect(mocks.fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2&config=true', {
750
- body: JSON.stringify({
751
- token: 'TEST_API_KEY',
752
- distinct_id: posthog.getDistinctId(),
753
- groups: {},
754
- person_properties: {},
755
- group_properties: {},
756
- $anon_distinct_id: posthog.getAnonymousId(),
757
- }),
758
- method: 'POST',
759
- headers: {
760
- 'Content-Type': 'application/json',
761
- 'User-Agent': 'posthog-core-tests',
762
- },
763
- signal: expect.anything(),
764
- })
765
-
766
- // Verify all flag methods return undefined when quota limited
767
- expect(posthog.getFeatureFlags()).toEqual(undefined)
768
- expect(posthog.getFeatureFlag('feature-1')).toEqual(undefined)
769
- expect(posthog.getFeatureFlagPayloads()).toEqual(undefined)
770
- expect(posthog.getFeatureFlagPayload('feature-1')).toEqual(undefined)
771
- })
772
-
773
- it('should emit debug message when quota limited', async () => {
774
- const warnSpy = jest.spyOn(console, 'warn')
775
- posthog.debug(true)
776
- await posthog.reloadFeatureFlagsAsync()
777
-
778
- expect(warnSpy).toHaveBeenCalledWith(
779
- '[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts'
780
- )
781
- })
782
- })
783
- })
784
-
785
- describe('bootstrapped feature flags', () => {
786
- beforeEach(() => {
787
- ;[posthog, mocks] = createTestClient(
788
- 'TEST_API_KEY',
789
- {
790
- flushAt: 1,
791
- bootstrap: {
792
- distinctId: 'tomato',
793
- featureFlags: {
794
- 'bootstrap-1': 'variant-1',
795
- 'feature-1': 'feature-1-bootstrap-value',
796
- enabled: true,
797
- disabled: false,
798
- },
799
- featureFlagPayloads: {
800
- 'bootstrap-1': {
801
- some: 'key',
802
- },
803
- 'feature-1': {
804
- color: 'feature-1-bootstrap-color',
805
- },
806
- enabled: 200,
807
- 'not-in-featureFlags': {
808
- color: { foo: 'bar' },
809
- },
810
- },
811
- },
812
- },
813
- (_mocks) => {
814
- _mocks.fetch.mockImplementation((url) => {
815
- if (url.includes('/flags/')) {
816
- return Promise.reject(new Error('Not responding to emulate use of bootstrapped values'))
817
- }
818
-
819
- return Promise.resolve({
820
- status: 200,
821
- text: () => Promise.resolve('ok'),
822
- json: () =>
823
- Promise.resolve({
824
- status: 'ok',
825
- }),
826
- })
827
- })
828
- }
829
- )
830
- })
831
-
832
- it('getFeatureFlags should return bootstrapped flags', async () => {
833
- expect(posthog.getFeatureFlags()).toEqual({
834
- 'bootstrap-1': 'variant-1',
835
- enabled: true,
836
- 'feature-1': 'feature-1-bootstrap-value',
837
- 'not-in-featureFlags': true,
838
- })
839
- expect(posthog.getDistinctId()).toEqual('tomato')
840
- expect(posthog.getAnonymousId()).toEqual('tomato')
841
- })
842
-
843
- it('getFeatureFlag should return bootstrapped flags', async () => {
844
- expect(posthog.getFeatureFlag('my-flag')).toEqual(false)
845
- expect(posthog.getFeatureFlag('bootstrap-1')).toEqual('variant-1')
846
- expect(posthog.getFeatureFlag('enabled')).toEqual(true)
847
- expect(posthog.getFeatureFlag('disabled')).toEqual(false)
848
- expect(posthog.getFeatureFlag('not-in-featureFlags')).toEqual(true)
849
- })
850
-
851
- it('getFeatureFlag should capture $feature_flag_called with bootstrapped values', async () => {
852
- expect(posthog.getFeatureFlag('bootstrap-1')).toEqual('variant-1')
853
-
854
- await waitForPromises()
855
- expect(mocks.fetch).toHaveBeenCalledTimes(1)
856
-
857
- expect(parseBody(mocks.fetch.mock.calls[0])).toMatchObject({
858
- batch: [
859
- {
860
- event: '$feature_flag_called',
861
- distinct_id: posthog.getDistinctId(),
862
- properties: {
863
- $feature_flag: 'bootstrap-1',
864
- $feature_flag_response: 'variant-1',
865
- '$feature/bootstrap-1': 'variant-1',
866
- $feature_flag_bootstrapped_response: 'variant-1',
867
- $feature_flag_bootstrapped_payload: { some: 'key' },
868
- $used_bootstrap_value: true,
869
- },
870
- type: 'capture',
871
- },
872
- ],
873
- })
874
- })
875
-
876
- it('isFeatureEnabled should return true/false for bootstrapped flags', () => {
877
- expect(posthog.isFeatureEnabled('my-flag')).toEqual(false)
878
- expect(posthog.isFeatureEnabled('bootstrap-1')).toEqual(true)
879
- expect(posthog.isFeatureEnabled('enabled')).toEqual(true)
880
- expect(posthog.isFeatureEnabled('disabled')).toEqual(false)
881
- expect(posthog.isFeatureEnabled('not-in-featureFlags')).toEqual(true)
882
- })
883
-
884
- it('getFeatureFlagPayload should return bootstrapped payloads', () => {
885
- expect(posthog.getFeatureFlagPayload('my-flag')).toEqual(null)
886
- expect(posthog.getFeatureFlagPayload('bootstrap-1')).toEqual({
887
- some: 'key',
888
- })
889
- expect(posthog.getFeatureFlagPayload('enabled')).toEqual(200)
890
- expect(posthog.getFeatureFlagPayload('not-in-featureFlags')).toEqual({
891
- color: { foo: 'bar' },
892
- })
893
- })
894
-
895
- describe('when loaded', () => {
896
- beforeEach(() => {
897
- ;[posthog, mocks] = createTestClient(
898
- 'TEST_API_KEY',
899
- {
900
- flushAt: 1,
901
- bootstrap: {
902
- distinctId: 'tomato',
903
- featureFlags: {
904
- 'bootstrap-1': 'variant-1',
905
- 'feature-1': 'feature-1-bootstrap-value',
906
- enabled: true,
907
- disabled: false,
908
- },
909
- featureFlagPayloads: {
910
- 'bootstrap-1': {
911
- some: 'key',
912
- },
913
- 'feature-1': {
914
- color: 'feature-1-bootstrap-color',
915
- },
916
- enabled: 200,
917
- },
918
- },
919
- },
920
- (_mocks) => {
921
- _mocks.fetch.mockImplementation((url) => {
922
- if (url.includes('/flags/')) {
923
- return Promise.resolve({
924
- status: 200,
925
- text: () => Promise.resolve('ok'),
926
- json: () =>
927
- Promise.resolve({
928
- flags: createMockFeatureFlags(),
929
- }),
930
- })
931
- }
932
-
933
- return Promise.resolve({
934
- status: 200,
935
- text: () => Promise.resolve('ok'),
936
- json: () =>
937
- Promise.resolve({
938
- status: 'ok',
939
- }),
940
- })
941
- })
942
- }
943
- )
944
-
945
- posthog.reloadFeatureFlags()
946
- })
947
-
948
- it('should load new feature flags', async () => {
949
- expect(mocks.fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2&config=true', {
950
- body: JSON.stringify({
951
- token: 'TEST_API_KEY',
952
- distinct_id: posthog.getDistinctId(),
953
- groups: {},
954
- person_properties: {},
955
- group_properties: {},
956
- $anon_distinct_id: 'tomato',
957
- }),
958
- method: 'POST',
959
- headers: {
960
- 'Content-Type': 'application/json',
961
- 'User-Agent': 'posthog-core-tests',
962
- },
963
- signal: expect.anything(),
964
- })
965
-
966
- expect(posthog.getFeatureFlags()).toEqual({
967
- 'feature-1': true,
968
- 'feature-2': true,
969
- 'json-payload': true,
970
- 'feature-variant': 'variant',
971
- })
972
- })
973
-
974
- it('should load new feature flag payloads', async () => {
975
- expect(mocks.fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2&config=true', {
976
- body: JSON.stringify({
977
- token: 'TEST_API_KEY',
978
- distinct_id: posthog.getDistinctId(),
979
- groups: {},
980
- person_properties: {},
981
- group_properties: {},
982
- $anon_distinct_id: 'tomato',
983
- }),
984
- method: 'POST',
985
- headers: {
986
- 'Content-Type': 'application/json',
987
- 'User-Agent': 'posthog-core-tests',
988
- },
989
- signal: expect.anything(),
990
- })
991
- expect(posthog.getFeatureFlagPayload('feature-1')).toEqual({
992
- color: 'blue',
993
- })
994
- expect(posthog.getFeatureFlagPayload('feature-variant')).toEqual([5])
995
- })
996
-
997
- it('should capture feature_flag_called with bootstrapped values', async () => {
998
- expect(posthog.getFeatureFlag('feature-1')).toEqual(true)
999
-
1000
- await waitForPromises()
1001
- expect(mocks.fetch).toHaveBeenCalledTimes(2)
1002
-
1003
- expect(parseBody(mocks.fetch.mock.calls[1])).toMatchObject({
1004
- batch: [
1005
- {
1006
- event: '$feature_flag_called',
1007
- distinct_id: posthog.getDistinctId(),
1008
- properties: {
1009
- $feature_flag: 'feature-1',
1010
- $feature_flag_response: true,
1011
- '$feature/feature-1': true,
1012
- $feature_flag_bootstrapped_response: 'feature-1-bootstrap-value',
1013
- $feature_flag_bootstrapped_payload: { color: 'feature-1-bootstrap-color' },
1014
- $used_bootstrap_value: false,
1015
- },
1016
- type: 'capture',
1017
- },
1018
- ],
1019
- })
1020
- })
1021
- })
1022
- })
1023
-
1024
- describe('bootstapped do not overwrite values', () => {
1025
- beforeEach(() => {
1026
- ;[posthog, mocks] = createTestClient(
1027
- 'TEST_API_KEY',
1028
- {
1029
- flushAt: 1,
1030
- bootstrap: {
1031
- distinctId: 'tomato',
1032
- featureFlags: { 'bootstrap-1': 'variant-1', enabled: true, disabled: false },
1033
- featureFlagPayloads: {
1034
- 'bootstrap-1': {
1035
- some: 'key',
1036
- },
1037
- enabled: 200,
1038
- },
1039
- },
1040
- },
1041
- (_mocks) => {
1042
- _mocks.fetch.mockImplementation((url) => {
1043
- if (url.includes('/flags/')) {
1044
- return Promise.resolve({
1045
- status: 200,
1046
- text: () => Promise.resolve('ok'),
1047
- json: () =>
1048
- Promise.resolve({
1049
- flags: createMockFeatureFlags(),
1050
- }),
1051
- })
1052
- }
1053
-
1054
- return Promise.resolve({
1055
- status: 200,
1056
- text: () => Promise.resolve('ok'),
1057
- json: () =>
1058
- Promise.resolve({
1059
- status: 'ok',
1060
- }),
1061
- })
1062
- })
1063
- },
1064
- // Storage cache
1065
- {
1066
- distinct_id: '123',
1067
- feature_flag_details: {
1068
- flags: {
1069
- 'bootstrap-1': {
1070
- key: 'bootstrap-1',
1071
- enabled: true,
1072
- variant: 'variant-2',
1073
- reason: {
1074
- code: 'matched_condition',
1075
- description: 'matched condition set 1',
1076
- condition_index: 0,
1077
- },
1078
- metadata: {
1079
- id: 1,
1080
- version: 1,
1081
- description: 'bootstrap-1',
1082
- payload: '{"some":"other-key"}',
1083
- },
1084
- },
1085
- requestId: '8c865d72-94ef-4088-8b4e-cdb7983f0f81',
1086
- },
1087
- },
1088
- }
1089
- )
1090
- })
1091
-
1092
- it('distinct id should not be overwritten if already there', () => {
1093
- expect(posthog.getDistinctId()).toEqual('123')
1094
- })
1095
-
1096
- it('flags should not be overwritten if already there', () => {
1097
- expect(posthog.getFeatureFlag('bootstrap-1')).toEqual('variant-2')
1098
- })
1099
-
1100
- it('flag payloads should not be overwritten if already there', () => {
1101
- expect(posthog.getFeatureFlagPayload('bootstrap-1')).toEqual({
1102
- some: 'other-key',
1103
- })
1104
- })
1105
- })
1106
- })