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.
@@ -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 (!this.loadedSuccessfullyOnce || forceReload) await this._loadFeatureFlags();
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
- this.featureFlags = responseJson.flags ?? [];
284
- this.featureFlagsByKey = this.featureFlags.reduce((acc, curr)=>(acc[curr.key] = curr, acc), {});
285
- this.groupTypeMapping = responseJson.group_type_mapping || {};
286
- this.cohorts = responseJson.cohorts || {};
287
- this.loadedSuccessfullyOnce = true;
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
- async _requestFeatureFlagDefinitions() {
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
- return await this.fetch(url, options);
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.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,QAAQ,EACR,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,eAAe,CAAA;AAEtB,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,CAAA;IACzC,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;IACrD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,YAAa,SAAQ,eAAe;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;IACxC,gBAAgB,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAA;IACpD,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,CAAA;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,KAAK,GAAG,IAAI,CAAA;IAClB,MAAM,EAAE,aAAa,EAAE,GAAG,YAAY,EAAE,CAAA;CACzC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,OAAO,CAAA;AAE/E,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,EAAE,YAAY,EAAE,CAAA;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,KAAK,YAAY,GAAG,IAAI,CAAA;AAE9E,MAAM,MAAM,cAAc,GAAG,kBAAkB,GAAG;IAChD,WAAW,CAAC,EAAE,QAAQ,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,0BAA0B,CAAC,EAAE,OAAO,CAAA;IAEpC,2BAA2B,CAAC,EAAE,MAAM,CAAA;IAEpC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAA;IAGpF,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,YAAY,GAAG,YAAY,EAAE,CAAA;IAC3C;;;;;;;;;OASG;IACH,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,MAAM,EAAE,CAAA;IACpC;;;;;;;;;;;;;;;;OAgBG;IACH,+BAA+B,CAAC,EAAE,OAAO,CAAA;CAC1C,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE;QACR,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAA;QAC/B,YAAY,CAAC,EAAE;YACb,QAAQ,EAAE;gBACR,GAAG,EAAE,MAAM,CAAA;gBACX,kBAAkB,EAAE,MAAM,CAAA;aAC3B,EAAE,CAAA;SACJ,CAAA;QACD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAClC,CAAA;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;IACf,kBAAkB,EAAE,IAAI,GAAG,MAAM,CAAA;IACjC,4BAA4B,EAAE,OAAO,CAAA;IACrC,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB,CAAA;AAED,MAAM,WAAW,QAAQ;IACvB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,YAAY,GAAG,IAAI,CAAA;IAExF;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1G;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,EAAE,eAAe,GAAG,IAAI,CAAA;IAE3D;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE7E;;;;;;;;;;OAUG;IACH,KAAK,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAExD;;;;;OAKG;IACH,cAAc,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1E;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CACd,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACzC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;QACxD,mBAAmB,CAAC,EAAE,OAAO,CAAA;QAC7B,qBAAqB,CAAC,EAAE,OAAO,CAAA;KAChC,GACA,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IAE/B;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CACZ,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACzC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;QACxD,mBAAmB,CAAC,EAAE,OAAO,CAAA;QAC7B,qBAAqB,CAAC,EAAE,OAAO,CAAA;KAChC,GACA,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAA;IAExC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,qBAAqB,CACnB,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,gBAAgB,EAC7B,OAAO,CAAC,EAAE;QACR,mBAAmB,CAAC,EAAE,OAAO,CAAA;KAC9B,GACA,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAA;IAEhC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,oBAAoB,GAAG,IAAI,CAAA;IAE9E;;;OAGG;IACH,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnC;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAE1C;;;;OAIG;IACH,2BAA2B,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAEjE;;;OAGG;IACH,sBAAsB,IAAI,OAAO,CAAA;CAClC"}
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.11.2";
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.11.2';
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.11.2';
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.11.2",
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.2"
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
- [CoreErrorTracking.nodeStackLineParser]
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
- [CoreErrorTracking.nodeStackLineParser],
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
- await this._loadFeatureFlags()
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
- this.featureFlags = (responseJson.flags as PostHogFeatureFlag[]) ?? []
641
- this.featureFlagsByKey = this.featureFlags.reduce(
642
- (acc, curr) => ((acc[curr.key] = curr), acc),
643
- <Record<string, PostHogFeatureFlag>>{}
644
- )
645
- this.groupTypeMapping = (responseJson.group_type_mapping as Record<string, string>) || {}
646
- this.cohorts = (responseJson.cohorts as Record<string, PropertyGroup>) || {}
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
- async _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse> {
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
- return await this.fetch(url, options)
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