posthog-node 4.10.1 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -6
- package/lib/index.cjs.js +437 -114
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +323 -26
- package/lib/index.esm.js +419 -114
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/featureFlagUtils.d.ts +34 -0
- package/lib/posthog-core/src/index.d.ts +32 -7
- package/lib/posthog-core/src/types.d.ts +109 -18
- package/lib/posthog-node/src/crypto-helpers.d.ts +3 -0
- package/lib/posthog-node/src/crypto.d.ts +2 -0
- package/lib/posthog-node/src/feature-flags.d.ts +10 -10
- package/lib/posthog-node/src/lazy.d.ts +23 -0
- package/lib/posthog-node/src/posthog-node.d.ts +5 -4
- package/lib/posthog-node/src/types.d.ts +3 -3
- package/lib/posthog-node/test/test-utils.d.ts +4 -1
- package/package.json +1 -1
- package/src/crypto-helpers.ts +36 -0
- package/src/crypto.ts +22 -0
- package/src/feature-flags.ts +119 -91
- package/src/lazy.ts +55 -0
- package/src/posthog-node.ts +44 -27
- package/src/types.ts +3 -3
- package/test/crypto.spec.ts +36 -0
- package/test/feature-flags.decide.spec.ts +293 -0
- package/test/feature-flags.spec.ts +2 -2
- package/test/lazy.spec.ts +71 -0
- package/test/posthog-node.spec.ts +49 -18
- package/test/test-utils.ts +27 -1
- package/benchmarks/rusha-vs-native.mjs +0 -70
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { PostHog as PostHog, PostHogOptions } from '../src/posthog-node'
|
|
2
|
+
import fetch from '../src/fetch'
|
|
3
|
+
import { apiImplementation, apiImplementationV4 } from './test-utils'
|
|
4
|
+
import { waitForPromises } from 'posthog-core/test/test-utils/test-utils'
|
|
5
|
+
import { PostHogV4DecideResponse } from 'posthog-core/src/types'
|
|
6
|
+
jest.mock('../src/fetch')
|
|
7
|
+
|
|
8
|
+
jest.spyOn(console, 'debug').mockImplementation()
|
|
9
|
+
|
|
10
|
+
const mockedFetch = jest.mocked(fetch, true)
|
|
11
|
+
|
|
12
|
+
const posthogImmediateResolveOptions: PostHogOptions = {
|
|
13
|
+
fetchRetryCount: 0,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('decide v4', () => {
|
|
17
|
+
describe('getFeatureFlag v4', () => {
|
|
18
|
+
it('returns false if the flag is not found', async () => {
|
|
19
|
+
const decideResponse: PostHogV4DecideResponse = {
|
|
20
|
+
flags: {},
|
|
21
|
+
errorsWhileComputingFlags: false,
|
|
22
|
+
requestId: '0152a345-295f-4fba-adac-2e6ea9c91082',
|
|
23
|
+
}
|
|
24
|
+
mockedFetch.mockImplementation(apiImplementationV4(decideResponse))
|
|
25
|
+
|
|
26
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
27
|
+
host: 'http://example.com',
|
|
28
|
+
...posthogImmediateResolveOptions,
|
|
29
|
+
})
|
|
30
|
+
let capturedMessage: any
|
|
31
|
+
posthog.on('capture', (message) => {
|
|
32
|
+
capturedMessage = message
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const result = await posthog.getFeatureFlag('non-existent-flag', 'some-distinct-id')
|
|
36
|
+
|
|
37
|
+
expect(result).toBe(false)
|
|
38
|
+
expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object))
|
|
39
|
+
|
|
40
|
+
await waitForPromises()
|
|
41
|
+
expect(capturedMessage).toMatchObject({
|
|
42
|
+
distinct_id: 'some-distinct-id',
|
|
43
|
+
event: '$feature_flag_called',
|
|
44
|
+
library: posthog.getLibraryId(),
|
|
45
|
+
library_version: posthog.getLibraryVersion(),
|
|
46
|
+
properties: {
|
|
47
|
+
'$feature/non-existent-flag': false,
|
|
48
|
+
$feature_flag: 'non-existent-flag',
|
|
49
|
+
$feature_flag_response: false,
|
|
50
|
+
$feature_flag_request_id: '0152a345-295f-4fba-adac-2e6ea9c91082',
|
|
51
|
+
$groups: undefined,
|
|
52
|
+
$lib: posthog.getLibraryId(),
|
|
53
|
+
$lib_version: posthog.getLibraryVersion(),
|
|
54
|
+
locally_evaluated: false,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it.each([
|
|
60
|
+
{
|
|
61
|
+
key: 'variant-flag',
|
|
62
|
+
expectedResponse: 'variant-value',
|
|
63
|
+
expectedReason: 'Matched condition set 3',
|
|
64
|
+
expectedId: 2,
|
|
65
|
+
expectedVersion: 23,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: 'boolean-flag',
|
|
69
|
+
expectedResponse: true,
|
|
70
|
+
expectedReason: 'Matched condition set 1',
|
|
71
|
+
expectedId: 1,
|
|
72
|
+
expectedVersion: 12,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'non-matching-flag',
|
|
76
|
+
expectedResponse: false,
|
|
77
|
+
expectedReason: 'Did not match any condition',
|
|
78
|
+
expectedId: 3,
|
|
79
|
+
expectedVersion: 2,
|
|
80
|
+
},
|
|
81
|
+
])(
|
|
82
|
+
'captures a feature flag called event with extra metadata when the flag is found',
|
|
83
|
+
async ({ key, expectedResponse, expectedReason, expectedId, expectedVersion }) => {
|
|
84
|
+
const decideResponse: PostHogV4DecideResponse = {
|
|
85
|
+
flags: {
|
|
86
|
+
'variant-flag': {
|
|
87
|
+
key: 'variant-flag',
|
|
88
|
+
enabled: true,
|
|
89
|
+
variant: 'variant-value',
|
|
90
|
+
reason: {
|
|
91
|
+
code: 'variant',
|
|
92
|
+
condition_index: 2,
|
|
93
|
+
description: 'Matched condition set 3',
|
|
94
|
+
},
|
|
95
|
+
metadata: {
|
|
96
|
+
id: 2,
|
|
97
|
+
version: 23,
|
|
98
|
+
payload: '{"key": "value"}',
|
|
99
|
+
description: 'description',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
'boolean-flag': {
|
|
103
|
+
key: 'boolean-flag',
|
|
104
|
+
enabled: true,
|
|
105
|
+
variant: undefined,
|
|
106
|
+
reason: {
|
|
107
|
+
code: 'boolean',
|
|
108
|
+
condition_index: 1,
|
|
109
|
+
description: 'Matched condition set 1',
|
|
110
|
+
},
|
|
111
|
+
metadata: {
|
|
112
|
+
id: 1,
|
|
113
|
+
version: 12,
|
|
114
|
+
payload: undefined,
|
|
115
|
+
description: 'description',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
'non-matching-flag': {
|
|
119
|
+
key: 'non-matching-flag',
|
|
120
|
+
enabled: false,
|
|
121
|
+
variant: undefined,
|
|
122
|
+
reason: {
|
|
123
|
+
code: 'boolean',
|
|
124
|
+
condition_index: 1,
|
|
125
|
+
description: 'Did not match any condition',
|
|
126
|
+
},
|
|
127
|
+
metadata: {
|
|
128
|
+
id: 3,
|
|
129
|
+
version: 2,
|
|
130
|
+
payload: undefined,
|
|
131
|
+
description: 'description',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
errorsWhileComputingFlags: false,
|
|
136
|
+
requestId: '0152a345-295f-4fba-adac-2e6ea9c91082',
|
|
137
|
+
}
|
|
138
|
+
mockedFetch.mockImplementation(apiImplementationV4(decideResponse))
|
|
139
|
+
|
|
140
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
141
|
+
host: 'http://example.com',
|
|
142
|
+
...posthogImmediateResolveOptions,
|
|
143
|
+
})
|
|
144
|
+
let capturedMessage: any
|
|
145
|
+
posthog.on('capture', (message) => {
|
|
146
|
+
capturedMessage = message
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const result = await posthog.getFeatureFlag(key, 'some-distinct-id')
|
|
150
|
+
|
|
151
|
+
expect(result).toBe(expectedResponse)
|
|
152
|
+
expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object))
|
|
153
|
+
|
|
154
|
+
await waitForPromises()
|
|
155
|
+
expect(capturedMessage).toMatchObject({
|
|
156
|
+
distinct_id: 'some-distinct-id',
|
|
157
|
+
event: '$feature_flag_called',
|
|
158
|
+
library: posthog.getLibraryId(),
|
|
159
|
+
library_version: posthog.getLibraryVersion(),
|
|
160
|
+
properties: {
|
|
161
|
+
[`$feature/${key}`]: expectedResponse,
|
|
162
|
+
$feature_flag: key,
|
|
163
|
+
$feature_flag_response: expectedResponse,
|
|
164
|
+
$feature_flag_id: expectedId,
|
|
165
|
+
$feature_flag_version: expectedVersion,
|
|
166
|
+
$feature_flag_reason: expectedReason,
|
|
167
|
+
$feature_flag_request_id: '0152a345-295f-4fba-adac-2e6ea9c91082',
|
|
168
|
+
$groups: undefined,
|
|
169
|
+
$lib: posthog.getLibraryId(),
|
|
170
|
+
$lib_version: posthog.getLibraryVersion(),
|
|
171
|
+
locally_evaluated: false,
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
describe('getFeatureFlagPayload v4', () => {
|
|
178
|
+
it('returns payload', async () => {
|
|
179
|
+
mockedFetch.mockImplementation(
|
|
180
|
+
apiImplementationV4({
|
|
181
|
+
flags: {
|
|
182
|
+
'flag-with-payload': {
|
|
183
|
+
key: 'flag-with-payload',
|
|
184
|
+
enabled: true,
|
|
185
|
+
variant: undefined,
|
|
186
|
+
reason: {
|
|
187
|
+
code: 'boolean',
|
|
188
|
+
condition_index: 1,
|
|
189
|
+
description: 'Matched condition set 2',
|
|
190
|
+
},
|
|
191
|
+
metadata: {
|
|
192
|
+
id: 1,
|
|
193
|
+
version: 12,
|
|
194
|
+
payload: '[0, 1, 2]',
|
|
195
|
+
description: 'description',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
errorsWhileComputingFlags: false,
|
|
200
|
+
})
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
204
|
+
host: 'http://example.com',
|
|
205
|
+
...posthogImmediateResolveOptions,
|
|
206
|
+
})
|
|
207
|
+
let capturedMessage: any
|
|
208
|
+
posthog.on('capture', (message) => {
|
|
209
|
+
capturedMessage = message
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const result = await posthog.getFeatureFlagPayload('flag-with-payload', 'some-distinct-id')
|
|
213
|
+
|
|
214
|
+
expect(result).toEqual([0, 1, 2])
|
|
215
|
+
expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object))
|
|
216
|
+
|
|
217
|
+
await waitForPromises()
|
|
218
|
+
expect(capturedMessage).toBeUndefined()
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('decide v3', () => {
|
|
225
|
+
describe('getFeatureFlag v3', () => {
|
|
226
|
+
it('returns false if the flag is not found', async () => {
|
|
227
|
+
mockedFetch.mockImplementation(apiImplementation({ decideFlags: {} }))
|
|
228
|
+
|
|
229
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
230
|
+
host: 'http://example.com',
|
|
231
|
+
...posthogImmediateResolveOptions,
|
|
232
|
+
})
|
|
233
|
+
let capturedMessage: any
|
|
234
|
+
posthog.on('capture', (message) => {
|
|
235
|
+
capturedMessage = message
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const result = await posthog.getFeatureFlag('non-existent-flag', 'some-distinct-id')
|
|
239
|
+
|
|
240
|
+
expect(result).toBe(false)
|
|
241
|
+
expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object))
|
|
242
|
+
|
|
243
|
+
await waitForPromises()
|
|
244
|
+
expect(capturedMessage).toMatchObject({
|
|
245
|
+
distinct_id: 'some-distinct-id',
|
|
246
|
+
event: '$feature_flag_called',
|
|
247
|
+
library: posthog.getLibraryId(),
|
|
248
|
+
library_version: posthog.getLibraryVersion(),
|
|
249
|
+
properties: {
|
|
250
|
+
'$feature/non-existent-flag': false,
|
|
251
|
+
$feature_flag: 'non-existent-flag',
|
|
252
|
+
$feature_flag_response: false,
|
|
253
|
+
$groups: undefined,
|
|
254
|
+
$lib: posthog.getLibraryId(),
|
|
255
|
+
$lib_version: posthog.getLibraryVersion(),
|
|
256
|
+
locally_evaluated: false,
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('getFeatureFlagPayload v3', () => {
|
|
262
|
+
it('returns payload', async () => {
|
|
263
|
+
mockedFetch.mockImplementation(
|
|
264
|
+
apiImplementation({
|
|
265
|
+
decideFlags: {
|
|
266
|
+
'flag-with-payload': true,
|
|
267
|
+
},
|
|
268
|
+
decideFlagPayloads: {
|
|
269
|
+
'flag-with-payload': [0, 1, 2],
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
275
|
+
host: 'http://example.com',
|
|
276
|
+
...posthogImmediateResolveOptions,
|
|
277
|
+
})
|
|
278
|
+
let capturedMessage: any = undefined
|
|
279
|
+
posthog.on('capture', (message) => {
|
|
280
|
+
capturedMessage = message
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const result = await posthog.getFeatureFlagPayload('flag-with-payload', 'some-distinct-id')
|
|
284
|
+
|
|
285
|
+
expect(result).toEqual([0, 1, 2])
|
|
286
|
+
expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object))
|
|
287
|
+
|
|
288
|
+
await waitForPromises()
|
|
289
|
+
expect(capturedMessage).toBeUndefined()
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
@@ -347,7 +347,7 @@ describe('local evaluation', () => {
|
|
|
347
347
|
})
|
|
348
348
|
).toEqual('decide-fallback-value')
|
|
349
349
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
350
|
-
'http://example.com/decide/?v=
|
|
350
|
+
'http://example.com/decide/?v=4',
|
|
351
351
|
expect.objectContaining({
|
|
352
352
|
body: JSON.stringify({
|
|
353
353
|
token: 'TEST_API_KEY',
|
|
@@ -371,7 +371,7 @@ describe('local evaluation', () => {
|
|
|
371
371
|
await posthog.getFeatureFlag('complex-flag', 'some-distinct-id', { personProperties: { doesnt_matter: '1' } })
|
|
372
372
|
).toEqual('decide-fallback-value')
|
|
373
373
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
374
|
-
'http://example.com/decide/?v=
|
|
374
|
+
'http://example.com/decide/?v=4',
|
|
375
375
|
expect.objectContaining({
|
|
376
376
|
body: JSON.stringify({
|
|
377
377
|
token: 'TEST_API_KEY',
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Lazy } from '../src/lazy'
|
|
2
|
+
|
|
3
|
+
describe('Lazy', () => {
|
|
4
|
+
it('should only call the factory once', async (): Promise<void> => {
|
|
5
|
+
let callCount = 0
|
|
6
|
+
const factory = async (): Promise<string> => {
|
|
7
|
+
callCount++
|
|
8
|
+
return 'value'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const lazy = new Lazy(factory)
|
|
12
|
+
expect(callCount).toBe(0)
|
|
13
|
+
|
|
14
|
+
const value1 = await lazy.getValue()
|
|
15
|
+
expect(value1).toBe('value')
|
|
16
|
+
expect(callCount).toBe(1)
|
|
17
|
+
|
|
18
|
+
const value2 = await lazy.getValue()
|
|
19
|
+
expect(value2).toBe('value')
|
|
20
|
+
expect(callCount).toBe(1)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should handle errors in the factory', async (): Promise<void> => {
|
|
24
|
+
const factory = async (): Promise<string> => {
|
|
25
|
+
throw new Error('Factory error')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lazy = new Lazy(factory)
|
|
29
|
+
await expect(lazy.getValue()).rejects.toThrow('Factory error')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should handle undefined values', async (): Promise<void> => {
|
|
33
|
+
const factory = async (): Promise<undefined> => {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lazy = new Lazy(factory)
|
|
38
|
+
const value = await lazy.getValue()
|
|
39
|
+
expect(value).toBeUndefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should handle complex types', async (): Promise<void> => {
|
|
43
|
+
interface ComplexType {
|
|
44
|
+
id: number
|
|
45
|
+
name: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const factory = async (): Promise<ComplexType> => {
|
|
49
|
+
return { id: 1, name: 'test' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lazy = new Lazy<ComplexType>(factory)
|
|
53
|
+
const value = await lazy.getValue()
|
|
54
|
+
expect(value).toEqual({ id: 1, name: 'test' })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should handle concurrent calls', async (): Promise<void> => {
|
|
58
|
+
let callCount = 0
|
|
59
|
+
const factory = async (): Promise<string> => {
|
|
60
|
+
callCount++
|
|
61
|
+
return 'value'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lazy = new Lazy(factory)
|
|
65
|
+
const [value1, value2] = await Promise.all([lazy.getValue(), lazy.getValue()])
|
|
66
|
+
|
|
67
|
+
expect(value1).toBe('value')
|
|
68
|
+
expect(value2).toBe('value')
|
|
69
|
+
expect(callCount).toBe(1)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -328,6 +328,17 @@ describe('PostHog Node.js', () => {
|
|
|
328
328
|
|
|
329
329
|
await client.shutdown()
|
|
330
330
|
})
|
|
331
|
+
|
|
332
|
+
it('should warn if capture is called with a string', () => {
|
|
333
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
334
|
+
posthog.debug(true)
|
|
335
|
+
// @ts-expect-error - Testing the warning when passing a string instead of an object
|
|
336
|
+
posthog.capture('test-event')
|
|
337
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
338
|
+
'Called capture() with a string as the first argument when an object was expected.'
|
|
339
|
+
)
|
|
340
|
+
warnSpy.mockRestore()
|
|
341
|
+
})
|
|
331
342
|
})
|
|
332
343
|
|
|
333
344
|
describe('shutdown', () => {
|
|
@@ -603,7 +614,7 @@ describe('PostHog Node.js', () => {
|
|
|
603
614
|
)
|
|
604
615
|
expect(mockedFetch).toHaveBeenCalledTimes(1)
|
|
605
616
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
606
|
-
'http://example.com/decide/?v=
|
|
617
|
+
'http://example.com/decide/?v=4',
|
|
607
618
|
expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
|
|
608
619
|
)
|
|
609
620
|
})
|
|
@@ -636,7 +647,7 @@ describe('PostHog Node.js', () => {
|
|
|
636
647
|
await waitForPromises()
|
|
637
648
|
|
|
638
649
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
639
|
-
'http://example.com/decide/?v=
|
|
650
|
+
'http://example.com/decide/?v=4',
|
|
640
651
|
expect.objectContaining({ method: 'POST' })
|
|
641
652
|
)
|
|
642
653
|
|
|
@@ -645,7 +656,7 @@ describe('PostHog Node.js', () => {
|
|
|
645
656
|
distinct_id: 'distinct_id',
|
|
646
657
|
event: 'node test event',
|
|
647
658
|
properties: expect.objectContaining({
|
|
648
|
-
$active_feature_flags: ['feature-1', 'feature-2', 'feature-
|
|
659
|
+
$active_feature_flags: ['feature-1', 'feature-2', 'feature-array', 'feature-variant'],
|
|
649
660
|
'$feature/feature-1': true,
|
|
650
661
|
'$feature/feature-2': true,
|
|
651
662
|
'$feature/feature-array': true,
|
|
@@ -661,7 +672,7 @@ describe('PostHog Node.js', () => {
|
|
|
661
672
|
|
|
662
673
|
expect(mockedFetch).not.toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
663
674
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
664
|
-
'http://example.com/decide/?v=
|
|
675
|
+
'http://example.com/decide/?v=4',
|
|
665
676
|
expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
|
|
666
677
|
)
|
|
667
678
|
})
|
|
@@ -721,7 +732,7 @@ describe('PostHog Node.js', () => {
|
|
|
721
732
|
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
722
733
|
// no decide call
|
|
723
734
|
expect(mockedFetch).not.toHaveBeenCalledWith(
|
|
724
|
-
'http://example.com/decide/?v=
|
|
735
|
+
'http://example.com/decide/?v=4',
|
|
725
736
|
expect.objectContaining({ method: 'POST' })
|
|
726
737
|
)
|
|
727
738
|
|
|
@@ -779,7 +790,7 @@ describe('PostHog Node.js', () => {
|
|
|
779
790
|
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
780
791
|
// no decide call
|
|
781
792
|
expect(mockedFetch).not.toHaveBeenCalledWith(
|
|
782
|
-
'http://example.com/decide/?v=
|
|
793
|
+
'http://example.com/decide/?v=4',
|
|
783
794
|
expect.objectContaining({ method: 'POST' })
|
|
784
795
|
)
|
|
785
796
|
|
|
@@ -827,12 +838,12 @@ describe('PostHog Node.js', () => {
|
|
|
827
838
|
await waitForFlushTimer()
|
|
828
839
|
|
|
829
840
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
830
|
-
'http://example.com/decide/?v=
|
|
841
|
+
'http://example.com/decide/?v=4',
|
|
831
842
|
expect.objectContaining({ method: 'POST', body: expect.not.stringContaining('geoip_disable') })
|
|
832
843
|
)
|
|
833
844
|
|
|
834
845
|
expect(getLastBatchEvents()?.[0].properties).toEqual({
|
|
835
|
-
$active_feature_flags: ['feature-1', 'feature-2', 'feature-
|
|
846
|
+
$active_feature_flags: ['feature-1', 'feature-2', 'feature-array', 'feature-variant'],
|
|
836
847
|
'$feature/feature-1': true,
|
|
837
848
|
'$feature/feature-2': true,
|
|
838
849
|
'$feature/feature-array': true,
|
|
@@ -1080,7 +1091,7 @@ describe('PostHog Node.js', () => {
|
|
|
1080
1091
|
).resolves.toEqual(2)
|
|
1081
1092
|
expect(mockedFetch).toHaveBeenCalledTimes(1)
|
|
1082
1093
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1083
|
-
'http://example.com/decide/?v=
|
|
1094
|
+
'http://example.com/decide/?v=4',
|
|
1084
1095
|
expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
|
|
1085
1096
|
)
|
|
1086
1097
|
})
|
|
@@ -1122,7 +1133,7 @@ describe('PostHog Node.js', () => {
|
|
|
1122
1133
|
).resolves.toEqual([1])
|
|
1123
1134
|
expect(mockedFetch).toHaveBeenCalledTimes(1)
|
|
1124
1135
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1125
|
-
'http://example.com/decide/?v=
|
|
1136
|
+
'http://example.com/decide/?v=4',
|
|
1126
1137
|
expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
|
|
1127
1138
|
)
|
|
1128
1139
|
})
|
|
@@ -1142,7 +1153,7 @@ describe('PostHog Node.js', () => {
|
|
|
1142
1153
|
).resolves.toEqual(2)
|
|
1143
1154
|
expect(mockedFetch).toHaveBeenCalledTimes(1)
|
|
1144
1155
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1145
|
-
'http://example.com/decide/?v=
|
|
1156
|
+
'http://example.com/decide/?v=4',
|
|
1146
1157
|
expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
|
|
1147
1158
|
)
|
|
1148
1159
|
|
|
@@ -1151,7 +1162,7 @@ describe('PostHog Node.js', () => {
|
|
|
1151
1162
|
await expect(posthog.isFeatureEnabled('feature-variant', '123', { disableGeoip: false })).resolves.toEqual(true)
|
|
1152
1163
|
expect(mockedFetch).toHaveBeenCalledTimes(1)
|
|
1153
1164
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1154
|
-
'http://example.com/decide/?v=
|
|
1165
|
+
'http://example.com/decide/?v=4',
|
|
1155
1166
|
expect.objectContaining({ method: 'POST', body: expect.not.stringContaining('geoip_disable') })
|
|
1156
1167
|
)
|
|
1157
1168
|
})
|
|
@@ -1165,7 +1176,7 @@ describe('PostHog Node.js', () => {
|
|
|
1165
1176
|
jest.runOnlyPendingTimers()
|
|
1166
1177
|
|
|
1167
1178
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1168
|
-
'http://example.com/decide/?v=
|
|
1179
|
+
'http://example.com/decide/?v=4',
|
|
1169
1180
|
expect.objectContaining({
|
|
1170
1181
|
body: JSON.stringify({
|
|
1171
1182
|
token: 'TEST_API_KEY',
|
|
@@ -1195,7 +1206,7 @@ describe('PostHog Node.js', () => {
|
|
|
1195
1206
|
jest.runOnlyPendingTimers()
|
|
1196
1207
|
|
|
1197
1208
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1198
|
-
'http://example.com/decide/?v=
|
|
1209
|
+
'http://example.com/decide/?v=4',
|
|
1199
1210
|
expect.objectContaining({
|
|
1200
1211
|
body: JSON.stringify({
|
|
1201
1212
|
token: 'TEST_API_KEY',
|
|
@@ -1226,7 +1237,7 @@ describe('PostHog Node.js', () => {
|
|
|
1226
1237
|
jest.runOnlyPendingTimers()
|
|
1227
1238
|
|
|
1228
1239
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1229
|
-
'http://example.com/decide/?v=
|
|
1240
|
+
'http://example.com/decide/?v=4',
|
|
1230
1241
|
expect.objectContaining({
|
|
1231
1242
|
body: JSON.stringify({
|
|
1232
1243
|
token: 'TEST_API_KEY',
|
|
@@ -1250,7 +1261,7 @@ describe('PostHog Node.js', () => {
|
|
|
1250
1261
|
jest.runOnlyPendingTimers()
|
|
1251
1262
|
|
|
1252
1263
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1253
|
-
'http://example.com/decide/?v=
|
|
1264
|
+
'http://example.com/decide/?v=4',
|
|
1254
1265
|
expect.objectContaining({
|
|
1255
1266
|
body: JSON.stringify({
|
|
1256
1267
|
token: 'TEST_API_KEY',
|
|
@@ -1270,7 +1281,7 @@ describe('PostHog Node.js', () => {
|
|
|
1270
1281
|
jest.runOnlyPendingTimers()
|
|
1271
1282
|
|
|
1272
1283
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1273
|
-
'http://example.com/decide/?v=
|
|
1284
|
+
'http://example.com/decide/?v=4',
|
|
1274
1285
|
expect.objectContaining({
|
|
1275
1286
|
body: JSON.stringify({
|
|
1276
1287
|
token: 'TEST_API_KEY',
|
|
@@ -1292,7 +1303,7 @@ describe('PostHog Node.js', () => {
|
|
|
1292
1303
|
jest.runOnlyPendingTimers()
|
|
1293
1304
|
|
|
1294
1305
|
expect(mockedFetch).toHaveBeenCalledWith(
|
|
1295
|
-
'http://example.com/decide/?v=
|
|
1306
|
+
'http://example.com/decide/?v=4',
|
|
1296
1307
|
expect.objectContaining({
|
|
1297
1308
|
body: JSON.stringify({
|
|
1298
1309
|
token: 'TEST_API_KEY',
|
|
@@ -1308,5 +1319,25 @@ describe('PostHog Node.js', () => {
|
|
|
1308
1319
|
})
|
|
1309
1320
|
)
|
|
1310
1321
|
})
|
|
1322
|
+
|
|
1323
|
+
it('should log error when decide response has errors', async () => {
|
|
1324
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
1325
|
+
|
|
1326
|
+
mockedFetch.mockImplementation(
|
|
1327
|
+
apiImplementation({
|
|
1328
|
+
decideFlags: { 'feature-1': true },
|
|
1329
|
+
decideFlagPayloads: {},
|
|
1330
|
+
errorsWhileComputingFlags: true,
|
|
1331
|
+
})
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
await posthog.getFeatureFlag('feature-1', '123')
|
|
1335
|
+
|
|
1336
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1337
|
+
'[FEATURE FLAGS] Error while computing feature flags, some flags may be missing or incorrect. Learn more at https://posthog.com/docs/feature-flags/best-practices'
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
errorSpy.mockRestore()
|
|
1341
|
+
})
|
|
1311
1342
|
})
|
|
1312
1343
|
})
|
package/test/test-utils.ts
CHANGED
|
@@ -1,15 +1,40 @@
|
|
|
1
|
+
import { PostHogV4DecideResponse } from 'posthog-core/src/types'
|
|
2
|
+
|
|
3
|
+
export const apiImplementationV4 = (decideResponse?: PostHogV4DecideResponse) => {
|
|
4
|
+
return (url: any): Promise<any> => {
|
|
5
|
+
if ((url as any).includes('/decide/?v=4')) {
|
|
6
|
+
return Promise.resolve({
|
|
7
|
+
status: 200,
|
|
8
|
+
text: () => Promise.resolve('ok'),
|
|
9
|
+
json: () => Promise.resolve(decideResponse),
|
|
10
|
+
}) as any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return Promise.resolve({
|
|
14
|
+
status: 400,
|
|
15
|
+
text: () => Promise.resolve('ok'),
|
|
16
|
+
json: () =>
|
|
17
|
+
Promise.resolve({
|
|
18
|
+
status: 'ok',
|
|
19
|
+
}),
|
|
20
|
+
}) as any
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
export const apiImplementation = ({
|
|
2
25
|
localFlags,
|
|
3
26
|
decideFlags,
|
|
4
27
|
decideFlagPayloads,
|
|
5
28
|
decideStatus = 200,
|
|
6
29
|
localFlagsStatus = 200,
|
|
30
|
+
errorsWhileComputingFlags = false,
|
|
7
31
|
}: {
|
|
8
32
|
localFlags?: any
|
|
9
33
|
decideFlags?: any
|
|
10
34
|
decideFlagPayloads?: any
|
|
11
35
|
decideStatus?: number
|
|
12
36
|
localFlagsStatus?: number
|
|
37
|
+
errorsWhileComputingFlags?: boolean
|
|
13
38
|
}) => {
|
|
14
39
|
return (url: any): Promise<any> => {
|
|
15
40
|
if ((url as any).includes('/decide/')) {
|
|
@@ -25,6 +50,7 @@ export const apiImplementation = ({
|
|
|
25
50
|
featureFlagPayloads: Object.fromEntries(
|
|
26
51
|
Object.entries(decideFlagPayloads || {}).map(([k, v]) => [k, JSON.stringify(v)])
|
|
27
52
|
),
|
|
53
|
+
errorsWhileComputingFlags,
|
|
28
54
|
})
|
|
29
55
|
}
|
|
30
56
|
},
|
|
@@ -65,4 +91,4 @@ export const anyLocalEvalCall = [
|
|
|
65
91
|
'http://example.com/api/feature_flag/local_evaluation?token=TEST_API_KEY&send_cohorts',
|
|
66
92
|
expect.any(Object),
|
|
67
93
|
]
|
|
68
|
-
export const anyDecideCall = ['http://example.com/decide/?v=
|
|
94
|
+
export const anyDecideCall = ['http://example.com/decide/?v=4', expect.any(Object)]
|