posthog-node 4.10.0 → 4.10.2

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.
@@ -1,18 +1,22 @@
1
- import { PostHogFetchOptions, PostHogFetchResponse, PostHogAutocaptureElement, PostHogDecideResponse, PostHogCoreOptions, PostHogEventProperties, PostHogPersistedProperty, PostHogCaptureOptions, JsonType } from './types';
1
+ import { PostHogFetchOptions, PostHogFetchResponse, PostHogAutocaptureElement, PostHogDecideResponse, PostHogCoreOptions, PostHogEventProperties, PostHogPersistedProperty, PostHogCaptureOptions, JsonType, PostHogRemoteConfig } from './types';
2
2
  import { RetriableOptions } from './utils';
3
3
  import { LZString } from './lz-string';
4
4
  import { SimpleEventEmitter } from './eventemitter';
5
+ import { SurveyResponse } from './surveys-types';
5
6
  export * as utils from './utils';
6
7
  export declare abstract class PostHogCoreStateless {
7
8
  readonly apiKey: string;
8
9
  readonly host: string;
9
10
  readonly flushAt: number;
11
+ readonly preloadFeatureFlags: boolean;
12
+ readonly disableSurveys: boolean;
10
13
  private maxBatchSize;
11
14
  private maxQueueSize;
12
15
  private flushInterval;
13
16
  private flushPromise;
14
17
  private requestTimeout;
15
18
  private featureFlagsRequestTimeoutMs;
19
+ private remoteConfigRequestTimeoutMs;
16
20
  private captureMode;
17
21
  private removeDebugCallback?;
18
22
  private disableGeoip;
@@ -25,6 +29,7 @@ export declare abstract class PostHogCoreStateless {
25
29
  protected _retryOptions: RetriableOptions;
26
30
  protected _initPromise: Promise<void>;
27
31
  protected _isInitialized: boolean;
32
+ protected _remoteConfigResponsePromise?: Promise<PostHogRemoteConfig | undefined>;
28
33
  abstract fetch(url: string, options: PostHogFetchOptions): Promise<PostHogFetchResponse>;
29
34
  abstract getLibraryId(): string;
30
35
  abstract getLibraryVersion(): string;
@@ -58,6 +63,7 @@ export declare abstract class PostHogCoreStateless {
58
63
  *** GROUPS
59
64
  ***/
60
65
  protected groupIdentifyStateless(groupType: string, groupKey: string | number, groupProperties?: PostHogEventProperties, options?: PostHogCaptureOptions, distinctId?: string, eventProperties?: PostHogEventProperties): void;
66
+ protected getRemoteConfig(): Promise<PostHogRemoteConfig | undefined>;
61
67
  /***
62
68
  *** FEATURE FLAGS
63
69
  ***/
@@ -79,6 +85,10 @@ export declare abstract class PostHogCoreStateless {
79
85
  payloads: PostHogDecideResponse['featureFlagPayloads'] | undefined;
80
86
  requestId: PostHogDecideResponse['requestId'] | undefined;
81
87
  }>;
88
+ /***
89
+ *** SURVEYS
90
+ ***/
91
+ getSurveysStateless(): Promise<SurveyResponse['surveys']>;
82
92
  /***
83
93
  *** QUEUEING AND FLUSHING
84
94
  ***/
@@ -170,10 +180,13 @@ export declare abstract class PostHogCore extends PostHogCoreStateless {
170
180
  groupProperties(properties: {
171
181
  [type: string]: Record<string, string>;
172
182
  }): void;
183
+ private remoteConfigAsync;
173
184
  /***
174
185
  *** FEATURE FLAGS
175
186
  ***/
176
187
  private decideAsync;
188
+ private cacheSessionReplay;
189
+ private _remoteConfigAsync;
177
190
  private _decideAsync;
178
191
  private setKnownFeatureFlags;
179
192
  private setKnownFeatureFlagPayloads;
@@ -187,6 +200,7 @@ export declare abstract class PostHogCore extends PostHogCoreStateless {
187
200
  };
188
201
  isFeatureEnabled(key: string): boolean | undefined;
189
202
  reloadFeatureFlags(cb?: (err?: Error, flags?: PostHogDecideResponse['featureFlags']) => void): void;
203
+ reloadRemoteConfigAsync(): Promise<PostHogRemoteConfig | undefined>;
190
204
  reloadFeatureFlagsAsync(sendAnonDistinctId?: boolean): Promise<PostHogDecideResponse['featureFlags'] | undefined>;
191
205
  onFeatureFlags(cb: (flags: PostHogDecideResponse['featureFlags']) => void): () => void;
192
206
  onFeatureFlag(key: string, cb: (value: string | boolean) => void): () => void;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ import { Survey } from './surveys-types';
2
3
  export type PostHogCoreOptions = {
3
4
  /** PostHog API host, usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' */
4
5
  host?: string;
@@ -20,6 +21,18 @@ export type PostHogCoreOptions = {
20
21
  sendFeatureFlagEvent?: boolean;
21
22
  /** Whether to load feature flags when initialized or not */
22
23
  preloadFeatureFlags?: boolean;
24
+ /**
25
+ * Whether to load remote config when initialized or not
26
+ * Experimental support
27
+ * Default: false - Remote config is loaded by default
28
+ */
29
+ disableRemoteConfig?: boolean;
30
+ /**
31
+ * Whether to load surveys when initialized or not
32
+ * Experimental support
33
+ * Default: false - Surveys are loaded by default, but requires the `PostHogSurveyProvider` to be used
34
+ */
35
+ disableSurveys?: boolean;
23
36
  /** Option to bootstrap the library with given distinctId and feature flags */
24
37
  bootstrap?: {
25
38
  distinctId?: string;
@@ -35,6 +48,8 @@ export type PostHogCoreOptions = {
35
48
  requestTimeout?: number;
36
49
  /** Timeout in milliseconds for feature flag calls. Defaults to 10 seconds for stateful clients, and 3 seconds for stateless. */
37
50
  featureFlagsRequestTimeoutMs?: number;
51
+ /** Timeout in milliseconds for remote config calls. Defaults to 3 seconds. */
52
+ remoteConfigRequestTimeoutMs?: number;
38
53
  /** For Session Analysis how long before we expire a session (defaults to 30 mins) */
39
54
  sessionExpirationTimeSeconds?: number;
40
55
  /** Whether to post events to PostHog in JSON or compressed format. Defaults to 'json' */
@@ -61,7 +76,11 @@ export declare enum PostHogPersistedProperty {
61
76
  InstalledAppBuild = "installed_app_build",
62
77
  InstalledAppVersion = "installed_app_version",
63
78
  SessionReplay = "session_replay",
64
- DecideEndpointWasHit = "decide_endpoint_was_hit"
79
+ DecideEndpointWasHit = "decide_endpoint_was_hit",
80
+ SurveyLastSeenDate = "survey_last_seen_date",
81
+ SurveysSeen = "surveys_seen",
82
+ Surveys = "surveys",
83
+ RemoteConfig = "remote_config"
65
84
  }
66
85
  export type PostHogFetchOptions = {
67
86
  method: 'GET' | 'POST' | 'PUT' | 'PATCH';
@@ -102,16 +121,20 @@ export type PostHogAutocaptureElement = {
102
121
  } & {
103
122
  [key: string]: any;
104
123
  };
105
- export type PostHogDecideResponse = {
106
- config: {
107
- enable_collect_everything: boolean;
108
- };
109
- editorParams: {
110
- toolbarVersion: string;
111
- jsURL: string;
124
+ export interface PostHogRemoteConfig {
125
+ sessionRecording?: boolean | {
126
+ [key: string]: JsonType;
112
127
  };
113
- isAuthenticated: true;
114
- supportedCompression: string[];
128
+ /**
129
+ * Whether surveys are enabled
130
+ */
131
+ surveys?: boolean | Survey[];
132
+ /**
133
+ * Indicates if the team has any flags enabled (if not we don't need to load them)
134
+ */
135
+ hasFeatureFlags?: boolean;
136
+ }
137
+ export interface PostHogDecideResponse extends Omit<PostHogRemoteConfig, 'surveys' | 'hasFeatureFlags'> {
115
138
  featureFlags: {
116
139
  [key: string]: string | boolean;
117
140
  };
@@ -119,12 +142,12 @@ export type PostHogDecideResponse = {
119
142
  [key: string]: JsonType;
120
143
  };
121
144
  errorsWhileComputingFlags: boolean;
122
- quotaLimited?: string[];
123
145
  sessionRecording?: boolean | {
124
146
  [key: string]: JsonType;
125
147
  };
148
+ quotaLimited?: string[];
126
149
  requestId?: string;
127
- };
150
+ }
128
151
  export type PostHogFlagsAndPayloadsResponse = {
129
152
  featureFlags: PostHogDecideResponse['featureFlags'];
130
153
  featureFlagPayloads: PostHogDecideResponse['featureFlagPayloads'];
@@ -37,8 +37,8 @@ declare class FeatureFlagsPoller {
37
37
  customHeaders?: {
38
38
  [key: string]: string;
39
39
  };
40
- lastRequestWasAuthenticationError: boolean;
41
- authenticationErrorCount: number;
40
+ shouldBeginExponentialBackoff: boolean;
41
+ backOffCount: number;
42
42
  constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host, customHeaders, ...options }: FeatureFlagsPollerOptions);
43
43
  debug(enabled?: boolean): void;
44
44
  private logMsgIfDebug;
@@ -30,7 +30,7 @@ export declare class PostHog extends PostHogCoreStateless implements PostHogNode
30
30
  enable(): Promise<void>;
31
31
  disable(): Promise<void>;
32
32
  debug(enabled?: boolean): void;
33
- capture({ distinctId, event, properties, groups, sendFeatureFlags, timestamp, disableGeoip, uuid, }: EventMessage): void;
33
+ capture(props: EventMessage): void;
34
34
  identify({ distinctId, properties, disableGeoip }: IdentifyMessage): void;
35
35
  alias(data: {
36
36
  distinctId: string;
@@ -1,9 +1,10 @@
1
- export declare const apiImplementation: ({ localFlags, decideFlags, decideFlagPayloads, decideStatus, localFlagsStatus, }: {
1
+ export declare const apiImplementation: ({ localFlags, decideFlags, decideFlagPayloads, decideStatus, localFlagsStatus, errorsWhileComputingFlags, }: {
2
2
  localFlags?: any;
3
3
  decideFlags?: any;
4
4
  decideFlagPayloads?: any;
5
5
  decideStatus?: number | undefined;
6
6
  localFlagsStatus?: number | undefined;
7
+ errorsWhileComputingFlags?: boolean | undefined;
7
8
  }) => (url: any) => Promise<any>;
8
9
  export declare const anyLocalEvalCall: any[];
9
10
  export declare const anyDecideCall: any[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "4.10.0",
3
+ "version": "4.10.2",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -101,11 +101,18 @@ export function createEventProcessor(
101
101
 
102
102
  const exceptions: _SentryException[] = event.exception?.values || []
103
103
 
104
- exceptions.map((exception) => {
105
- if (exception.stacktrace) {
106
- exception.stacktrace.type = 'raw'
107
- }
108
- })
104
+ const exceptionList = exceptions.map((exception) => ({
105
+ ...exception,
106
+ stacktrace: exception.stacktrace
107
+ ? {
108
+ ...exception.stacktrace,
109
+ type: 'raw',
110
+ frames: (exception.stacktrace.frames || []).map((frame: any) => {
111
+ return { ...frame, platform: 'node:javascript' }
112
+ }),
113
+ }
114
+ : undefined,
115
+ }))
109
116
 
110
117
  const properties: SentryExceptionProperties & {
111
118
  // two properties added to match any exception auto-capture
@@ -121,7 +128,7 @@ export function createEventProcessor(
121
128
  $exception_type: exceptions[0]?.type,
122
129
  $exception_personURL: personUrl,
123
130
  $exception_level: event.level,
124
- $exception_list: exceptions,
131
+ $exception_list: exceptionList,
125
132
  // Sentry Exception Properties
126
133
  $sentry_event_id: event.event_id,
127
134
  $sentry_exception: event.exception,
@@ -58,8 +58,8 @@ class FeatureFlagsPoller {
58
58
  debugMode: boolean = false
59
59
  onError?: (error: Error) => void
60
60
  customHeaders?: { [key: string]: string }
61
- lastRequestWasAuthenticationError: boolean = false
62
- authenticationErrorCount: number = 0
61
+ shouldBeginExponentialBackoff: boolean = false
62
+ backOffCount: number = 0
63
63
 
64
64
  constructor({
65
65
  pollingInterval,
@@ -384,11 +384,11 @@ class FeatureFlagsPoller {
384
384
  * @returns The polling interval to use for the next request.
385
385
  */
386
386
  private getPollingInterval(): number {
387
- if (!this.lastRequestWasAuthenticationError) {
387
+ if (!this.shouldBeginExponentialBackoff) {
388
388
  return this.pollingInterval
389
389
  }
390
390
 
391
- return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.authenticationErrorCount)
391
+ return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.backOffCount)
392
392
  }
393
393
 
394
394
  async _loadFeatureFlags(): Promise<void> {
@@ -402,59 +402,86 @@ class FeatureFlagsPoller {
402
402
  try {
403
403
  const res = await this._requestFeatureFlagDefinitions()
404
404
 
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
- )
405
+ // Handle undefined res case, this shouldn't happen, but it doesn't hurt to handle it anyway
406
+ if (!res) {
407
+ // Don't override existing flags when something goes wrong
408
+ return
411
409
  }
412
410
 
413
- if (res && res.status === 403) {
414
- this.lastRequestWasAuthenticationError = true
415
- this.authenticationErrorCount += 1
416
- throw new ClientError(
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`
418
- )
419
- }
411
+ // NB ON ERROR HANDLING & `loadedSuccessfullyOnce`:
412
+ //
413
+ // `loadedSuccessfullyOnce` indicates we've successfully loaded a valid set of flags at least once.
414
+ // If we set it to `true` in an error scenario (e.g. 402 Over Quota, 401 Invalid Key, etc.),
415
+ // any manual call to `loadFeatureFlags()` (without forceReload) will skip refetching entirely,
416
+ // leaving us stuck with zero or outdated flags. The poller does keep running, but we also want
417
+ // manual reloads to be possible as soon as the error condition is resolved.
418
+ //
419
+ // Therefore, on error statuses, we do *not* set `loadedSuccessfullyOnce = true`, ensuring that
420
+ // both the background poller and any subsequent manual calls can keep trying to load flags
421
+ // once the issue (quota, permission, rate limit, etc.) is resolved.
422
+ switch (res.status) {
423
+ case 401:
424
+ // Invalid API key
425
+ this.shouldBeginExponentialBackoff = true
426
+ this.backOffCount += 1
427
+ throw new ClientError(
428
+ `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`
429
+ )
420
430
 
421
- if (res && res.status === 402) {
422
- // Quota limited - clear all flags
423
- console.warn(
424
- '[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts'
425
- )
426
- this.featureFlags = []
427
- this.featureFlagsByKey = {}
428
- this.groupTypeMapping = {}
429
- this.cohorts = {}
430
- this.loadedSuccessfullyOnce = false
431
- return
432
- }
431
+ case 402:
432
+ // Quota exceeded - clear all flags
433
+ console.warn(
434
+ '[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts'
435
+ )
436
+ this.featureFlags = []
437
+ this.featureFlagsByKey = {}
438
+ this.groupTypeMapping = {}
439
+ this.cohorts = {}
440
+ return
441
+
442
+ case 403:
443
+ // Permissions issue
444
+ this.shouldBeginExponentialBackoff = true
445
+ this.backOffCount += 1
446
+ throw new ClientError(
447
+ `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`
448
+ )
433
449
 
434
- if (res && res.status !== 200) {
435
- // something else went wrong, or the server is down.
436
- // In this case, don't override existing flags
437
- return
438
- }
450
+ case 429:
451
+ // Rate limited
452
+ this.shouldBeginExponentialBackoff = true
453
+ this.backOffCount += 1
454
+ throw new ClientError(
455
+ `You are being rate limited. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`
456
+ )
439
457
 
440
- const responseJson = await res.json()
441
- if (!('flags' in responseJson)) {
442
- this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`))
443
- }
458
+ case 200: {
459
+ // Process successful response
460
+ const responseJson = await res.json()
461
+ if (!('flags' in responseJson)) {
462
+ this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`))
463
+ return
464
+ }
444
465
 
445
- this.featureFlags = responseJson.flags || []
446
- this.featureFlagsByKey = this.featureFlags.reduce(
447
- (acc, curr) => ((acc[curr.key] = curr), acc),
448
- <Record<string, PostHogFeatureFlag>>{}
449
- )
450
- this.groupTypeMapping = responseJson.group_type_mapping || {}
451
- this.cohorts = responseJson.cohorts || []
452
- this.loadedSuccessfullyOnce = true
453
- this.lastRequestWasAuthenticationError = false
454
- this.authenticationErrorCount = 0
466
+ this.featureFlags = responseJson.flags || []
467
+ this.featureFlagsByKey = this.featureFlags.reduce(
468
+ (acc, curr) => ((acc[curr.key] = curr), acc),
469
+ <Record<string, PostHogFeatureFlag>>{}
470
+ )
471
+ this.groupTypeMapping = responseJson.group_type_mapping || {}
472
+ this.cohorts = responseJson.cohorts || {}
473
+ this.loadedSuccessfullyOnce = true
474
+ this.shouldBeginExponentialBackoff = false
475
+ this.backOffCount = 0
476
+ break
477
+ }
478
+
479
+ default:
480
+ // Something else went wrong, or the server is down.
481
+ // In this case, don't override existing flags
482
+ return
483
+ }
455
484
  } catch (err) {
456
- // if an error that is not an instance of ClientError is thrown
457
- // we silently ignore the error when reloading feature flags
458
485
  if (err instanceof ClientError) {
459
486
  this.onError?.(err)
460
487
  }
@@ -116,16 +116,14 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
116
116
  this.featureFlagsPoller?.debug(enabled)
117
117
  }
118
118
 
119
- capture({
120
- distinctId,
121
- event,
122
- properties,
123
- groups,
124
- sendFeatureFlags,
125
- timestamp,
126
- disableGeoip,
127
- uuid,
128
- }: EventMessage): void {
119
+ capture(props: EventMessage): void {
120
+ if (typeof props === 'string') {
121
+ this.logMsgIfDebug(() =>
122
+ console.warn('Called capture() with a string as the first argument when an object was expected.')
123
+ )
124
+ }
125
+ const { distinctId, event, properties, groups, sendFeatureFlags, timestamp, disableGeoip, uuid }: EventMessage =
126
+ props
129
127
  const _capture = (props: EventMessage['properties']): void => {
130
128
  super.captureStateless(distinctId, event, props, { timestamp, disableGeoip, uuid })
131
129
  }
@@ -138,7 +138,7 @@ describe('PostHogSentryIntegration', () => {
138
138
  {
139
139
  type: 'Error',
140
140
  value: 'example error',
141
- stacktrace: { frames: [], type: 'raw' },
141
+ stacktrace: { frames: [] },
142
142
  mechanism: { type: 'generic', handled: true },
143
143
  },
144
144
  ],
@@ -328,6 +328,17 @@ describe('PostHog Node.js', () => {
328
328
 
329
329
  await client.shutdown()
330
330
  })
331
+
332
+ it('should warn if capture is called with a string', () => {
333
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
334
+ posthog.debug(true)
335
+ // @ts-expect-error - Testing the warning when passing a string instead of an object
336
+ posthog.capture('test-event')
337
+ expect(warnSpy).toHaveBeenCalledWith(
338
+ 'Called capture() with a string as the first argument when an object was expected.'
339
+ )
340
+ warnSpy.mockRestore()
341
+ })
331
342
  })
332
343
 
333
344
  describe('shutdown', () => {
@@ -1308,5 +1319,25 @@ describe('PostHog Node.js', () => {
1308
1319
  })
1309
1320
  )
1310
1321
  })
1322
+
1323
+ it('should log error when decide response has errors', async () => {
1324
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
1325
+
1326
+ mockedFetch.mockImplementation(
1327
+ apiImplementation({
1328
+ decideFlags: { 'feature-1': true },
1329
+ decideFlagPayloads: {},
1330
+ errorsWhileComputingFlags: true,
1331
+ })
1332
+ )
1333
+
1334
+ await posthog.getFeatureFlag('feature-1', '123')
1335
+
1336
+ expect(errorSpy).toHaveBeenCalledWith(
1337
+ '[FEATURE FLAGS] Error while computing feature flags, some flags may be missing or incorrect. Learn more at https://posthog.com/docs/feature-flags/best-practices'
1338
+ )
1339
+
1340
+ errorSpy.mockRestore()
1341
+ })
1311
1342
  })
1312
1343
  })
@@ -4,12 +4,14 @@ export const apiImplementation = ({
4
4
  decideFlagPayloads,
5
5
  decideStatus = 200,
6
6
  localFlagsStatus = 200,
7
+ errorsWhileComputingFlags = false,
7
8
  }: {
8
9
  localFlags?: any
9
10
  decideFlags?: any
10
11
  decideFlagPayloads?: any
11
12
  decideStatus?: number
12
13
  localFlagsStatus?: number
14
+ errorsWhileComputingFlags?: boolean
13
15
  }) => {
14
16
  return (url: any): Promise<any> => {
15
17
  if ((url as any).includes('/decide/')) {
@@ -25,6 +27,7 @@ export const apiImplementation = ({
25
27
  featureFlagPayloads: Object.fromEntries(
26
28
  Object.entries(decideFlagPayloads || {}).map(([k, v]) => [k, JSON.stringify(v)])
27
29
  ),
30
+ errorsWhileComputingFlags,
28
31
  })
29
32
  }
30
33
  },