posthog-node 4.0.0 → 4.0.1

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.
@@ -37,7 +37,7 @@ export declare type PostHogCoreOptions = {
37
37
  featureFlagsRequestTimeoutMs?: number;
38
38
  /** For Session Analysis how long before we expire a session (defaults to 30 mins) */
39
39
  sessionExpirationTimeSeconds?: number;
40
- /** Whether to post events to PostHog in JSON or compressed format */
40
+ /** Whether to post events to PostHog in JSON or compressed format. Defaults to 'form' */
41
41
  captureMode?: 'json' | 'form';
42
42
  disableGeoip?: boolean;
43
43
  };
@@ -47,7 +47,7 @@ export declare type PostHogFeatureFlag = {
47
47
  rollout_percentage: number;
48
48
  }[];
49
49
  };
50
- payloads?: Record<string, JsonType>;
50
+ payloads?: Record<string, string>;
51
51
  };
52
52
  deleted: boolean;
53
53
  active: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -148,11 +148,15 @@ class FeatureFlagsPoller {
148
148
  }
149
149
 
150
150
  // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
151
- if (response === undefined) {
151
+ if (response === undefined || response === null) {
152
152
  return null
153
153
  }
154
154
 
155
- return response
155
+ try {
156
+ return JSON.parse(response)
157
+ } catch {
158
+ return response
159
+ }
156
160
  }
157
161
 
158
162
  async getAllFlagsAndPayloads(
@@ -308,7 +312,7 @@ class FeatureFlagsPoller {
308
312
  let matches = false
309
313
 
310
314
  if (propertyType === 'cohort') {
311
- matches = matchCohort(prop, properties, this.cohorts)
315
+ matches = matchCohort(prop, properties, this.cohorts, this.debugMode)
312
316
  } else {
313
317
  matches = matchProperty(prop, properties)
314
318
  }
@@ -558,7 +562,8 @@ function matchProperty(
558
562
  function matchCohort(
559
563
  property: FeatureFlagCondition['properties'][number],
560
564
  propertyValues: Record<string, any>,
561
- cohortProperties: FeatureFlagsPoller['cohorts']
565
+ cohortProperties: FeatureFlagsPoller['cohorts'],
566
+ debugMode: boolean = false
562
567
  ): boolean {
563
568
  const cohortId = String(property.value)
564
569
  if (!(cohortId in cohortProperties)) {
@@ -566,13 +571,14 @@ function matchCohort(
566
571
  }
567
572
 
568
573
  const propertyGroup = cohortProperties[cohortId]
569
- return matchPropertyGroup(propertyGroup, propertyValues, cohortProperties)
574
+ return matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, debugMode)
570
575
  }
571
576
 
572
577
  function matchPropertyGroup(
573
578
  propertyGroup: PropertyGroup,
574
579
  propertyValues: Record<string, any>,
575
- cohortProperties: FeatureFlagsPoller['cohorts']
580
+ cohortProperties: FeatureFlagsPoller['cohorts'],
581
+ debugMode: boolean = false
576
582
  ): boolean {
577
583
  if (!propertyGroup) {
578
584
  return true
@@ -592,7 +598,7 @@ function matchPropertyGroup(
592
598
  // a nested property group
593
599
  for (const prop of properties as PropertyGroup[]) {
594
600
  try {
595
- const matches = matchPropertyGroup(prop, propertyValues, cohortProperties)
601
+ const matches = matchPropertyGroup(prop, propertyValues, cohortProperties, debugMode)
596
602
  if (propertyGroupType === 'AND') {
597
603
  if (!matches) {
598
604
  return false
@@ -605,7 +611,9 @@ function matchPropertyGroup(
605
611
  }
606
612
  } catch (err) {
607
613
  if (err instanceof InconclusiveMatchError) {
608
- console.debug(`Failed to compute property ${prop} locally: ${err}`)
614
+ if (debugMode) {
615
+ console.debug(`Failed to compute property ${prop} locally: ${err}`)
616
+ }
609
617
  errorMatchingLocally = true
610
618
  } else {
611
619
  throw err
@@ -623,7 +631,7 @@ function matchPropertyGroup(
623
631
  try {
624
632
  let matches: boolean
625
633
  if (prop.type === 'cohort') {
626
- matches = matchCohort(prop, propertyValues, cohortProperties)
634
+ matches = matchCohort(prop, propertyValues, cohortProperties, debugMode)
627
635
  } else {
628
636
  matches = matchProperty(prop, propertyValues)
629
637
  }
@@ -649,7 +657,9 @@ function matchPropertyGroup(
649
657
  }
650
658
  } catch (err) {
651
659
  if (err instanceof InconclusiveMatchError) {
652
- console.debug(`Failed to compute property ${prop} locally: ${err}`)
660
+ if (debugMode) {
661
+ console.debug(`Failed to compute property ${prop} locally: ${err}`)
662
+ }
653
663
  errorMatchingLocally = true
654
664
  } else {
655
665
  throw err
@@ -340,12 +340,7 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
340
340
  disableGeoip
341
341
  )
342
342
  }
343
-
344
- try {
345
- return JSON.parse(response as any)
346
- } catch {
347
- return response
348
- }
343
+ return response
349
344
  }
350
345
 
351
346
  async isFeatureEnabled(
package/src/types.ts CHANGED
@@ -54,7 +54,7 @@ export type PostHogFeatureFlag = {
54
54
  rollout_percentage: number
55
55
  }[]
56
56
  }
57
- payloads?: Record<string, JsonType>
57
+ payloads?: Record<string, string>
58
58
  }
59
59
  deleted: boolean
60
60
  active: boolean
@@ -466,11 +466,14 @@ describe('PostHog Node.js', () => {
466
466
  'feature-2': true,
467
467
  'feature-variant': 'variant',
468
468
  'disabled-flag': false,
469
+ 'feature-array': true,
469
470
  }
470
471
 
472
+ // these are stringified in apiImplementation
471
473
  const mockFeatureFlagPayloads = {
472
474
  'feature-1': { color: 'blue' },
473
475
  'feature-variant': 2,
476
+ 'feature-array': [1],
474
477
  }
475
478
 
476
479
  const multivariateFlag = {
@@ -497,7 +500,7 @@ describe('PostHog Node.js', () => {
497
500
  { key: 'third-variant', name: 'Third Variant', rollout_percentage: 25 },
498
501
  ],
499
502
  },
500
- payloads: { 'first-variant': 'some-payload', 'third-variant': { a: 'json' } },
503
+ payloads: { 'first-variant': 'some-payload', 'third-variant': JSON.stringify({ a: 'json' }) },
501
504
  },
502
505
  }
503
506
  const basicFlag = {
@@ -520,7 +523,7 @@ describe('PostHog Node.js', () => {
520
523
  rollout_percentage: 100,
521
524
  },
522
525
  ],
523
- payloads: { true: 300 },
526
+ payloads: { true: '300' },
524
527
  },
525
528
  }
526
529
  const falseFlag = {
@@ -536,7 +539,23 @@ describe('PostHog Node.js', () => {
536
539
  rollout_percentage: 0,
537
540
  },
538
541
  ],
539
- payloads: { true: 300 },
542
+ payloads: { true: '300' },
543
+ },
544
+ }
545
+
546
+ const arrayFlag = {
547
+ id: 5,
548
+ name: 'Beta Feature',
549
+ key: 'feature-array',
550
+ active: true,
551
+ filters: {
552
+ groups: [
553
+ {
554
+ properties: [],
555
+ rollout_percentage: 100,
556
+ },
557
+ ],
558
+ payloads: { true: JSON.stringify([1]) },
540
559
  },
541
560
  }
542
561
 
@@ -544,7 +563,7 @@ describe('PostHog Node.js', () => {
544
563
  apiImplementation({
545
564
  decideFlags: mockFeatureFlags,
546
565
  decideFlagPayloads: mockFeatureFlagPayloads,
547
- localFlags: { flags: [multivariateFlag, basicFlag, falseFlag] },
566
+ localFlags: { flags: [multivariateFlag, basicFlag, falseFlag, arrayFlag] },
548
567
  })
549
568
  )
550
569
 
@@ -603,9 +622,10 @@ describe('PostHog Node.js', () => {
603
622
  distinct_id: 'distinct_id',
604
623
  event: 'node test event',
605
624
  properties: expect.objectContaining({
606
- $active_feature_flags: ['feature-1', 'feature-2', 'feature-variant'],
625
+ $active_feature_flags: ['feature-1', 'feature-2', 'feature-variant', 'feature-array'],
607
626
  '$feature/feature-1': true,
608
627
  '$feature/feature-2': true,
628
+ '$feature/feature-array': true,
609
629
  '$feature/feature-variant': 'variant',
610
630
  $lib: 'posthog-node',
611
631
  $lib_version: '1.2.3',
@@ -659,8 +679,9 @@ describe('PostHog Node.js', () => {
659
679
  distinct_id: 'distinct_id',
660
680
  event: 'node test event',
661
681
  properties: expect.objectContaining({
662
- $active_feature_flags: ['beta-feature-local'],
682
+ $active_feature_flags: ['beta-feature-local', 'feature-array'],
663
683
  '$feature/beta-feature-local': 'third-variant',
684
+ '$feature/feature-array': true,
664
685
  '$feature/false-flag': false,
665
686
  $lib: 'posthog-node',
666
687
  $lib_version: '1.2.3',
@@ -756,9 +777,10 @@ describe('PostHog Node.js', () => {
756
777
  )
757
778
 
758
779
  expect(getLastBatchEvents()?.[0].properties).toEqual({
759
- $active_feature_flags: ['feature-1', 'feature-2', 'feature-variant'],
780
+ $active_feature_flags: ['feature-1', 'feature-2', 'feature-variant', 'feature-array'],
760
781
  '$feature/feature-1': true,
761
782
  '$feature/feature-2': true,
783
+ '$feature/feature-array': true,
762
784
  '$feature/disabled-flag': false,
763
785
  '$feature/feature-variant': 'variant',
764
786
  $lib: 'posthog-node',
@@ -1008,6 +1030,46 @@ describe('PostHog Node.js', () => {
1008
1030
  )
1009
1031
  })
1010
1032
 
1033
+ it('should not double parse json with getFeatureFlagPayloads and local eval', async () => {
1034
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
1035
+
1036
+ posthog = new PostHog('TEST_API_KEY', {
1037
+ host: 'http://example.com',
1038
+ flushAt: 1,
1039
+ fetchRetryCount: 0,
1040
+ personalApiKey: 'TEST_PERSONAL_API_KEY',
1041
+ })
1042
+
1043
+ mockedFetch.mockClear()
1044
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
1045
+
1046
+ await expect(
1047
+ posthog.getFeatureFlagPayload('feature-array', '123', true, { onlyEvaluateLocally: true })
1048
+ ).resolves.toEqual([1])
1049
+ expect(mockedFetch).toHaveBeenCalledTimes(1)
1050
+ expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
1051
+
1052
+ mockedFetch.mockClear()
1053
+
1054
+ await expect(posthog.getFeatureFlagPayload('feature-array', '123')).resolves.toEqual([1])
1055
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
1056
+
1057
+ await expect(posthog.getFeatureFlagPayload('false-flag', '123', true)).resolves.toEqual(300)
1058
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
1059
+ })
1060
+
1061
+ it('should not double parse json with getFeatureFlagPayloads and server eval', async () => {
1062
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
1063
+ await expect(
1064
+ posthog.getFeatureFlagPayload('feature-array', '123', undefined, { groups: { org: '123' } })
1065
+ ).resolves.toEqual([1])
1066
+ expect(mockedFetch).toHaveBeenCalledTimes(1)
1067
+ expect(mockedFetch).toHaveBeenCalledWith(
1068
+ 'http://example.com/decide/?v=3',
1069
+ expect.objectContaining({ method: 'POST', body: expect.stringContaining('"geoip_disable":true') })
1070
+ )
1071
+ })
1072
+
1011
1073
  it('should do getFeatureFlagPayloads without matchValue', async () => {
1012
1074
  expect(mockedFetch).toHaveBeenCalledTimes(0)
1013
1075
  await expect(
@@ -20,7 +20,9 @@ export const apiImplementation = ({
20
20
  } else {
21
21
  return Promise.resolve({
22
22
  featureFlags: decideFlags,
23
- featureFlagPayloads: decideFlagPayloads,
23
+ featureFlagPayloads: Object.fromEntries(
24
+ Object.entries(decideFlagPayloads || {}).map(([k, v]) => [k, JSON.stringify(v)])
25
+ ),
24
26
  })
25
27
  }
26
28
  },