@posthog/core 1.0.2 → 1.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.
Files changed (204) hide show
  1. package/dist/error-tracking/chunk-ids.d.ts +5 -0
  2. package/dist/error-tracking/chunk-ids.d.ts.map +1 -0
  3. package/dist/error-tracking/chunk-ids.js +68 -0
  4. package/dist/error-tracking/chunk-ids.mjs +34 -0
  5. package/dist/error-tracking/coercers/dom-exception-coercer.d.ts +10 -0
  6. package/dist/error-tracking/coercers/dom-exception-coercer.d.ts.map +1 -0
  7. package/dist/error-tracking/coercers/dom-exception-coercer.js +65 -0
  8. package/dist/error-tracking/coercers/dom-exception-coercer.mjs +31 -0
  9. package/dist/error-tracking/coercers/error-coercer.d.ts +9 -0
  10. package/dist/error-tracking/coercers/error-coercer.d.ts.map +1 -0
  11. package/dist/error-tracking/coercers/error-coercer.js +61 -0
  12. package/dist/error-tracking/coercers/error-coercer.mjs +27 -0
  13. package/dist/error-tracking/coercers/error-event-coercer.d.ts +7 -0
  14. package/dist/error-tracking/coercers/error-event-coercer.d.ts.map +1 -0
  15. package/dist/error-tracking/coercers/error-event-coercer.js +52 -0
  16. package/dist/error-tracking/coercers/error-event-coercer.mjs +18 -0
  17. package/dist/error-tracking/coercers/event-coercer.d.ts +6 -0
  18. package/dist/error-tracking/coercers/event-coercer.d.ts.map +1 -0
  19. package/dist/error-tracking/coercers/event-coercer.js +51 -0
  20. package/dist/error-tracking/coercers/event-coercer.mjs +17 -0
  21. package/dist/error-tracking/coercers/index.d.ts +9 -0
  22. package/dist/error-tracking/coercers/index.d.ts.map +1 -0
  23. package/dist/error-tracking/coercers/index.js +123 -0
  24. package/dist/error-tracking/coercers/index.mjs +8 -0
  25. package/dist/error-tracking/coercers/object-coercer.d.ts +14 -0
  26. package/dist/error-tracking/coercers/object-coercer.d.ts.map +1 -0
  27. package/dist/error-tracking/coercers/object-coercer.js +85 -0
  28. package/dist/error-tracking/coercers/object-coercer.mjs +51 -0
  29. package/dist/error-tracking/coercers/primitive-coercer.d.ts +7 -0
  30. package/dist/error-tracking/coercers/primitive-coercer.d.ts.map +1 -0
  31. package/dist/error-tracking/coercers/primitive-coercer.js +49 -0
  32. package/dist/error-tracking/coercers/primitive-coercer.mjs +15 -0
  33. package/dist/error-tracking/coercers/promise-rejection-event.d.ts +7 -0
  34. package/dist/error-tracking/coercers/promise-rejection-event.d.ts.map +1 -0
  35. package/dist/error-tracking/coercers/promise-rejection-event.js +59 -0
  36. package/dist/error-tracking/coercers/promise-rejection-event.mjs +25 -0
  37. package/dist/error-tracking/coercers/string-coercer.d.ts +7 -0
  38. package/dist/error-tracking/coercers/string-coercer.d.ts.map +1 -0
  39. package/dist/error-tracking/coercers/string-coercer.js +63 -0
  40. package/dist/error-tracking/coercers/string-coercer.mjs +29 -0
  41. package/dist/error-tracking/coercers/utils.d.ts +8 -0
  42. package/dist/error-tracking/coercers/utils.d.ts.map +1 -0
  43. package/dist/error-tracking/coercers/utils.js +55 -0
  44. package/dist/error-tracking/coercers/utils.mjs +18 -0
  45. package/dist/error-tracking/error-properties-builder.d.ts +18 -0
  46. package/dist/error-tracking/error-properties-builder.d.ts.map +1 -0
  47. package/dist/error-tracking/error-properties-builder.js +152 -0
  48. package/dist/error-tracking/error-properties-builder.mjs +118 -0
  49. package/dist/error-tracking/index.d.ts +6 -0
  50. package/dist/error-tracking/index.d.ts.map +1 -0
  51. package/dist/error-tracking/index.js +87 -0
  52. package/dist/error-tracking/index.mjs +4 -0
  53. package/dist/error-tracking/parsers/base.d.ts +9 -0
  54. package/dist/error-tracking/parsers/base.d.ts.map +1 -0
  55. package/dist/error-tracking/parsers/base.js +71 -0
  56. package/dist/error-tracking/parsers/base.mjs +19 -0
  57. package/dist/error-tracking/parsers/chrome.d.ts +3 -0
  58. package/dist/error-tracking/parsers/chrome.d.ts.map +1 -0
  59. package/dist/error-tracking/parsers/chrome.js +61 -0
  60. package/dist/error-tracking/parsers/chrome.mjs +27 -0
  61. package/dist/error-tracking/parsers/gecko.d.ts +3 -0
  62. package/dist/error-tracking/parsers/gecko.d.ts.map +1 -0
  63. package/dist/error-tracking/parsers/gecko.js +58 -0
  64. package/dist/error-tracking/parsers/gecko.mjs +24 -0
  65. package/dist/error-tracking/parsers/index.d.ts +9 -0
  66. package/dist/error-tracking/parsers/index.d.ts.map +1 -0
  67. package/dist/error-tracking/parsers/index.js +99 -0
  68. package/dist/error-tracking/parsers/index.mjs +44 -0
  69. package/dist/error-tracking/parsers/node.d.ts +3 -0
  70. package/dist/error-tracking/parsers/node.d.ts.map +1 -0
  71. package/dist/error-tracking/parsers/node.js +99 -0
  72. package/dist/error-tracking/parsers/node.mjs +65 -0
  73. package/dist/error-tracking/parsers/opera.d.ts +4 -0
  74. package/dist/error-tracking/parsers/opera.d.ts.map +1 -0
  75. package/dist/error-tracking/parsers/opera.js +49 -0
  76. package/dist/error-tracking/parsers/opera.mjs +12 -0
  77. package/dist/error-tracking/parsers/react-native.d.ts +1 -0
  78. package/dist/error-tracking/parsers/react-native.d.ts.map +1 -0
  79. package/dist/error-tracking/parsers/react-native.js +5 -0
  80. package/dist/error-tracking/parsers/react-native.mjs +0 -0
  81. package/dist/error-tracking/parsers/safari.d.ts +22 -0
  82. package/dist/error-tracking/parsers/safari.d.ts.map +1 -0
  83. package/dist/error-tracking/parsers/safari.js +47 -0
  84. package/dist/error-tracking/parsers/safari.mjs +13 -0
  85. package/dist/error-tracking/parsers/winjs.d.ts +3 -0
  86. package/dist/error-tracking/parsers/winjs.d.ts.map +1 -0
  87. package/dist/error-tracking/parsers/winjs.js +41 -0
  88. package/dist/error-tracking/parsers/winjs.mjs +7 -0
  89. package/dist/error-tracking/types.d.ts +87 -0
  90. package/dist/error-tracking/types.d.ts.map +1 -0
  91. package/dist/error-tracking/types.js +43 -0
  92. package/dist/error-tracking/types.mjs +9 -0
  93. package/dist/error-tracking/utils.d.ts +13 -0
  94. package/dist/error-tracking/utils.d.ts.map +1 -0
  95. package/dist/error-tracking/utils.js +57 -0
  96. package/dist/error-tracking/utils.mjs +23 -0
  97. package/dist/eventemitter.js +4 -4
  98. package/dist/eventemitter.mjs +4 -4
  99. package/dist/featureFlagUtils.js +20 -45
  100. package/dist/featureFlagUtils.mjs +20 -45
  101. package/dist/gzip.js +1 -2
  102. package/dist/gzip.mjs +1 -2
  103. package/dist/index.d.ts +5 -255
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +61 -1223
  106. package/dist/index.mjs +6 -1190
  107. package/dist/posthog-core-stateless.d.ts +204 -0
  108. package/dist/posthog-core-stateless.d.ts.map +1 -0
  109. package/dist/posthog-core-stateless.js +675 -0
  110. package/dist/posthog-core-stateless.mjs +632 -0
  111. package/dist/posthog-core.d.ts +171 -0
  112. package/dist/posthog-core.d.ts.map +1 -0
  113. package/dist/posthog-core.js +554 -0
  114. package/dist/posthog-core.mjs +520 -0
  115. package/dist/testing/PostHogCoreTestClient.d.ts +2 -1
  116. package/dist/testing/PostHogCoreTestClient.d.ts.map +1 -1
  117. package/dist/testing/PostHogCoreTestClient.js +9 -11
  118. package/dist/testing/PostHogCoreTestClient.mjs +8 -10
  119. package/dist/testing/test-utils.d.ts +2 -0
  120. package/dist/testing/test-utils.d.ts.map +1 -1
  121. package/dist/testing/test-utils.js +13 -1
  122. package/dist/testing/test-utils.mjs +11 -2
  123. package/dist/utils/bucketed-rate-limiter.d.ts.map +1 -1
  124. package/dist/utils/bucketed-rate-limiter.js +8 -12
  125. package/dist/utils/bucketed-rate-limiter.mjs +8 -12
  126. package/dist/utils/index.js +3 -3
  127. package/dist/utils/index.mjs +3 -3
  128. package/dist/utils/number-utils.d.ts.map +1 -1
  129. package/dist/utils/type-utils.d.ts +8 -1
  130. package/dist/utils/type-utils.d.ts.map +1 -1
  131. package/dist/utils/type-utils.js +61 -6
  132. package/dist/utils/type-utils.mjs +36 -2
  133. package/dist/vendor/uuidv7.js +12 -16
  134. package/dist/vendor/uuidv7.mjs +12 -16
  135. package/package.json +10 -4
  136. package/src/__tests__/featureFlagUtils.spec.ts +427 -0
  137. package/src/__tests__/gzip.spec.ts +69 -0
  138. package/src/__tests__/posthog.ai.spec.ts +110 -0
  139. package/src/__tests__/posthog.capture.spec.ts +91 -0
  140. package/src/__tests__/posthog.core.spec.ts +135 -0
  141. package/src/__tests__/posthog.debug.spec.ts +36 -0
  142. package/src/__tests__/posthog.enqueue.spec.ts +93 -0
  143. package/src/__tests__/posthog.featureflags.spec.ts +1106 -0
  144. package/src/__tests__/posthog.featureflags.v1.spec.ts +922 -0
  145. package/src/__tests__/posthog.flush.spec.ts +237 -0
  146. package/src/__tests__/posthog.gdpr.spec.ts +50 -0
  147. package/src/__tests__/posthog.groups.spec.ts +96 -0
  148. package/src/__tests__/posthog.identify.spec.ts +194 -0
  149. package/src/__tests__/posthog.init.spec.ts +110 -0
  150. package/src/__tests__/posthog.listeners.spec.ts +51 -0
  151. package/src/__tests__/posthog.register.spec.ts +47 -0
  152. package/src/__tests__/posthog.reset.spec.ts +76 -0
  153. package/src/__tests__/posthog.sessions.spec.ts +63 -0
  154. package/src/__tests__/posthog.setProperties.spec.ts +102 -0
  155. package/src/__tests__/posthog.shutdown.spec.ts +88 -0
  156. package/src/__tests__/utils.spec.ts +36 -0
  157. package/src/error-tracking/chunk-ids.ts +58 -0
  158. package/src/error-tracking/coercers/dom-exception-coercer.ts +38 -0
  159. package/src/error-tracking/coercers/error-coercer.ts +36 -0
  160. package/src/error-tracking/coercers/error-event-coercer.ts +24 -0
  161. package/src/error-tracking/coercers/event-coercer.ts +19 -0
  162. package/src/error-tracking/coercers/index.ts +8 -0
  163. package/src/error-tracking/coercers/object-coercer.ts +76 -0
  164. package/src/error-tracking/coercers/primitive-coercer.ts +19 -0
  165. package/src/error-tracking/coercers/promise-rejection-event.spec.ts +77 -0
  166. package/src/error-tracking/coercers/promise-rejection-event.ts +53 -0
  167. package/src/error-tracking/coercers/string-coercer.spec.ts +26 -0
  168. package/src/error-tracking/coercers/string-coercer.ts +31 -0
  169. package/src/error-tracking/coercers/utils.ts +33 -0
  170. package/src/error-tracking/error-properties-builder.coerce.spec.ts +202 -0
  171. package/src/error-tracking/error-properties-builder.parse.spec.ts +30 -0
  172. package/src/error-tracking/error-properties-builder.ts +169 -0
  173. package/src/error-tracking/index.ts +5 -0
  174. package/src/error-tracking/parsers/base.ts +29 -0
  175. package/src/error-tracking/parsers/chrome.ts +53 -0
  176. package/src/error-tracking/parsers/gecko.ts +38 -0
  177. package/src/error-tracking/parsers/index.ts +104 -0
  178. package/src/error-tracking/parsers/node.ts +111 -0
  179. package/src/error-tracking/parsers/opera.ts +18 -0
  180. package/src/error-tracking/parsers/react-native.ts +0 -0
  181. package/src/error-tracking/parsers/safari.ts +33 -0
  182. package/src/error-tracking/parsers/winjs.ts +12 -0
  183. package/src/error-tracking/types.ts +107 -0
  184. package/src/error-tracking/utils.ts +39 -0
  185. package/src/eventemitter.ts +27 -0
  186. package/src/featureFlagUtils.ts +192 -0
  187. package/src/gzip.ts +29 -0
  188. package/src/index.ts +8 -0
  189. package/src/posthog-core-stateless.ts +1226 -0
  190. package/src/posthog-core.ts +958 -0
  191. package/src/testing/PostHogCoreTestClient.ts +91 -0
  192. package/src/testing/index.ts +2 -0
  193. package/src/testing/test-utils.ts +47 -0
  194. package/src/types.ts +544 -0
  195. package/src/utils/bucketed-rate-limiter.spec.ts +33 -0
  196. package/src/utils/bucketed-rate-limiter.ts +85 -0
  197. package/src/utils/index.ts +98 -0
  198. package/src/utils/number-utils.spec.ts +89 -0
  199. package/src/utils/number-utils.ts +30 -0
  200. package/src/utils/promise-queue.spec.ts +55 -0
  201. package/src/utils/promise-queue.ts +30 -0
  202. package/src/utils/string-utils.ts +23 -0
  203. package/src/utils/type-utils.ts +134 -0
  204. package/src/vendor/uuidv7.ts +479 -0
@@ -0,0 +1,1106 @@
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
+ })