posthog-node 4.6.0 → 4.8.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.
@@ -186,9 +186,22 @@ export declare abstract class PostHogCore extends PostHogCoreStateless {
186
186
  /***
187
187
  *** ERROR TRACKING
188
188
  ***/
189
- captureException(error: Error, additionalProperties?: {
189
+ captureException(error: unknown, additionalProperties?: {
190
190
  [key: string]: any;
191
191
  }): void;
192
+ /**
193
+ * Capture written user feedback for a LLM trace. Numeric values are converted to strings.
194
+ * @param traceId The trace ID to capture feedback for.
195
+ * @param userFeedback The feedback to capture.
196
+ */
197
+ captureTraceFeedback(traceId: string | number, userFeedback: string): void;
198
+ /**
199
+ * Capture a metric for a LLM trace. Numeric values are converted to strings.
200
+ * @param traceId The trace ID to capture the metric for.
201
+ * @param metricName The name of the metric to capture.
202
+ * @param metricValue The value of the metric to capture.
203
+ */
204
+ captureTraceMetric(traceId: string | number, metricName: string, metricValue: string | number | boolean): void;
192
205
  }
193
206
  export * from './types';
194
207
  export { LZString };
@@ -11,4 +11,5 @@ export declare function currentTimestamp(): number;
11
11
  export declare function currentISOTime(): string;
12
12
  export declare function safeSetTimeout(fn: () => void, timeout: number): any;
13
13
  export declare const isPromise: (obj: any) => obj is Promise<any>;
14
+ export declare const isError: (x: unknown) => x is Error;
14
15
  export declare function getFetch(): FetchLike | undefined;
@@ -37,6 +37,8 @@ declare class FeatureFlagsPoller {
37
37
  customHeaders?: {
38
38
  [key: string]: string;
39
39
  };
40
+ lastRequestWasAuthenticationError: boolean;
41
+ authenticationErrorCount: number;
40
42
  constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host, customHeaders, ...options }: FeatureFlagsPollerOptions);
41
43
  debug(enabled?: boolean): void;
42
44
  private logMsgIfDebug;
@@ -57,6 +59,13 @@ declare class FeatureFlagsPoller {
57
59
  key: string;
58
60
  }[];
59
61
  loadFeatureFlags(forceReload?: boolean): Promise<void>;
62
+ /**
63
+ * If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
64
+ * until a successful request is made, up to a maximum of 60 seconds.
65
+ *
66
+ * @returns The polling interval to use for the next request.
67
+ */
68
+ private getPollingInterval;
60
69
  _loadFeatureFlags(): Promise<void>;
61
70
  private getPersonalApiKeyRequestOptions;
62
71
  _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse>;
@@ -10,6 +10,9 @@ export type PostHogOptions = PostHogCoreOptions & {
10
10
  maxCacheSize?: number;
11
11
  fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
12
12
  };
13
+ export declare const MINIMUM_POLLING_INTERVAL = 100;
14
+ export declare const THIRTY_SECONDS: number;
15
+ export declare const SIXTY_SECONDS: number;
13
16
  export declare class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
14
17
  private _memoryStorage;
15
18
  private featureFlagsPoller?;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "4.6.0",
3
+ "version": "4.8.0",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,6 +3,7 @@ import { FeatureFlagCondition, FlagProperty, PostHogFeatureFlag, PropertyGroup }
3
3
  import { JsonType, PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
4
4
  import { safeSetTimeout } from 'posthog-core/src/utils'
5
5
  import fetch from './fetch'
6
+ import { SIXTY_SECONDS } from './posthog-node'
6
7
 
7
8
  // eslint-disable-next-line
8
9
  const LONG_SCALE = 0xfffffffffffffff
@@ -57,6 +58,8 @@ class FeatureFlagsPoller {
57
58
  debugMode: boolean = false
58
59
  onError?: (error: Error) => void
59
60
  customHeaders?: { [key: string]: string }
61
+ lastRequestWasAuthenticationError: boolean = false
62
+ authenticationErrorCount: number = 0
60
63
 
61
64
  constructor({
62
65
  pollingInterval,
@@ -78,7 +81,6 @@ class FeatureFlagsPoller {
78
81
  this.projectApiKey = projectApiKey
79
82
  this.host = host
80
83
  this.poller = undefined
81
- // NOTE: as any is required here as the AbortSignal typing is slightly misaligned but works just fine
82
84
  this.fetch = options.fetch || fetch
83
85
  this.onError = options.onError
84
86
  this.customHeaders = customHeaders
@@ -375,19 +377,44 @@ class FeatureFlagsPoller {
375
377
  }
376
378
  }
377
379
 
380
+ /**
381
+ * If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
382
+ * until a successful request is made, up to a maximum of 60 seconds.
383
+ *
384
+ * @returns The polling interval to use for the next request.
385
+ */
386
+ private getPollingInterval(): number {
387
+ if (!this.lastRequestWasAuthenticationError) {
388
+ return this.pollingInterval
389
+ }
390
+
391
+ return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.authenticationErrorCount)
392
+ }
393
+
378
394
  async _loadFeatureFlags(): Promise<void> {
379
395
  if (this.poller) {
380
396
  clearTimeout(this.poller)
381
397
  this.poller = undefined
382
398
  }
383
- this.poller = setTimeout(() => this._loadFeatureFlags(), this.pollingInterval)
399
+
400
+ this.poller = setTimeout(() => this._loadFeatureFlags(), this.getPollingInterval())
384
401
 
385
402
  try {
386
403
  const res = await this._requestFeatureFlagDefinitions()
387
404
 
388
405
  if (res && res.status === 401) {
406
+ this.lastRequestWasAuthenticationError = true
407
+ this.authenticationErrorCount += 1
408
+ throw new ClientError(
409
+ `Your project key or personal API key is invalid. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`
410
+ )
411
+ }
412
+
413
+ if (res && res.status === 403) {
414
+ this.lastRequestWasAuthenticationError = true
415
+ this.authenticationErrorCount += 1
389
416
  throw new ClientError(
390
- `Your personalApiKey is invalid. Are you sure you're not using your Project API key? More information: https://posthog.com/docs/api/overview`
417
+ `Your personal API key does not have permission to fetch feature flag definitions for local evaluation. Setting next polling interval to ${this.getPollingInterval()}ms. Are you sure you're using the correct personal and Project API key pair? More information: https://posthog.com/docs/api/overview`
391
418
  )
392
419
  }
393
420
 
@@ -410,6 +437,8 @@ class FeatureFlagsPoller {
410
437
  this.groupTypeMapping = responseJson.group_type_mapping || {}
411
438
  this.cohorts = responseJson.cohorts || []
412
439
  this.loadedSuccessfullyOnce = true
440
+ this.lastRequestWasAuthenticationError = false
441
+ this.authenticationErrorCount = 0
413
442
  } catch (err) {
414
443
  // if an error that is not an instance of ClientError is thrown
415
444
  // we silently ignore the error when reloading feature flags
@@ -28,7 +28,11 @@ export type PostHogOptions = PostHogCoreOptions & {
28
28
  fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
29
29
  }
30
30
 
31
- const THIRTY_SECONDS = 30 * 1000
31
+ // Standard local evaluation rate limit is 600 per minute (10 per second),
32
+ // so the fastest a poller should ever be set is 100ms.
33
+ export const MINIMUM_POLLING_INTERVAL = 100
34
+ export const THIRTY_SECONDS = 30 * 1000
35
+ export const SIXTY_SECONDS = 60 * 1000
32
36
  const MAX_CACHE_SIZE = 50 * 1000
33
37
 
34
38
  // The actual exported Nodejs API.
@@ -47,12 +51,20 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
47
51
 
48
52
  this.options = options
49
53
 
54
+ this.options.featureFlagsPollingInterval =
55
+ typeof options.featureFlagsPollingInterval === 'number'
56
+ ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL)
57
+ : THIRTY_SECONDS
58
+
50
59
  if (options.personalApiKey) {
60
+ if (options.personalApiKey.includes('phc_')) {
61
+ throw new Error(
62
+ 'Your Personal API key is invalid. These keys are prefixed with "phx_" and can be created in PostHog project settings.'
63
+ )
64
+ }
65
+
51
66
  this.featureFlagsPoller = new FeatureFlagsPoller({
52
- pollingInterval:
53
- typeof options.featureFlagsPollingInterval === 'number'
54
- ? options.featureFlagsPollingInterval
55
- : THIRTY_SECONDS,
67
+ pollingInterval: this.options.featureFlagsPollingInterval,
56
68
  personalApiKey: options.personalApiKey,
57
69
  projectApiKey: apiKey,
58
70
  timeout: options.requestTimeout ?? 10000, // 10 seconds
@@ -1,9 +1,9 @@
1
- import { PostHog as PostHog } from '../src/posthog-node'
2
- jest.mock('../src/fetch')
1
+ import { MINIMUM_POLLING_INTERVAL, PostHog as PostHog, THIRTY_SECONDS } from '../src/posthog-node'
3
2
  import fetch from '../src/fetch'
4
3
  import { anyDecideCall, anyLocalEvalCall, apiImplementation } from './test-utils'
5
4
  import { waitForPromises, wait } from '../../posthog-core/test/test-utils/test-utils'
6
5
  import { randomUUID } from 'crypto'
6
+ jest.mock('../src/fetch')
7
7
 
8
8
  jest.mock('../package.json', () => ({ version: '1.2.3' }))
9
9
 
@@ -666,6 +666,38 @@ describe('PostHog Node.js', () => {
666
666
  )
667
667
  })
668
668
 
669
+ it('should use minimum featureFlagsPollingInterval of 100ms if set less to less than 100', async () => {
670
+ posthog = new PostHog('TEST_API_KEY', {
671
+ host: 'http://example.com',
672
+ fetchRetryCount: 0,
673
+ personalApiKey: 'TEST_PERSONAL_API_KEY',
674
+ featureFlagsPollingInterval: 98,
675
+ })
676
+
677
+ expect(posthog.options.featureFlagsPollingInterval).toEqual(MINIMUM_POLLING_INTERVAL)
678
+ })
679
+
680
+ it('should use default featureFlagsPollingInterval of 30000ms if none provided', async () => {
681
+ posthog = new PostHog('TEST_API_KEY', {
682
+ host: 'http://example.com',
683
+ fetchRetryCount: 0,
684
+ personalApiKey: 'TEST_PERSONAL_API_KEY',
685
+ })
686
+
687
+ expect(posthog.options.featureFlagsPollingInterval).toEqual(THIRTY_SECONDS)
688
+ })
689
+
690
+ it('should throw an error when creating SDK if a project key is passed in as personalApiKey', async () => {
691
+ expect(() => {
692
+ posthog = new PostHog('TEST_API_KEY', {
693
+ host: 'http://example.com',
694
+ fetchRetryCount: 0,
695
+ personalApiKey: 'phc_abc123',
696
+ featureFlagsPollingInterval: 100,
697
+ })
698
+ }).toThrow(Error)
699
+ })
700
+
669
701
  it('captures feature flags with locally evaluated flags', async () => {
670
702
  mockedFetch.mockClear()
671
703
  mockedFetch.mockClear()