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.
- package/CHANGELOG.md +10 -0
- package/lib/index.cjs.js +38 -5
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.esm.js +36 -6
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +14 -1
- package/lib/posthog-core/src/utils.d.ts +1 -0
- package/lib/posthog-node/src/feature-flags.d.ts +9 -0
- package/lib/posthog-node/src/posthog-node.d.ts +3 -0
- package/package.json +1 -1
- package/src/feature-flags.ts +32 -3
- package/src/posthog-node.ts +17 -5
- package/test/posthog-node.spec.ts +34 -2
|
@@ -186,9 +186,22 @@ export declare abstract class PostHogCore extends PostHogCoreStateless {
|
|
|
186
186
|
/***
|
|
187
187
|
*** ERROR TRACKING
|
|
188
188
|
***/
|
|
189
|
-
captureException(error:
|
|
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
package/src/feature-flags.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
package/src/posthog-node.ts
CHANGED
|
@@ -28,7 +28,11 @@ export type PostHogOptions = PostHogCoreOptions & {
|
|
|
28
28
|
fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
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()
|