posthog-node 4.14.0 → 4.16.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.
@@ -3,6 +3,7 @@ import { RetriableOptions } from './utils';
3
3
  import { LZString } from './lz-string';
4
4
  import { SimpleEventEmitter } from './eventemitter';
5
5
  export * as utils from './utils';
6
+ export declare function logFlushError(err: any): Promise<void>;
6
7
  export declare abstract class PostHogCoreStateless {
7
8
  readonly apiKey: string;
8
9
  readonly host: string;
@@ -13,6 +14,7 @@ export declare abstract class PostHogCoreStateless {
13
14
  private maxQueueSize;
14
15
  private flushInterval;
15
16
  private flushPromise;
17
+ private shutdownPromise;
16
18
  private requestTimeout;
17
19
  private featureFlagsRequestTimeoutMs;
18
20
  private remoteConfigRequestTimeoutMs;
@@ -52,12 +54,19 @@ export declare abstract class PostHogCoreStateless {
52
54
  *** TRACKING
53
55
  ***/
54
56
  protected identifyStateless(distinctId: string, properties?: PostHogEventProperties, options?: PostHogCaptureOptions): void;
57
+ protected identifyStatelessImmediate(distinctId: string, properties?: PostHogEventProperties, options?: PostHogCaptureOptions): Promise<void>;
55
58
  protected captureStateless(distinctId: string, event: string, properties?: {
56
59
  [key: string]: any;
57
60
  }, options?: PostHogCaptureOptions): void;
61
+ protected captureStatelessImmediate(distinctId: string, event: string, properties?: {
62
+ [key: string]: any;
63
+ }, options?: PostHogCaptureOptions): Promise<void>;
58
64
  protected aliasStateless(alias: string, distinctId: string, properties?: {
59
65
  [key: string]: any;
60
66
  }, options?: PostHogCaptureOptions): void;
67
+ protected aliasStatelessImmediate(alias: string, distinctId: string, properties?: {
68
+ [key: string]: any;
69
+ }, options?: PostHogCaptureOptions): Promise<void>;
61
70
  /***
62
71
  *** GROUPS
63
72
  ***/
@@ -106,6 +115,8 @@ export declare abstract class PostHogCoreStateless {
106
115
  *** QUEUEING AND FLUSHING
107
116
  ***/
108
117
  protected enqueue(type: string, _message: any, options?: PostHogCaptureOptions): void;
118
+ protected sendImmediate(type: string, _message: any, options?: PostHogCaptureOptions): Promise<void>;
119
+ private prepareMessage;
109
120
  private clearFlushTimer;
110
121
  /**
111
122
  * Helper for flushing the queue in the background
@@ -118,6 +129,12 @@ export declare abstract class PostHogCoreStateless {
118
129
  };
119
130
  private _flush;
120
131
  private fetchWithRetry;
132
+ _shutdown(shutdownTimeoutMs?: number): Promise<void>;
133
+ /**
134
+ * Call shutdown() once before the node process exits, so ensure that all events have been sent and all promises
135
+ * have resolved. Do not use this function if you intend to keep using this PostHog instance after calling it.
136
+ * @param shutdownTimeoutMs
137
+ */
121
138
  shutdown(shutdownTimeoutMs?: number): Promise<void>;
122
139
  }
123
140
  export declare abstract class PostHogCore extends PostHogCoreStateless {
@@ -1,12 +1,13 @@
1
1
  import { FetchLike } from './types';
2
2
  export declare const NEW_FLAGS_ROLLOUT_PERCENTAGE = 1;
3
3
  export declare const NEW_FLAGS_EXCLUDED_HASHES: Set<string>;
4
+ export declare const STRING_FORMAT = "utf8";
4
5
  export declare function assert(truthyValue: any, message: string): void;
5
6
  export declare function removeTrailingSlash(url: string): string;
6
7
  export interface RetriableOptions {
7
8
  retryCount: number;
8
9
  retryDelay: number;
9
- retryCheck: (err: any) => boolean;
10
+ retryCheck: (err: unknown) => boolean;
10
11
  }
11
12
  export declare function retriable<T>(fn: () => Promise<T>, props: RetriableOptions): Promise<T>;
12
13
  export declare function currentTimestamp(): number;
@@ -32,12 +32,19 @@ export declare class PostHog extends PostHogCoreStateless implements PostHogNode
32
32
  disable(): Promise<void>;
33
33
  debug(enabled?: boolean): void;
34
34
  capture(props: EventMessage): void;
35
+ captureImmediate(props: EventMessage): Promise<void>;
35
36
  identify({ distinctId, properties, disableGeoip }: IdentifyMessage): void;
37
+ identifyImmediate({ distinctId, properties, disableGeoip }: IdentifyMessage): Promise<void>;
36
38
  alias(data: {
37
39
  distinctId: string;
38
40
  alias: string;
39
41
  disableGeoip?: boolean;
40
42
  }): void;
43
+ aliasImmediate(data: {
44
+ distinctId: string;
45
+ alias: string;
46
+ disableGeoip?: boolean;
47
+ }): Promise<void>;
41
48
  isLocalEvaluationReady(): boolean;
42
49
  waitForLocalEvaluationReady(timeoutMs?: number): Promise<boolean>;
43
50
  getFeatureFlag(key: string, distinctId: string, options?: {
@@ -85,7 +92,7 @@ export declare class PostHog extends PostHogCoreStateless implements PostHogNode
85
92
  * This is useful to call if you want to ensure that the feature flags are up to date before calling getFeatureFlag.
86
93
  */
87
94
  reloadFeatureFlags(): Promise<void>;
88
- shutdown(shutdownTimeoutMs?: number): Promise<void>;
95
+ _shutdown(shutdownTimeoutMs?: number): Promise<void>;
89
96
  private addLocalPersonAndGroupProperties;
90
97
  captureException(error: unknown, distinctId?: string, additionalProperties?: Record<string | number, any>): void;
91
98
  }
@@ -70,6 +70,15 @@ export type PostHogNodeV1 = {
70
70
  * @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event.
71
71
  */
72
72
  capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessage): void;
73
+ /**
74
+ * @description Capture an event immediately. Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
75
+ * @param distinctId which uniquely identifies your user
76
+ * @param event We recommend using [verb] [noun], like movie played or movie updated to easily identify what your events mean later on.
77
+ * @param properties OPTIONAL | which can be a object with any information you'd like to add
78
+ * @param groups OPTIONAL | object of what groups are related to this event, example: { company: 'id:5' }. Can be used to analyze companies instead of users.
79
+ * @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event.
80
+ */
81
+ captureImmediate({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessage): Promise<void>;
73
82
  /**
74
83
  * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog,
75
84
  * and even do things like segment users by these properties.
@@ -78,6 +87,13 @@ export type PostHogNodeV1 = {
78
87
  * @param properties with a dict with any key: value pairs
79
88
  */
80
89
  identify({ distinctId, properties }: IdentifyMessage): void;
90
+ /**
91
+ * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog.
92
+ * Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
93
+ * @param distinctId which uniquely identifies your user
94
+ * @param properties with a dict with any key: value pairs
95
+ */
96
+ identifyImmediate({ distinctId, properties }: IdentifyMessage): Promise<void>;
81
97
  /**
82
98
  * @description To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call.
83
99
  * This will allow you to answer questions like "Which marketing channels leads to users churning after a month?"
@@ -93,6 +109,16 @@ export type PostHogNodeV1 = {
93
109
  distinctId: string;
94
110
  alias: string;
95
111
  }): void;
112
+ /**
113
+ * @description To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call.
114
+ * Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
115
+ * @param distinctId the current unique id
116
+ * @param alias the unique ID of the user before
117
+ */
118
+ aliasImmediate(data: {
119
+ distinctId: string;
120
+ alias: string;
121
+ }): Promise<void>;
96
122
  /**
97
123
  * @description PostHog feature flags (https://posthog.com/docs/features/feature-flags)
98
124
  * allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog,
@@ -14,4 +14,5 @@ export declare const apiImplementation: ({ localFlags, decideFlags, decideFlagPa
14
14
  }) => (url: any) => Promise<any>;
15
15
  export declare const anyLocalEvalCall: any[];
16
16
  export declare const anyDecideCall: any[];
17
+ export declare const isPending: (promise: Promise<any>) => boolean;
17
18
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "4.14.0",
3
+ "version": "4.16.0",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -199,6 +199,84 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
199
199
  this.addPendingPromise(capturePromise)
200
200
  }
201
201
 
202
+ async captureImmediate(props: EventMessage): Promise<void> {
203
+ if (typeof props === 'string') {
204
+ this.logMsgIfDebug(() =>
205
+ console.warn('Called capture() with a string as the first argument when an object was expected.')
206
+ )
207
+ }
208
+ const { distinctId, event, properties, groups, sendFeatureFlags, timestamp, disableGeoip, uuid }: EventMessage =
209
+ props
210
+
211
+ const _capture = (props: EventMessage['properties']): Promise<void> => {
212
+ return super.captureStatelessImmediate(distinctId, event, props, { timestamp, disableGeoip, uuid })
213
+ }
214
+
215
+ const _getFlags = async (
216
+ distinctId: EventMessage['distinctId'],
217
+ groups: EventMessage['groups'],
218
+ disableGeoip: EventMessage['disableGeoip']
219
+ ): Promise<PostHogDecideResponse['featureFlags'] | undefined> => {
220
+ return (await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)).flags
221
+ }
222
+
223
+ const capturePromise = Promise.resolve()
224
+ .then(async () => {
225
+ if (sendFeatureFlags) {
226
+ // If we are sending feature flags, we need to make sure we have the latest flags
227
+ // return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
228
+ return await _getFlags(distinctId, groups, disableGeoip)
229
+ }
230
+
231
+ if (event === '$feature_flag_called') {
232
+ // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
233
+ return {}
234
+ }
235
+
236
+ if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
237
+ // Otherwise we may as well check for the flags locally and include them if they are already loaded
238
+ const groupsWithStringValues: Record<string, string> = {}
239
+ for (const [key, value] of Object.entries(groups || {})) {
240
+ groupsWithStringValues[key] = String(value)
241
+ }
242
+
243
+ return await this.getAllFlags(distinctId, {
244
+ groups: groupsWithStringValues,
245
+ disableGeoip,
246
+ onlyEvaluateLocally: true,
247
+ })
248
+ }
249
+ return {}
250
+ })
251
+ .then((flags) => {
252
+ // Derive the relevant flag properties to add
253
+ const additionalProperties: Record<string, any> = {}
254
+ if (flags) {
255
+ for (const [feature, variant] of Object.entries(flags)) {
256
+ additionalProperties[`$feature/${feature}`] = variant
257
+ }
258
+ }
259
+ const activeFlags = Object.keys(flags || {})
260
+ .filter((flag) => flags?.[flag] !== false)
261
+ .sort()
262
+ if (activeFlags.length > 0) {
263
+ additionalProperties['$active_feature_flags'] = activeFlags
264
+ }
265
+
266
+ return additionalProperties
267
+ })
268
+ .catch(() => {
269
+ // Something went wrong getting the flag info - we should capture the event anyways
270
+ return {}
271
+ })
272
+ .then((additionalProperties) => {
273
+ // No matter what - capture the event
274
+ _capture({ ...additionalProperties, ...properties, $groups: groups })
275
+ })
276
+
277
+ await capturePromise
278
+ }
279
+
202
280
  identify({ distinctId, properties, disableGeoip }: IdentifyMessage): void {
203
281
  // Catch properties passed as $set and move them to the top level
204
282
 
@@ -219,10 +297,32 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
219
297
  )
220
298
  }
221
299
 
300
+ async identifyImmediate({ distinctId, properties, disableGeoip }: IdentifyMessage): Promise<void> {
301
+ // promote $set and $set_once to top level
302
+ const userPropsOnce = properties?.$set_once
303
+ delete properties?.$set_once
304
+
305
+ // if no $set is provided we assume all properties are $set
306
+ const userProps = properties?.$set || properties
307
+
308
+ await super.identifyStatelessImmediate(
309
+ distinctId,
310
+ {
311
+ $set: userProps,
312
+ $set_once: userPropsOnce,
313
+ },
314
+ { disableGeoip }
315
+ )
316
+ }
317
+
222
318
  alias(data: { distinctId: string; alias: string; disableGeoip?: boolean }): void {
223
319
  super.aliasStateless(data.alias, data.distinctId, undefined, { disableGeoip: data.disableGeoip })
224
320
  }
225
321
 
322
+ async aliasImmediate(data: { distinctId: string; alias: string; disableGeoip?: boolean }): Promise<void> {
323
+ await super.aliasStatelessImmediate(data.alias, data.distinctId, undefined, { disableGeoip: data.disableGeoip })
324
+ }
325
+
226
326
  isLocalEvaluationReady(): boolean {
227
327
  return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false
228
328
  }
@@ -535,9 +635,9 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
535
635
  await this.featureFlagsPoller?.loadFeatureFlags(true)
536
636
  }
537
637
 
538
- async shutdown(shutdownTimeoutMs?: number): Promise<void> {
638
+ async _shutdown(shutdownTimeoutMs?: number): Promise<void> {
539
639
  this.featureFlagsPoller?.stopPoller()
540
- return super.shutdown(shutdownTimeoutMs)
640
+ return super._shutdown(shutdownTimeoutMs)
541
641
  }
542
642
 
543
643
  private addLocalPersonAndGroupProperties(
package/src/types.ts CHANGED
@@ -79,6 +79,16 @@ export type PostHogNodeV1 = {
79
79
  */
80
80
  capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessage): void
81
81
 
82
+ /**
83
+ * @description Capture an event immediately. Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
84
+ * @param distinctId which uniquely identifies your user
85
+ * @param event We recommend using [verb] [noun], like movie played or movie updated to easily identify what your events mean later on.
86
+ * @param properties OPTIONAL | which can be a object with any information you'd like to add
87
+ * @param groups OPTIONAL | object of what groups are related to this event, example: { company: 'id:5' }. Can be used to analyze companies instead of users.
88
+ * @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event.
89
+ */
90
+ captureImmediate({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessage): Promise<void>
91
+
82
92
  /**
83
93
  * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog,
84
94
  * and even do things like segment users by these properties.
@@ -88,6 +98,14 @@ export type PostHogNodeV1 = {
88
98
  */
89
99
  identify({ distinctId, properties }: IdentifyMessage): void
90
100
 
101
+ /**
102
+ * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog.
103
+ * Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
104
+ * @param distinctId which uniquely identifies your user
105
+ * @param properties with a dict with any key: value pairs
106
+ */
107
+ identifyImmediate({ distinctId, properties }: IdentifyMessage): Promise<void>
108
+
91
109
  /**
92
110
  * @description To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call.
93
111
  * This will allow you to answer questions like "Which marketing channels leads to users churning after a month?"
@@ -101,6 +119,14 @@ export type PostHogNodeV1 = {
101
119
  */
102
120
  alias(data: { distinctId: string; alias: string }): void
103
121
 
122
+ /**
123
+ * @description To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call.
124
+ * Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
125
+ * @param distinctId the current unique id
126
+ * @param alias the unique ID of the user before
127
+ */
128
+ aliasImmediate(data: { distinctId: string; alias: string }): Promise<void>
129
+
104
130
  /**
105
131
  * @description PostHog feature flags (https://posthog.com/docs/features/feature-flags)
106
132
  * allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog,
@@ -1,6 +1,6 @@
1
1
  import { MINIMUM_POLLING_INTERVAL, PostHog as PostHog, THIRTY_SECONDS } from '../src/posthog-node'
2
2
  import fetch from '../src/fetch'
3
- import { anyDecideCall, anyLocalEvalCall, apiImplementation } from './test-utils'
3
+ import { anyDecideCall, anyLocalEvalCall, apiImplementation, isPending } from './test-utils'
4
4
  import { waitForPromises, wait } from '../../posthog-core/test/test-utils/test-utils'
5
5
  import { randomUUID } from 'crypto'
6
6
  jest.mock('../src/fetch')
@@ -381,33 +381,31 @@ describe('PostHog Node.js', () => {
381
381
  })
382
382
  ph.debug(true)
383
383
 
384
- // using debug mode to check console.log output
385
- // which tells us when the flush is complete
384
+ ph.capture({ event: 'test-event-1', distinctId: '123' })
386
385
 
387
- ph.capture({ event: 'test-event', distinctId: '123' })
388
- await wait(100)
389
- expect(logSpy).toHaveBeenCalledTimes(1)
386
+ // start flushing, but don't wait for promise to resolve before resuming events
387
+ const flushPromise = ph.flush()
388
+ expect(isPending(flushPromise)).toEqual(true)
390
389
 
391
- ph.capture({ event: 'test-event', distinctId: '123' })
392
- ph.capture({ event: 'test-event', distinctId: '123' })
393
- await wait(100)
394
- expect(logSpy).toHaveBeenCalledTimes(3)
395
- await wait(400) // The flush will resolve in this time
396
- ph.capture({ event: 'test-event', distinctId: '123' })
397
- ph.capture({ event: 'test-event', distinctId: '123' })
398
- await wait(100)
399
- expect(logSpy).toHaveBeenCalledTimes(6) // 5 captures and 1 flush
400
- expect(5).toEqual(logSpy.mock.calls.filter((call) => call[1].includes('capture')).length)
401
- expect(1).toEqual(logSpy.mock.calls.filter((call) => call[1].includes('flush')).length)
390
+ ph.capture({ event: 'test-event-2', distinctId: '123' })
402
391
 
403
- logSpy.mockClear()
404
- expect(logSpy).toHaveBeenCalledTimes(0)
392
+ // start shutdown, but don't wait for promise to resolve before resuming events
393
+ const shutdownPromise = ph.shutdown()
405
394
 
406
- console.warn('YOO!!')
395
+ ph.capture({ event: 'test-event-3', distinctId: '123' })
396
+
397
+ // wait for shutdown to finish
398
+ await shutdownPromise
399
+ expect(isPending(flushPromise)).toEqual(false)
400
+
401
+ expect(3).toEqual(logSpy.mock.calls.filter((call) => call[1].includes('capture')).length)
402
+ const flushedEvents = logSpy.mock.calls.filter((call) => call[1].includes('flush')).flatMap((flush) => flush[2])
403
+ expect(flushedEvents).toMatchObject([
404
+ { event: 'test-event-1' },
405
+ { event: 'test-event-2' },
406
+ { event: 'test-event-3' },
407
+ ])
407
408
 
408
- await ph.shutdown()
409
- // 1 final flush for the events that were queued during shutdown
410
- expect(1).toEqual(logSpy.mock.calls.filter((call) => call[1].includes('flush')).length)
411
409
  logSpy.mockRestore()
412
410
  warnSpy.mockRestore()
413
411
  })
@@ -1,4 +1,5 @@
1
1
  import { PostHogV4DecideResponse } from 'posthog-core/src/types'
2
+ import util from 'util'
2
3
 
3
4
  type ErrorResponse = {
4
5
  status: number
@@ -104,3 +105,7 @@ export const anyLocalEvalCall = [
104
105
  expect.any(Object),
105
106
  ]
106
107
  export const anyDecideCall = ['http://example.com/flags/?v=2', expect.any(Object)]
108
+
109
+ export const isPending = (promise: Promise<any>): boolean => {
110
+ return util.inspect(promise).includes('pending')
111
+ }