posthog-node 4.5.1 → 4.6.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.
@@ -58,8 +58,10 @@ declare class FeatureFlagsPoller {
58
58
  }[];
59
59
  loadFeatureFlags(forceReload?: boolean): Promise<void>;
60
60
  _loadFeatureFlags(): Promise<void>;
61
+ private getPersonalApiKeyRequestOptions;
61
62
  _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse>;
62
63
  stopPoller(): void;
64
+ _requestRemoteConfigPayload(flagKey: string): Promise<PostHogFetchResponse>;
63
65
  }
64
66
  declare function matchProperty(property: FeatureFlagCondition['properties'][number], propertyValues: Record<string, any>, warnFunction?: (msg: string) => void): boolean;
65
67
  declare function relativeDateParseForFeatureFlagMatching(value: string): Date | null;
@@ -50,6 +50,7 @@ export declare class PostHog extends PostHogCoreStateless implements PostHogNode
50
50
  sendFeatureFlagEvents?: boolean;
51
51
  disableGeoip?: boolean;
52
52
  }): Promise<JsonType | undefined>;
53
+ getRemoteConfigPayload(flagKey: string): Promise<JsonType | undefined>;
53
54
  isFeatureEnabled(key: string, distinctId: string, options?: {
54
55
  groups?: Record<string, string>;
55
56
  personProperties?: Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "4.5.1",
3
+ "version": "4.6.0",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -419,17 +419,21 @@ class FeatureFlagsPoller {
419
419
  }
420
420
  }
421
421
 
422
- async _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse> {
423
- const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`
424
-
425
- const options: PostHogFetchOptions = {
426
- method: 'GET',
422
+ private getPersonalApiKeyRequestOptions(method: 'GET' | 'POST' | 'PUT' | 'PATCH' = 'GET'): PostHogFetchOptions {
423
+ return {
424
+ method,
427
425
  headers: {
428
426
  ...this.customHeaders,
429
427
  'Content-Type': 'application/json',
430
428
  Authorization: `Bearer ${this.personalApiKey}`,
431
429
  },
432
430
  }
431
+ }
432
+
433
+ async _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse> {
434
+ const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`
435
+
436
+ const options = this.getPersonalApiKeyRequestOptions()
433
437
 
434
438
  let abortTimeout = null
435
439
 
@@ -451,6 +455,26 @@ class FeatureFlagsPoller {
451
455
  stopPoller(): void {
452
456
  clearTimeout(this.poller)
453
457
  }
458
+
459
+ _requestRemoteConfigPayload(flagKey: string): Promise<PostHogFetchResponse> {
460
+ const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config/`
461
+
462
+ const options = this.getPersonalApiKeyRequestOptions()
463
+
464
+ let abortTimeout = null
465
+ if (this.timeout && typeof this.timeout === 'number') {
466
+ const controller = new AbortController()
467
+ abortTimeout = safeSetTimeout(() => {
468
+ controller.abort()
469
+ }, this.timeout)
470
+ options.signal = controller.signal
471
+ }
472
+ try {
473
+ return this.fetch(url, options)
474
+ } finally {
475
+ clearTimeout(abortTimeout)
476
+ }
477
+ }
454
478
  }
455
479
 
456
480
  // # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
@@ -135,8 +135,13 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
135
135
  return await _getFlags(distinctId, groups, disableGeoip)
136
136
  }
137
137
 
138
+ if (event === '$feature_flag_called') {
139
+ // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
140
+ return {}
141
+ }
142
+
138
143
  if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
139
- // Otherwise we may as well check for the flags locally and include them if there
144
+ // Otherwise we may as well check for the flags locally and include them if they are already loaded
140
145
  const groupsWithStringValues: Record<string, string> = {}
141
146
  for (const [key, value] of Object.entries(groups || {})) {
142
147
  groupsWithStringValues[key] = String(value)
@@ -354,6 +359,10 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
354
359
  return response
355
360
  }
356
361
 
362
+ async getRemoteConfigPayload(flagKey: string): Promise<JsonType | undefined> {
363
+ return (await this.featureFlagsPoller?._requestRemoteConfigPayload(flagKey))?.json()
364
+ }
365
+
357
366
  async isFeatureEnabled(
358
367
  key: string,
359
368
  distinctId: string,
@@ -5,6 +5,7 @@ import { matchProperty, InconclusiveMatchError, relativeDateParseForFeatureFlagM
5
5
  jest.mock('../src/fetch')
6
6
  import fetch from '../src/fetch'
7
7
  import { anyDecideCall, anyLocalEvalCall, apiImplementation } from './test-utils'
8
+ import { waitForPromises } from 'posthog-core/test/test-utils/test-utils'
8
9
 
9
10
  jest.spyOn(console, 'debug').mockImplementation()
10
11
 
@@ -1796,6 +1797,84 @@ describe('local evaluation', () => {
1796
1797
  })
1797
1798
  })
1798
1799
 
1800
+ describe('getFeatureFlag', () => {
1801
+ it('should capture $feature_flag_called when called, but not add all cached flags', async () => {
1802
+ const flags = {
1803
+ flags: [
1804
+ {
1805
+ id: 1,
1806
+ name: 'Beta Feature',
1807
+ key: 'complex-flag',
1808
+ active: true,
1809
+ filters: {
1810
+ groups: [
1811
+ {
1812
+ variant: null,
1813
+ properties: [{ key: 'region', type: 'person', value: 'USA', operator: 'exact' }],
1814
+ rollout_percentage: 100,
1815
+ },
1816
+ ],
1817
+ },
1818
+ },
1819
+ {
1820
+ id: 2,
1821
+ name: 'Gamma Feature',
1822
+ key: 'simple-flag',
1823
+ active: true,
1824
+ filters: {
1825
+ groups: [
1826
+ {
1827
+ variant: null,
1828
+ properties: [],
1829
+ rollout_percentage: 100,
1830
+ },
1831
+ ],
1832
+ },
1833
+ },
1834
+ ],
1835
+ }
1836
+ mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
1837
+ const posthog = new PostHog('TEST_API_KEY', {
1838
+ host: 'http://example.com',
1839
+ personalApiKey: 'TEST_PERSONAL_API_KEY',
1840
+ ...posthogImmediateResolveOptions,
1841
+ })
1842
+ let capturedMessage: any
1843
+ posthog.on('capture', (message) => {
1844
+ capturedMessage = message
1845
+ })
1846
+
1847
+ expect(
1848
+ await posthog.getFeatureFlag('complex-flag', 'some-distinct-id', {
1849
+ personProperties: {
1850
+ region: 'USA',
1851
+ } as unknown as Record<string, string>,
1852
+ })
1853
+ ).toEqual(true)
1854
+
1855
+ await waitForPromises()
1856
+
1857
+ expect(capturedMessage).toMatchObject({
1858
+ distinct_id: 'some-distinct-id',
1859
+ event: '$feature_flag_called',
1860
+ library: posthog.getLibraryId(),
1861
+ library_version: posthog.getLibraryVersion(),
1862
+ properties: {
1863
+ '$feature/complex-flag': true,
1864
+ $feature_flag: 'complex-flag',
1865
+ $feature_flag_response: true,
1866
+ $groups: undefined,
1867
+ $lib: posthog.getLibraryId(),
1868
+ $lib_version: posthog.getLibraryVersion(),
1869
+ locally_evaluated: true,
1870
+ },
1871
+ })
1872
+
1873
+ expect(capturedMessage.properties).not.toHaveProperty('$active_feature_flags')
1874
+ expect(capturedMessage.properties).not.toHaveProperty('$feature/simple-flag')
1875
+ })
1876
+ })
1877
+
1799
1878
  describe('match properties', () => {
1800
1879
  jest.useFakeTimers()
1801
1880
 
@@ -1078,7 +1078,9 @@ describe('PostHog Node.js', () => {
1078
1078
  expect(mockedFetch).toHaveBeenCalledTimes(0)
1079
1079
 
1080
1080
  await expect(posthog.getFeatureFlagPayload('false-flag', '123', true)).resolves.toEqual(300)
1081
- expect(mockedFetch).toHaveBeenCalledTimes(0)
1081
+ // Check no non-batch API calls were made
1082
+ const additionalNonBatchCalls = mockedFetch.mock.calls.filter((call) => !call[0].includes('/batch'))
1083
+ expect(additionalNonBatchCalls.length).toBe(0)
1082
1084
  })
1083
1085
 
1084
1086
  it('should not double parse json with getFeatureFlagPayloads and server eval', async () => {