posthog-node 2.5.4 → 3.0.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.
@@ -38,7 +38,7 @@ export const apiImplementation = ({
38
38
  }) as any
39
39
  }
40
40
 
41
- if ((url as any).includes('api/feature_flag/local_evaluation?token=TEST_API_KEY')) {
41
+ if ((url as any).includes('api/feature_flag/local_evaluation?token=TEST_API_KEY&send_cohorts')) {
42
42
  return Promise.resolve({
43
43
  status: 200,
44
44
  text: () => Promise.resolve('ok'),
@@ -58,7 +58,7 @@ export const apiImplementation = ({
58
58
  }
59
59
 
60
60
  export const anyLocalEvalCall = [
61
- 'http://example.com/api/feature_flag/local_evaluation?token=TEST_API_KEY',
61
+ 'http://example.com/api/feature_flag/local_evaluation?token=TEST_API_KEY&send_cohorts',
62
62
  expect.any(Object),
63
63
  ]
64
64
  export const anyDecideCall = ['http://example.com/decide/?v=3', expect.any(Object)]
@@ -336,6 +336,7 @@ describe('local evaluation', () => {
336
336
  groups: {},
337
337
  person_properties: { region: 'USA', email: 'a@b.com' },
338
338
  group_properties: {},
339
+ geoip_disable: true,
339
340
  }),
340
341
  })
341
342
  )
@@ -354,6 +355,7 @@ describe('local evaluation', () => {
354
355
  groups: {},
355
356
  person_properties: { doesnt_matter: '1' },
356
357
  group_properties: {},
358
+ geoip_disable: true,
357
359
  }),
358
360
  })
359
361
  )
@@ -1179,6 +1181,177 @@ describe('local evaluation', () => {
1179
1181
  expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
1180
1182
  })
1181
1183
 
1184
+ it('computes complex cohorts locally', async () => {
1185
+ const flags = {
1186
+ flags: [
1187
+ {
1188
+ id: 1,
1189
+ name: 'Beta Feature',
1190
+ key: 'beta-feature',
1191
+ is_simple_flag: false,
1192
+ active: true,
1193
+ rollout_percentage: 100,
1194
+ filters: {
1195
+ groups: [
1196
+ {
1197
+ properties: [
1198
+ {
1199
+ key: 'region',
1200
+ operator: 'exact',
1201
+ value: ['USA'],
1202
+ type: 'person',
1203
+ },
1204
+ { key: 'id', value: 98, type: 'cohort' },
1205
+ ],
1206
+ rollout_percentage: 100,
1207
+ },
1208
+ ],
1209
+ },
1210
+ },
1211
+ ],
1212
+ cohorts: {
1213
+ '98': {
1214
+ type: 'OR',
1215
+ values: [
1216
+ { key: 'id', value: 1, type: 'cohort' },
1217
+ {
1218
+ key: 'nation',
1219
+ operator: 'exact',
1220
+ value: ['UK'],
1221
+ type: 'person',
1222
+ },
1223
+ ],
1224
+ },
1225
+ '1': {
1226
+ type: 'AND',
1227
+ values: [{ key: 'other', operator: 'exact', value: ['thing'], type: 'person' }],
1228
+ },
1229
+ },
1230
+ }
1231
+ mockedFetch.mockImplementation(
1232
+ apiImplementation({
1233
+ localFlags: flags,
1234
+ decideFlags: {},
1235
+ })
1236
+ )
1237
+
1238
+ posthog = new PostHog('TEST_API_KEY', {
1239
+ host: 'http://example.com',
1240
+ personalApiKey: 'TEST_PERSONAL_API_KEY',
1241
+ })
1242
+
1243
+ expect(
1244
+ await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', { personProperties: { region: 'UK' } })
1245
+ ).toEqual(false)
1246
+ expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
1247
+
1248
+ // # even though 'other' property is not present, the cohort should still match since it's an OR condition
1249
+ expect(
1250
+ await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
1251
+ personProperties: { region: 'USA', nation: 'UK' },
1252
+ })
1253
+ ).toEqual(true)
1254
+ expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
1255
+
1256
+ // # even though 'other' property is not present, the cohort should still match since it's an OR condition
1257
+ expect(
1258
+ await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
1259
+ personProperties: { region: 'USA', other: 'thing' },
1260
+ })
1261
+ ).toEqual(true)
1262
+ expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
1263
+ })
1264
+
1265
+ it('computes complex cohorts with negation locally', async () => {
1266
+ const flags = {
1267
+ flags: [
1268
+ {
1269
+ id: 1,
1270
+ name: 'Beta Feature',
1271
+ key: 'beta-feature',
1272
+ is_simple_flag: false,
1273
+ active: true,
1274
+ rollout_percentage: 100,
1275
+ filters: {
1276
+ groups: [
1277
+ {
1278
+ properties: [
1279
+ {
1280
+ key: 'region',
1281
+ operator: 'exact',
1282
+ value: ['USA'],
1283
+ type: 'person',
1284
+ },
1285
+ { key: 'id', value: 98, type: 'cohort' },
1286
+ ],
1287
+ rollout_percentage: 100,
1288
+ },
1289
+ ],
1290
+ },
1291
+ },
1292
+ ],
1293
+ cohorts: {
1294
+ '98': {
1295
+ type: 'OR',
1296
+ values: [
1297
+ { key: 'id', value: 1, type: 'cohort' },
1298
+ {
1299
+ key: 'nation',
1300
+ operator: 'exact',
1301
+ value: ['UK'],
1302
+ type: 'person',
1303
+ },
1304
+ ],
1305
+ },
1306
+ '1': {
1307
+ type: 'AND',
1308
+ values: [{ key: 'other', operator: 'exact', value: ['thing'], type: 'person', negation: true }],
1309
+ },
1310
+ },
1311
+ }
1312
+ mockedFetch.mockImplementation(
1313
+ apiImplementation({
1314
+ localFlags: flags,
1315
+ decideFlags: {},
1316
+ })
1317
+ )
1318
+
1319
+ posthog = new PostHog('TEST_API_KEY', {
1320
+ host: 'http://example.com',
1321
+ personalApiKey: 'TEST_PERSONAL_API_KEY',
1322
+ })
1323
+
1324
+ expect(
1325
+ await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', { personProperties: { region: 'UK' } })
1326
+ ).toEqual(false)
1327
+ expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
1328
+
1329
+ // # even though 'other' property is not present, the cohort should still match since it's an OR condition
1330
+ expect(
1331
+ await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
1332
+ personProperties: { region: 'USA', nation: 'UK' },
1333
+ })
1334
+ ).toEqual(true)
1335
+ expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
1336
+
1337
+ // # since 'other' is negated, we return False. Since 'nation' is not present, we can't tell whether the flag should be true or false, so go to decide
1338
+ expect(
1339
+ await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
1340
+ personProperties: { region: 'USA', other: 'thing' },
1341
+ })
1342
+ ).toEqual(false)
1343
+ expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
1344
+
1345
+ mockedFetch.mockClear()
1346
+
1347
+ expect(
1348
+ await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
1349
+ personProperties: { region: 'USA', other: 'thing2' },
1350
+ })
1351
+ ).toEqual(true)
1352
+ expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
1353
+ })
1354
+
1182
1355
  it('gets feature flag with variant overrides', async () => {
1183
1356
  const flags = {
1184
1357
  flags: [
@@ -12,7 +12,8 @@ const mockedFetch = jest.mocked(fetch, true)
12
12
  const getLastBatchEvents = (): any[] | undefined => {
13
13
  expect(mockedFetch).toHaveBeenCalledWith('http://example.com/batch/', expect.objectContaining({ method: 'POST' }))
14
14
 
15
- const call = mockedFetch.mock.calls.find((x) => (x[0] as string).includes('/batch/'))
15
+ // reverse mock calls array to get the last call
16
+ const call = mockedFetch.mock.calls.reverse().find((x) => (x[0] as string).includes('/batch/'))
16
17
  if (!call) {
17
18
  return undefined
18
19
  }
@@ -51,14 +52,21 @@ describe('PostHog Node.js', () => {
51
52
 
52
53
  jest.runOnlyPendingTimers()
53
54
  const batchEvents = getLastBatchEvents()
54
- expect(batchEvents).toMatchObject([
55
+ expect(batchEvents).toEqual([
55
56
  {
56
57
  distinct_id: '123',
57
58
  event: 'test-event',
58
59
  properties: {
59
60
  $groups: { org: 123 },
60
61
  foo: 'bar',
62
+ $geoip_disable: true,
63
+ $lib: 'posthog-node',
64
+ $lib_version: '1.2.3',
61
65
  },
66
+ timestamp: expect.any(String),
67
+ type: 'capture',
68
+ library: 'posthog-node',
69
+ library_version: '1.2.3',
62
70
  },
63
71
  ])
64
72
  })
@@ -97,6 +105,7 @@ describe('PostHog Node.js', () => {
97
105
  properties: expect.objectContaining({
98
106
  $groups: { other_group: 'x' },
99
107
  foo: 'bar',
108
+ $geoip_disable: true,
100
109
  }),
101
110
  library: 'posthog-node',
102
111
  library_version: '1.2.3',
@@ -117,6 +126,7 @@ describe('PostHog Node.js', () => {
117
126
  $set: {
118
127
  foo: 'bar',
119
128
  },
129
+ $geoip_disable: true,
120
130
  },
121
131
  },
122
132
  ])
@@ -135,6 +145,7 @@ describe('PostHog Node.js', () => {
135
145
  $set: {
136
146
  foo: 'other',
137
147
  },
148
+ $geoip_disable: true,
138
149
  },
139
150
  },
140
151
  ])
@@ -152,6 +163,7 @@ describe('PostHog Node.js', () => {
152
163
  properties: {
153
164
  distinct_id: '123',
154
165
  alias: '1234',
166
+ $geoip_disable: true,
155
167
  },
156
168
  },
157
169
  ])
@@ -170,6 +182,83 @@ describe('PostHog Node.js', () => {
170
182
  },
171
183
  ])
172
184
  })
185
+
186
+ it('should respect disableGeoip setting if passed in', async () => {
187
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
188
+ posthog.capture({
189
+ distinctId: '123',
190
+ event: 'test-event',
191
+ properties: { foo: 'bar' },
192
+ groups: { org: 123 },
193
+ disableGeoip: false,
194
+ })
195
+
196
+ jest.runOnlyPendingTimers()
197
+ const batchEvents = getLastBatchEvents()
198
+ expect(batchEvents?.[0].properties).toEqual({
199
+ $groups: { org: 123 },
200
+ foo: 'bar',
201
+ $lib: 'posthog-node',
202
+ $lib_version: '1.2.3',
203
+ })
204
+ })
205
+
206
+ it('should use default is set, and override on specific disableGeoip calls', async () => {
207
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
208
+ const client = new PostHog('TEST_API_KEY', {
209
+ host: 'http://example.com',
210
+ disableGeoip: false,
211
+ })
212
+ client.debug()
213
+ client.capture({ distinctId: '123', event: 'test-event', properties: { foo: 'bar' }, groups: { org: 123 } })
214
+
215
+ jest.runOnlyPendingTimers()
216
+ let batchEvents = getLastBatchEvents()
217
+ expect(batchEvents?.[0].properties).toEqual({
218
+ $groups: { org: 123 },
219
+ foo: 'bar',
220
+ $lib: 'posthog-node',
221
+ $lib_version: '1.2.3',
222
+ })
223
+
224
+ client.capture({
225
+ distinctId: '123',
226
+ event: 'test-event',
227
+ properties: { foo: 'bar' },
228
+ groups: { org: 123 },
229
+ disableGeoip: true,
230
+ })
231
+
232
+ jest.runOnlyPendingTimers()
233
+ batchEvents = getLastBatchEvents()
234
+ console.warn(batchEvents)
235
+ expect(batchEvents?.[0].properties).toEqual({
236
+ $groups: { org: 123 },
237
+ foo: 'bar',
238
+ $lib: 'posthog-node',
239
+ $lib_version: '1.2.3',
240
+ $geoip_disable: true,
241
+ })
242
+
243
+ client.capture({
244
+ distinctId: '123',
245
+ event: 'test-event',
246
+ properties: { foo: 'bar' },
247
+ groups: { org: 123 },
248
+ disableGeoip: false,
249
+ })
250
+
251
+ jest.runOnlyPendingTimers()
252
+ batchEvents = getLastBatchEvents()
253
+ expect(batchEvents?.[0].properties).toEqual({
254
+ $groups: { org: 123 },
255
+ foo: 'bar',
256
+ $lib: 'posthog-node',
257
+ $lib_version: '1.2.3',
258
+ })
259
+
260
+ await client.shutdownAsync()
261
+ })
173
262
  })
174
263
 
175
264
  describe('shutdown', () => {
@@ -243,6 +332,7 @@ describe('PostHog Node.js', () => {
243
332
  $group_key: 'team-1',
244
333
  $group_set: { analytics: true },
245
334
  $lib: 'posthog-node',
335
+ $geoip_disable: true,
246
336
  },
247
337
  },
248
338
  ])
@@ -266,6 +356,7 @@ describe('PostHog Node.js', () => {
266
356
  $group_key: 'team-1',
267
357
  $group_set: { analytics: true },
268
358
  $lib: 'posthog-node',
359
+ $geoip_disable: true,
269
360
  },
270
361
  },
271
362
  ])
@@ -292,7 +383,6 @@ describe('PostHog Node.js', () => {
292
383
 
293
384
  posthog = new PostHog('TEST_API_KEY', {
294
385
  host: 'http://example.com',
295
- // flushAt: 1,
296
386
  })
297
387
  })
298
388
 
@@ -302,6 +392,10 @@ describe('PostHog Node.js', () => {
302
392
  'variant'
303
393
  )
304
394
  expect(mockedFetch).toHaveBeenCalledTimes(1)
395
+ expect(mockedFetch).toHaveBeenCalledWith(
396
+ 'http://example.com/decide/?v=3',
397
+ expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
398
+ )
305
399
  })
306
400
 
307
401
  it('should do isFeatureEnabled', async () => {
@@ -347,12 +441,57 @@ describe('PostHog Node.js', () => {
347
441
  '$feature/feature-variant': 'variant',
348
442
  $lib: 'posthog-node',
349
443
  $lib_version: '1.2.3',
444
+ $geoip_disable: true,
350
445
  }),
351
446
  })
352
447
  )
353
448
 
354
449
  // no calls to `/local_evaluation`
355
450
 
451
+ expect(mockedFetch).not.toHaveBeenCalledWith(...anyLocalEvalCall)
452
+ expect(mockedFetch).toHaveBeenCalledWith(
453
+ 'http://example.com/decide/?v=3',
454
+ expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
455
+ )
456
+ })
457
+
458
+ it('captures feature flags with same geoip setting as capture', async () => {
459
+ mockedFetch.mockClear()
460
+ mockedFetch.mockClear()
461
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
462
+
463
+ posthog = new PostHog('TEST_API_KEY', {
464
+ host: 'http://example.com',
465
+ flushAt: 1,
466
+ })
467
+
468
+ posthog.capture({
469
+ distinctId: 'distinct_id',
470
+ event: 'node test event',
471
+ sendFeatureFlags: true,
472
+ disableGeoip: false,
473
+ })
474
+
475
+ expect(mockedFetch).toHaveBeenCalledWith(
476
+ 'http://example.com/decide/?v=3',
477
+ expect.objectContaining({ method: 'POST', body: expect.not.stringContaining('geoip_disable') })
478
+ )
479
+
480
+ jest.runOnlyPendingTimers()
481
+
482
+ await waitForPromises()
483
+
484
+ expect(getLastBatchEvents()?.[0].properties).toEqual({
485
+ $active_feature_flags: ['feature-1', 'feature-2', 'feature-variant'],
486
+ '$feature/feature-1': true,
487
+ '$feature/feature-2': true,
488
+ '$feature/feature-variant': 'variant',
489
+ $lib: 'posthog-node',
490
+ $lib_version: '1.2.3',
491
+ })
492
+
493
+ // no calls to `/local_evaluation`
494
+
356
495
  expect(mockedFetch).not.toHaveBeenCalledWith(...anyLocalEvalCall)
357
496
  })
358
497
 
@@ -462,6 +601,7 @@ describe('PostHog Node.js', () => {
462
601
  $lib: 'posthog-node',
463
602
  $lib_version: '1.2.3',
464
603
  locally_evaluated: true,
604
+ $geoip_disable: true,
465
605
  }),
466
606
  })
467
607
  )
@@ -482,6 +622,7 @@ describe('PostHog Node.js', () => {
482
622
  await posthog.getFeatureFlag('beta-feature', 'some-distinct-id2', {
483
623
  groups: { x: 'y' },
484
624
  personProperties: { region: 'USA', name: 'Aloha' },
625
+ disableGeoip: false,
485
626
  })
486
627
  ).toEqual(true)
487
628
  jest.runOnlyPendingTimers()
@@ -491,16 +632,16 @@ describe('PostHog Node.js', () => {
491
632
  expect.objectContaining({
492
633
  distinct_id: 'some-distinct-id2',
493
634
  event: '$feature_flag_called',
494
- properties: expect.objectContaining({
495
- $feature_flag: 'beta-feature',
496
- $feature_flag_response: true,
497
- $lib: 'posthog-node',
498
- $lib_version: '1.2.3',
499
- locally_evaluated: true,
500
- $groups: { x: 'y' },
501
- }),
502
635
  })
503
636
  )
637
+ expect(getLastBatchEvents()?.[0].properties).toEqual({
638
+ $feature_flag: 'beta-feature',
639
+ $feature_flag_response: true,
640
+ $lib: 'posthog-node',
641
+ $lib_version: '1.2.3',
642
+ locally_evaluated: true,
643
+ $groups: { x: 'y' },
644
+ })
504
645
  mockedFetch.mockClear()
505
646
 
506
647
  // # called for different user, but send configuration is false, so should NOT call capture again
@@ -559,6 +700,10 @@ describe('PostHog Node.js', () => {
559
700
  posthog.getFeatureFlagPayload('feature-variant', '123', 'variant', { groups: { org: '123' } })
560
701
  ).resolves.toEqual(2)
561
702
  expect(mockedFetch).toHaveBeenCalledTimes(1)
703
+ expect(mockedFetch).toHaveBeenCalledWith(
704
+ 'http://example.com/decide/?v=3',
705
+ expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
706
+ )
562
707
  })
563
708
 
564
709
  it('should do getFeatureFlagPayloads without matchValue', async () => {
@@ -568,5 +713,26 @@ describe('PostHog Node.js', () => {
568
713
  ).resolves.toEqual(2)
569
714
  expect(mockedFetch).toHaveBeenCalledTimes(1)
570
715
  })
716
+
717
+ it('should do getFeatureFlags with geoip disabled and enabled', async () => {
718
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
719
+ await expect(
720
+ posthog.getFeatureFlagPayload('feature-variant', '123', 'variant', { groups: { org: '123' } })
721
+ ).resolves.toEqual(2)
722
+ expect(mockedFetch).toHaveBeenCalledTimes(1)
723
+ expect(mockedFetch).toHaveBeenCalledWith(
724
+ 'http://example.com/decide/?v=3',
725
+ expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
726
+ )
727
+
728
+ mockedFetch.mockClear()
729
+
730
+ await expect(posthog.isFeatureEnabled('feature-variant', '123', { disableGeoip: false })).resolves.toEqual(true)
731
+ expect(mockedFetch).toHaveBeenCalledTimes(1)
732
+ expect(mockedFetch).toHaveBeenCalledWith(
733
+ 'http://example.com/decide/?v=3',
734
+ expect.objectContaining({ method: 'POST', body: expect.not.stringContaining('geoip_disable') })
735
+ )
736
+ })
571
737
  })
572
738
  })