posthog-node 5.11.2 → 5.13.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/LICENSE +56 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -2
- package/dist/client.mjs +3 -2
- package/dist/entrypoints/index.edge.js +1 -3
- package/dist/entrypoints/index.edge.mjs +1 -3
- package/dist/entrypoints/index.node.js +1 -3
- package/dist/entrypoints/index.node.mjs +1 -3
- package/dist/experimental.d.ts +11 -0
- package/dist/experimental.d.ts.map +1 -0
- package/dist/experimental.js +18 -0
- package/dist/experimental.mjs +0 -0
- package/dist/extensions/feature-flags/cache.d.ts +110 -0
- package/dist/extensions/feature-flags/cache.d.ts.map +1 -0
- package/dist/extensions/feature-flags/cache.js +18 -0
- package/dist/extensions/feature-flags/cache.mjs +0 -0
- package/dist/extensions/feature-flags/feature-flags.d.ts +15 -1
- package/dist/extensions/feature-flags/feature-flags.d.ts.map +1 -1
- package/dist/extensions/feature-flags/feature-flags.js +71 -9
- package/dist/extensions/feature-flags/feature-flags.mjs +71 -9
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +6 -2
- package/src/client.ts +2 -1
- package/src/entrypoints/index.edge.ts +1 -1
- package/src/entrypoints/index.node.ts +1 -1
- package/src/experimental.ts +11 -0
- package/src/extensions/feature-flags/cache.ts +114 -0
- package/src/extensions/feature-flags/feature-flags.ts +141 -12
- package/src/types.ts +28 -0
- package/src/version.ts +1 -1
|
@@ -35,6 +35,7 @@ class FeatureFlagsPoller {
|
|
|
35
35
|
this.debugMode = false;
|
|
36
36
|
this.shouldBeginExponentialBackoff = false;
|
|
37
37
|
this.backOffCount = 0;
|
|
38
|
+
this.hasAttemptedCacheLoad = false;
|
|
38
39
|
this.pollingInterval = pollingInterval;
|
|
39
40
|
this.personalApiKey = personalApiKey;
|
|
40
41
|
this.featureFlags = [];
|
|
@@ -50,6 +51,7 @@ class FeatureFlagsPoller {
|
|
|
50
51
|
this.onError = options.onError;
|
|
51
52
|
this.customHeaders = customHeaders;
|
|
52
53
|
this.onLoad = options.onLoad;
|
|
54
|
+
this.cacheProvider = options.cacheProvider;
|
|
53
55
|
this.loadFeatureFlags();
|
|
54
56
|
}
|
|
55
57
|
debug(enabled = true) {
|
|
@@ -237,8 +239,39 @@ class FeatureFlagsPoller {
|
|
|
237
239
|
});
|
|
238
240
|
return lookupTable;
|
|
239
241
|
}
|
|
242
|
+
updateFlagState(flagData) {
|
|
243
|
+
this.featureFlags = flagData.flags;
|
|
244
|
+
this.featureFlagsByKey = flagData.flags.reduce((acc, curr)=>(acc[curr.key] = curr, acc), {});
|
|
245
|
+
this.groupTypeMapping = flagData.groupTypeMapping;
|
|
246
|
+
this.cohorts = flagData.cohorts;
|
|
247
|
+
this.loadedSuccessfullyOnce = true;
|
|
248
|
+
}
|
|
249
|
+
async loadFromCache(debugMessage) {
|
|
250
|
+
if (!this.cacheProvider) return false;
|
|
251
|
+
try {
|
|
252
|
+
const cached = await this.cacheProvider.getFlagDefinitions();
|
|
253
|
+
if (cached) {
|
|
254
|
+
this.updateFlagState(cached);
|
|
255
|
+
this.logMsgIfDebug(()=>console.debug(`[FEATURE FLAGS] ${debugMessage} (${cached.flags.length} flags)`));
|
|
256
|
+
this.onLoad?.(this.featureFlags.length);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
this.onError?.(new Error(`Failed to load from cache: ${err}`));
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
240
265
|
async loadFeatureFlags(forceReload = false) {
|
|
241
|
-
if (
|
|
266
|
+
if (this.cacheProvider && !this.hasAttemptedCacheLoad) {
|
|
267
|
+
this.hasAttemptedCacheLoad = true;
|
|
268
|
+
await this.loadFromCache('Loaded flags from cache');
|
|
269
|
+
}
|
|
270
|
+
if (this.loadingPromise) return this.loadingPromise;
|
|
271
|
+
if (!this.loadedSuccessfullyOnce || forceReload) {
|
|
272
|
+
this.loadingPromise = this._loadFeatureFlags();
|
|
273
|
+
await this.loadingPromise;
|
|
274
|
+
}
|
|
242
275
|
}
|
|
243
276
|
isLocalEvaluationReady() {
|
|
244
277
|
return (this.loadedSuccessfullyOnce ?? false) && (this.featureFlags?.length ?? 0) > 0;
|
|
@@ -254,6 +287,17 @@ class FeatureFlagsPoller {
|
|
|
254
287
|
}
|
|
255
288
|
this.poller = setTimeout(()=>this._loadFeatureFlags(), this.getPollingInterval());
|
|
256
289
|
try {
|
|
290
|
+
let shouldFetch = true;
|
|
291
|
+
if (this.cacheProvider) try {
|
|
292
|
+
shouldFetch = await this.cacheProvider.shouldFetchFlagDefinitions();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
this.onError?.(new Error(`Error in shouldFetchFlagDefinitions: ${err}`));
|
|
295
|
+
}
|
|
296
|
+
if (!shouldFetch) {
|
|
297
|
+
const loaded = await this.loadFromCache('Loaded flags from cache (skipped fetch)');
|
|
298
|
+
if (loaded) return;
|
|
299
|
+
if (this.loadedSuccessfullyOnce) return;
|
|
300
|
+
}
|
|
257
301
|
const res = await this._requestFeatureFlagDefinitions();
|
|
258
302
|
if (!res) return;
|
|
259
303
|
switch(res.status){
|
|
@@ -280,13 +324,19 @@ class FeatureFlagsPoller {
|
|
|
280
324
|
{
|
|
281
325
|
const responseJson = await res.json() ?? {};
|
|
282
326
|
if (!('flags' in responseJson)) return void this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`));
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
327
|
+
const flagData = {
|
|
328
|
+
flags: responseJson.flags ?? [],
|
|
329
|
+
groupTypeMapping: responseJson.group_type_mapping || {},
|
|
330
|
+
cohorts: responseJson.cohorts || {}
|
|
331
|
+
};
|
|
332
|
+
this.updateFlagState(flagData);
|
|
288
333
|
this.shouldBeginExponentialBackoff = false;
|
|
289
334
|
this.backOffCount = 0;
|
|
335
|
+
if (this.cacheProvider && shouldFetch) try {
|
|
336
|
+
await this.cacheProvider.onFlagDefinitionsReceived(flagData);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
this.onError?.(new Error(`Failed to store in cache: ${err}`));
|
|
339
|
+
}
|
|
290
340
|
this.onLoad?.(this.featureFlags.length);
|
|
291
341
|
break;
|
|
292
342
|
}
|
|
@@ -295,6 +345,8 @@ class FeatureFlagsPoller {
|
|
|
295
345
|
}
|
|
296
346
|
} catch (err) {
|
|
297
347
|
if (err instanceof ClientError) this.onError?.(err);
|
|
348
|
+
} finally{
|
|
349
|
+
this.loadingPromise = void 0;
|
|
298
350
|
}
|
|
299
351
|
}
|
|
300
352
|
getPersonalApiKeyRequestOptions(method = 'GET') {
|
|
@@ -307,7 +359,7 @@ class FeatureFlagsPoller {
|
|
|
307
359
|
}
|
|
308
360
|
};
|
|
309
361
|
}
|
|
310
|
-
|
|
362
|
+
_requestFeatureFlagDefinitions() {
|
|
311
363
|
const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`;
|
|
312
364
|
const options = this.getPersonalApiKeyRequestOptions();
|
|
313
365
|
let abortTimeout = null;
|
|
@@ -319,13 +371,23 @@ class FeatureFlagsPoller {
|
|
|
319
371
|
options.signal = controller.signal;
|
|
320
372
|
}
|
|
321
373
|
try {
|
|
322
|
-
|
|
374
|
+
const fetch1 = this.fetch;
|
|
375
|
+
return fetch1(url, options);
|
|
323
376
|
} finally{
|
|
324
377
|
clearTimeout(abortTimeout);
|
|
325
378
|
}
|
|
326
379
|
}
|
|
327
|
-
stopPoller() {
|
|
380
|
+
async stopPoller(timeoutMs = 30000) {
|
|
328
381
|
clearTimeout(this.poller);
|
|
382
|
+
if (this.cacheProvider) try {
|
|
383
|
+
const shutdownResult = this.cacheProvider.shutdown();
|
|
384
|
+
if (shutdownResult instanceof Promise) await Promise.race([
|
|
385
|
+
shutdownResult,
|
|
386
|
+
new Promise((_, reject)=>setTimeout(()=>reject(new Error(`Cache shutdown timeout after ${timeoutMs}ms`)), timeoutMs))
|
|
387
|
+
]);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
this.onError?.(new Error(`Error during cache shutdown: ${err}`));
|
|
390
|
+
}
|
|
329
391
|
}
|
|
330
392
|
}
|
|
331
393
|
async function _hash(key, distinctId, salt = '') {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PostHogCoreOptions, FeatureFlagValue, JsonType, PostHogFetchOptions, PostHogFetchResponse } from '@posthog/core';
|
|
2
|
+
import type { FlagDefinitionCacheProvider } from './extensions/feature-flags/cache';
|
|
2
3
|
export interface IdentifyMessage {
|
|
3
4
|
distinctId: string;
|
|
4
5
|
properties?: Record<string | number, any>;
|
|
@@ -52,6 +53,32 @@ export type PostHogOptions = PostHogCoreOptions & {
|
|
|
52
53
|
maxCacheSize?: number;
|
|
53
54
|
fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
|
|
54
55
|
enableLocalEvaluation?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* @experimental This API is experimental and may change in minor versions.
|
|
58
|
+
*
|
|
59
|
+
* Optional cache provider for feature flag definitions.
|
|
60
|
+
*
|
|
61
|
+
* Allows custom caching strategies (Redis, database, etc.) for flag definitions
|
|
62
|
+
* in multi-worker environments. If not provided, defaults to in-memory cache.
|
|
63
|
+
*
|
|
64
|
+
* This enables distributed coordination where only one worker fetches flags while
|
|
65
|
+
* others use cached data, reducing API calls and improving performance.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import { FlagDefinitionCacheProvider } from 'posthog-node/experimental'
|
|
70
|
+
*
|
|
71
|
+
* class RedisCacheProvider implements FlagDefinitionCacheProvider {
|
|
72
|
+
* // ... implementation
|
|
73
|
+
* }
|
|
74
|
+
*
|
|
75
|
+
* const client = new PostHog('api-key', {
|
|
76
|
+
* personalApiKey: 'personal-key',
|
|
77
|
+
* flagDefinitionCacheProvider: new RedisCacheProvider(redis)
|
|
78
|
+
* })
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
flagDefinitionCacheProvider?: FlagDefinitionCacheProvider;
|
|
55
82
|
/**
|
|
56
83
|
* Allows modification or dropping of events before they're sent to PostHog.
|
|
57
84
|
* If an array is provided, the functions are run in order.
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version = "5.
|
|
1
|
+
export declare const version = "5.13.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -26,7 +26,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
26
26
|
__webpack_require__.d(__webpack_exports__, {
|
|
27
27
|
version: ()=>version
|
|
28
28
|
});
|
|
29
|
-
const version = '5.
|
|
29
|
+
const version = '5.13.0';
|
|
30
30
|
exports.version = __webpack_exports__.version;
|
|
31
31
|
for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
32
32
|
"version"
|
package/dist/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = '5.
|
|
1
|
+
const version = '5.13.0';
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posthog-node",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.0",
|
|
4
4
|
"description": "PostHog Node.js integration",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
"module": "dist/entrypoints/index.node.mjs",
|
|
26
26
|
"types": "dist/entrypoints/index.node.d.ts",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@posthog/core": "1.5.
|
|
28
|
+
"@posthog/core": "1.5.3"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.0.0",
|
|
32
32
|
"jest": "^29.7.0",
|
|
33
|
+
"@types/jest": "^29.5.0",
|
|
33
34
|
"@rslib/core": "^0.10.5",
|
|
34
35
|
"@posthog-tooling/tsconfig-base": "1.0.0"
|
|
35
36
|
},
|
|
@@ -64,6 +65,9 @@
|
|
|
64
65
|
},
|
|
65
66
|
"import": "./dist/entrypoints/index.node.mjs",
|
|
66
67
|
"require": "./dist/entrypoints/index.node.js"
|
|
68
|
+
},
|
|
69
|
+
"./experimental": {
|
|
70
|
+
"types": "./dist/experimental.d.ts"
|
|
67
71
|
}
|
|
68
72
|
},
|
|
69
73
|
"scripts": {
|
package/src/client.ts
CHANGED
|
@@ -112,6 +112,7 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
112
112
|
this._events.emit('localEvaluationFlagsLoaded', count)
|
|
113
113
|
},
|
|
114
114
|
customHeaders: this.getCustomHeaders(),
|
|
115
|
+
cacheProvider: options.flagDefinitionCacheProvider,
|
|
115
116
|
})
|
|
116
117
|
}
|
|
117
118
|
}
|
|
@@ -1179,7 +1180,7 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1179
1180
|
* @returns Promise that resolves when shutdown is complete
|
|
1180
1181
|
*/
|
|
1181
1182
|
async _shutdown(shutdownTimeoutMs?: number): Promise<void> {
|
|
1182
|
-
this.featureFlagsPoller?.stopPoller()
|
|
1183
|
+
this.featureFlagsPoller?.stopPoller(shutdownTimeoutMs)
|
|
1183
1184
|
this.errorTracking.shutdown()
|
|
1184
1185
|
return super._shutdown(shutdownTimeoutMs)
|
|
1185
1186
|
}
|
|
@@ -12,7 +12,7 @@ ErrorTracking.errorPropertiesBuilder = new CoreErrorTracking.ErrorPropertiesBuil
|
|
|
12
12
|
new CoreErrorTracking.StringCoercer(),
|
|
13
13
|
new CoreErrorTracking.PrimitiveCoercer(),
|
|
14
14
|
],
|
|
15
|
-
|
|
15
|
+
CoreErrorTracking.createStackParser('node:javascript', CoreErrorTracking.nodeStackLineParser)
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
export class PostHog extends PostHogBackendClient {
|
|
@@ -15,7 +15,7 @@ ErrorTracking.errorPropertiesBuilder = new CoreErrorTracking.ErrorPropertiesBuil
|
|
|
15
15
|
new CoreErrorTracking.StringCoercer(),
|
|
16
16
|
new CoreErrorTracking.PrimitiveCoercer(),
|
|
17
17
|
],
|
|
18
|
-
|
|
18
|
+
CoreErrorTracking.createStackParser('node:javascript', CoreErrorTracking.nodeStackLineParser),
|
|
19
19
|
[createModulerModifier(), addSourceContext]
|
|
20
20
|
)
|
|
21
21
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experimental APIs
|
|
3
|
+
*
|
|
4
|
+
* This module exports experimental features that may change or be removed in minor versions.
|
|
5
|
+
* Use these APIs with caution and be prepared for breaking changes.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
* @experimental
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type { FlagDefinitionCacheProvider, FlagDefinitionCacheData } from './extensions/feature-flags/cache'
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { PostHogFeatureFlag, PropertyGroup } from '../../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents the complete set of feature flag data needed for local evaluation.
|
|
5
|
+
*
|
|
6
|
+
* This includes flag definitions, group type mappings, and cohort property groups.
|
|
7
|
+
*/
|
|
8
|
+
export interface FlagDefinitionCacheData {
|
|
9
|
+
/** Array of feature flag definitions */
|
|
10
|
+
flags: PostHogFeatureFlag[]
|
|
11
|
+
/** Mapping of group type index to group name */
|
|
12
|
+
groupTypeMapping: Record<string, string>
|
|
13
|
+
/** Cohort property groups for local evaluation */
|
|
14
|
+
cohorts: Record<string, PropertyGroup>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @experimental This API is experimental and may change in minor versions.
|
|
19
|
+
*
|
|
20
|
+
* Provider interface for caching feature flag definitions.
|
|
21
|
+
*
|
|
22
|
+
* Implementations can use this to control when flag definitions are fetched
|
|
23
|
+
* and how they're cached (Redis, database, filesystem, etc.).
|
|
24
|
+
*
|
|
25
|
+
* This interface is designed for server-side environments where multiple workers
|
|
26
|
+
* need to share flag definitions and coordinate fetching to reduce API calls.
|
|
27
|
+
*
|
|
28
|
+
* All methods may throw errors - the poller will catch and log them gracefully,
|
|
29
|
+
* ensuring cache provider errors never break flag evaluation.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { FlagDefinitionCacheProvider } from 'posthog-node/experimental'
|
|
34
|
+
*
|
|
35
|
+
* class RedisFlagCache implements FlagDefinitionCacheProvider {
|
|
36
|
+
* constructor(private redis: Redis, private teamKey: string) { }
|
|
37
|
+
*
|
|
38
|
+
* async getFlagDefinitions(): Promise<FlagDefinitionCacheData | undefined> {
|
|
39
|
+
* const cached = await this.redis.get(`posthog:flags:${this.teamKey}`)
|
|
40
|
+
* return cached ? JSON.parse(cached) : undefined
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* async shouldFetchFlagDefinitions(): Promise<boolean> {
|
|
44
|
+
* // Acquire distributed lock - only one worker fetches
|
|
45
|
+
* const acquired = await this.redis.set(`posthog:flags:${this.teamKey}:lock`, '1', 'EX', 60, 'NX')
|
|
46
|
+
* return acquired === 'OK'
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* async onFlagDefinitionsReceived(data: FlagDefinitionCacheData): Promise<void> {
|
|
50
|
+
* await this.redis.set(`posthog:flags:${this.teamKey}`, JSON.stringify(data), 'EX', 300)
|
|
51
|
+
* await this.redis.del(`posthog:flags:${this.teamKey}:lock`)
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* async shutdown(): Promise<void> {
|
|
55
|
+
* await this.redis.del(`posthog:flags:${this.teamKey}:lock`)
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export interface FlagDefinitionCacheProvider {
|
|
61
|
+
/**
|
|
62
|
+
* Retrieve cached flag definitions.
|
|
63
|
+
*
|
|
64
|
+
* Called when the poller is refreshing in-memory flag definitions. If this returns undefined
|
|
65
|
+
* (or throws an error), the poller will fetch fresh data from the PostHog API if no flag
|
|
66
|
+
* definitions are in memory. Otherwise, stale cache data is used until the next poll cycle.
|
|
67
|
+
*
|
|
68
|
+
* @returns cached definitions if available, undefined if cache is empty
|
|
69
|
+
* @throws if an error occurs while accessing the cache (error will be logged)
|
|
70
|
+
*/
|
|
71
|
+
getFlagDefinitions(): Promise<FlagDefinitionCacheData | undefined> | FlagDefinitionCacheData | undefined
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Determines whether this instance should fetch new flag definitions.
|
|
75
|
+
*
|
|
76
|
+
* Use this to implement distributed coordination (e.g., via distributed locks)
|
|
77
|
+
* to ensure only one instance fetches at a time in a multi-worker setup.
|
|
78
|
+
*
|
|
79
|
+
* When multiple workers share a cache, typically only one should fetch while
|
|
80
|
+
* others use cached data. Implementations can use Redis locks, database locks,
|
|
81
|
+
* or other coordination mechanisms.
|
|
82
|
+
*
|
|
83
|
+
* @returns true if this instance should fetch, false to skip and read cache
|
|
84
|
+
* @throws if coordination backend is unavailable (error will be logged, fetch continues)
|
|
85
|
+
*/
|
|
86
|
+
shouldFetchFlagDefinitions(): Promise<boolean> | boolean
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Called after successfully receiving new flag definitions from PostHog.
|
|
90
|
+
*
|
|
91
|
+
* Store the definitions in your cache backend here. This is called only
|
|
92
|
+
* after a successful API response with valid flag data.
|
|
93
|
+
*
|
|
94
|
+
* If this method throws, the error is logged but flag definitions are still
|
|
95
|
+
* stored in memory, ensuring local evaluation can still be performed.
|
|
96
|
+
*
|
|
97
|
+
* @param data - The complete flag definition data from PostHog
|
|
98
|
+
* @throws if storage backend is unavailable (error will be logged)
|
|
99
|
+
*/
|
|
100
|
+
onFlagDefinitionsReceived(data: FlagDefinitionCacheData): Promise<void> | void
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Called when the PostHog client shuts down.
|
|
104
|
+
*
|
|
105
|
+
* Release any held locks, close connections, or clean up resources here.
|
|
106
|
+
*
|
|
107
|
+
* Both sync and async cleanup are supported. Async cleanup has a timeout
|
|
108
|
+
* (default 30s, configurable via client shutdown options) to prevent the
|
|
109
|
+
* process shutdown from hanging indefinitely.
|
|
110
|
+
*
|
|
111
|
+
* @returns Promise that resolves when cleanup is complete, or void for sync cleanup
|
|
112
|
+
*/
|
|
113
|
+
shutdown(): Promise<void> | void
|
|
114
|
+
}
|
|
@@ -2,6 +2,7 @@ import { FeatureFlagCondition, FlagProperty, FlagPropertyValue, PostHogFeatureFl
|
|
|
2
2
|
import type { FeatureFlagValue, JsonType, PostHogFetchOptions, PostHogFetchResponse } from '@posthog/core'
|
|
3
3
|
import { safeSetTimeout } from '@posthog/core'
|
|
4
4
|
import { hashSHA1 } from './crypto'
|
|
5
|
+
import { FlagDefinitionCacheProvider, FlagDefinitionCacheData } from './cache'
|
|
5
6
|
|
|
6
7
|
const SIXTY_SECONDS = 60 * 1000
|
|
7
8
|
|
|
@@ -53,6 +54,7 @@ type FeatureFlagsPollerOptions = {
|
|
|
53
54
|
onError?: (error: Error) => void
|
|
54
55
|
onLoad?: (count: number) => void
|
|
55
56
|
customHeaders?: { [key: string]: string }
|
|
57
|
+
cacheProvider?: FlagDefinitionCacheProvider
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
class FeatureFlagsPoller {
|
|
@@ -74,6 +76,9 @@ class FeatureFlagsPoller {
|
|
|
74
76
|
shouldBeginExponentialBackoff: boolean = false
|
|
75
77
|
backOffCount: number = 0
|
|
76
78
|
onLoad?: (count: number) => void
|
|
79
|
+
private cacheProvider?: FlagDefinitionCacheProvider
|
|
80
|
+
private hasAttemptedCacheLoad: boolean = false
|
|
81
|
+
private loadingPromise?: Promise<void>
|
|
77
82
|
|
|
78
83
|
constructor({
|
|
79
84
|
pollingInterval,
|
|
@@ -99,6 +104,7 @@ class FeatureFlagsPoller {
|
|
|
99
104
|
this.onError = options.onError
|
|
100
105
|
this.customHeaders = customHeaders
|
|
101
106
|
this.onLoad = options.onLoad
|
|
107
|
+
this.cacheProvider = options.cacheProvider
|
|
102
108
|
void this.loadFeatureFlags()
|
|
103
109
|
}
|
|
104
110
|
|
|
@@ -537,9 +543,59 @@ class FeatureFlagsPoller {
|
|
|
537
543
|
return lookupTable
|
|
538
544
|
}
|
|
539
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Updates the internal flag state with the provided flag data.
|
|
548
|
+
*/
|
|
549
|
+
private updateFlagState(flagData: FlagDefinitionCacheData): void {
|
|
550
|
+
this.featureFlags = flagData.flags
|
|
551
|
+
this.featureFlagsByKey = flagData.flags.reduce(
|
|
552
|
+
(acc, curr) => ((acc[curr.key] = curr), acc),
|
|
553
|
+
<Record<string, PostHogFeatureFlag>>{}
|
|
554
|
+
)
|
|
555
|
+
this.groupTypeMapping = flagData.groupTypeMapping
|
|
556
|
+
this.cohorts = flagData.cohorts
|
|
557
|
+
this.loadedSuccessfullyOnce = true
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Attempts to load flags from cache and update internal state.
|
|
562
|
+
* Returns true if flags were successfully loaded from cache, false otherwise.
|
|
563
|
+
*/
|
|
564
|
+
private async loadFromCache(debugMessage: string): Promise<boolean> {
|
|
565
|
+
if (!this.cacheProvider) {
|
|
566
|
+
return false
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const cached = await this.cacheProvider.getFlagDefinitions()
|
|
571
|
+
if (cached) {
|
|
572
|
+
this.updateFlagState(cached)
|
|
573
|
+
this.logMsgIfDebug(() => console.debug(`[FEATURE FLAGS] ${debugMessage} (${cached.flags.length} flags)`))
|
|
574
|
+
this.onLoad?.(this.featureFlags.length)
|
|
575
|
+
return true
|
|
576
|
+
}
|
|
577
|
+
return false
|
|
578
|
+
} catch (err) {
|
|
579
|
+
this.onError?.(new Error(`Failed to load from cache: ${err}`))
|
|
580
|
+
return false
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
540
584
|
async loadFeatureFlags(forceReload = false): Promise<void> {
|
|
585
|
+
// On first load, try to initialize from cache (if a cache provider is configured)
|
|
586
|
+
if (this.cacheProvider && !this.hasAttemptedCacheLoad) {
|
|
587
|
+
this.hasAttemptedCacheLoad = true
|
|
588
|
+
await this.loadFromCache('Loaded flags from cache')
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// If a fetch is already in progress, wait for it
|
|
592
|
+
if (this.loadingPromise) {
|
|
593
|
+
return this.loadingPromise
|
|
594
|
+
}
|
|
595
|
+
|
|
541
596
|
if (!this.loadedSuccessfullyOnce || forceReload) {
|
|
542
|
-
|
|
597
|
+
this.loadingPromise = this._loadFeatureFlags()
|
|
598
|
+
await this.loadingPromise
|
|
543
599
|
}
|
|
544
600
|
}
|
|
545
601
|
|
|
@@ -574,6 +630,42 @@ class FeatureFlagsPoller {
|
|
|
574
630
|
this.poller = setTimeout(() => this._loadFeatureFlags(), this.getPollingInterval())
|
|
575
631
|
|
|
576
632
|
try {
|
|
633
|
+
let shouldFetch = true
|
|
634
|
+
if (this.cacheProvider) {
|
|
635
|
+
try {
|
|
636
|
+
shouldFetch = await this.cacheProvider.shouldFetchFlagDefinitions()
|
|
637
|
+
} catch (err) {
|
|
638
|
+
this.onError?.(new Error(`Error in shouldFetchFlagDefinitions: ${err}`))
|
|
639
|
+
// Important: if `shouldFetchFlagDefinitions` throws, we
|
|
640
|
+
// default to fetching.
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!shouldFetch) {
|
|
645
|
+
// If we're not supposed to fetch, we assume another instance
|
|
646
|
+
// is handling it. In this case, we'll just reload from cache.
|
|
647
|
+
const loaded = await this.loadFromCache('Loaded flags from cache (skipped fetch)')
|
|
648
|
+
if (loaded) {
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (this.loadedSuccessfullyOnce) {
|
|
653
|
+
// Respect the decision to not fetch, even if it means
|
|
654
|
+
// keeping stale feature flags.
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// If we've gotten here:
|
|
659
|
+
// - A cache provider is configured
|
|
660
|
+
// - We've been asked not to fetch
|
|
661
|
+
// - We failed to load from cache
|
|
662
|
+
// - We have no feature flag definitions to work with.
|
|
663
|
+
//
|
|
664
|
+
// This is the only case where we'll ignore the shouldFetch
|
|
665
|
+
// decision and proceed to fetch, because the alternative is
|
|
666
|
+
// worse: local evaluation is impossible.
|
|
667
|
+
}
|
|
668
|
+
|
|
577
669
|
const res = await this._requestFeatureFlagDefinitions()
|
|
578
670
|
|
|
579
671
|
// Handle undefined res case, this shouldn't happen, but it doesn't hurt to handle it anyway
|
|
@@ -637,16 +729,28 @@ class FeatureFlagsPoller {
|
|
|
637
729
|
return
|
|
638
730
|
}
|
|
639
731
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
this.
|
|
647
|
-
this.loadedSuccessfullyOnce = true
|
|
732
|
+
const flagData: FlagDefinitionCacheData = {
|
|
733
|
+
flags: (responseJson.flags as PostHogFeatureFlag[]) ?? [],
|
|
734
|
+
groupTypeMapping: (responseJson.group_type_mapping as Record<string, string>) || {},
|
|
735
|
+
cohorts: (responseJson.cohorts as Record<string, PropertyGroup>) || {},
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
this.updateFlagState(flagData)
|
|
648
739
|
this.shouldBeginExponentialBackoff = false
|
|
649
740
|
this.backOffCount = 0
|
|
741
|
+
|
|
742
|
+
if (this.cacheProvider && shouldFetch) {
|
|
743
|
+
// Only notify the cache if it's actually expecting new data
|
|
744
|
+
// E.g., if we weren't supposed to fetch but we missed the
|
|
745
|
+
// cache, we may not have a lock, so we skip this step
|
|
746
|
+
try {
|
|
747
|
+
await this.cacheProvider.onFlagDefinitionsReceived(flagData)
|
|
748
|
+
} catch (err) {
|
|
749
|
+
this.onError?.(new Error(`Failed to store in cache: ${err}`))
|
|
750
|
+
// Continue anyway, the data at least made it to memory
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
650
754
|
this.onLoad?.(this.featureFlags.length)
|
|
651
755
|
break
|
|
652
756
|
}
|
|
@@ -660,6 +764,8 @@ class FeatureFlagsPoller {
|
|
|
660
764
|
if (err instanceof ClientError) {
|
|
661
765
|
this.onError?.(err)
|
|
662
766
|
}
|
|
767
|
+
} finally {
|
|
768
|
+
this.loadingPromise = undefined
|
|
663
769
|
}
|
|
664
770
|
}
|
|
665
771
|
|
|
@@ -674,7 +780,7 @@ class FeatureFlagsPoller {
|
|
|
674
780
|
}
|
|
675
781
|
}
|
|
676
782
|
|
|
677
|
-
|
|
783
|
+
_requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse> {
|
|
678
784
|
const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`
|
|
679
785
|
|
|
680
786
|
const options = this.getPersonalApiKeyRequestOptions()
|
|
@@ -690,14 +796,37 @@ class FeatureFlagsPoller {
|
|
|
690
796
|
}
|
|
691
797
|
|
|
692
798
|
try {
|
|
693
|
-
|
|
799
|
+
// Unbind fetch from `this` to avoid potential issues in edge environments, e.g., Cloudflare Workers:
|
|
800
|
+
// https://developers.cloudflare.com/workers/observability/errors/#illegal-invocation-errors
|
|
801
|
+
const fetch = this.fetch
|
|
802
|
+
return fetch(url, options)
|
|
694
803
|
} finally {
|
|
695
804
|
clearTimeout(abortTimeout)
|
|
696
805
|
}
|
|
697
806
|
}
|
|
698
807
|
|
|
699
|
-
stopPoller(): void {
|
|
808
|
+
async stopPoller(timeoutMs: number = 30000): Promise<void> {
|
|
700
809
|
clearTimeout(this.poller)
|
|
810
|
+
|
|
811
|
+
if (this.cacheProvider) {
|
|
812
|
+
try {
|
|
813
|
+
const shutdownResult = this.cacheProvider.shutdown()
|
|
814
|
+
|
|
815
|
+
if (shutdownResult instanceof Promise) {
|
|
816
|
+
// This follows the same timeout logic defined in _shutdown.
|
|
817
|
+
// We time out after some period of time to avoid hanging the entire
|
|
818
|
+
// shutdown process if the cache provider misbehaves.
|
|
819
|
+
await Promise.race([
|
|
820
|
+
shutdownResult,
|
|
821
|
+
new Promise((_, reject) =>
|
|
822
|
+
setTimeout(() => reject(new Error(`Cache shutdown timeout after ${timeoutMs}ms`)), timeoutMs)
|
|
823
|
+
),
|
|
824
|
+
])
|
|
825
|
+
}
|
|
826
|
+
} catch (err) {
|
|
827
|
+
this.onError?.(new Error(`Error during cache shutdown: ${err}`))
|
|
828
|
+
}
|
|
829
|
+
}
|
|
701
830
|
}
|
|
702
831
|
}
|
|
703
832
|
|