posthog-node 5.8.8 → 5.9.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.
Files changed (129) hide show
  1. package/dist/{index.d.ts → client.d.ts} +7 -378
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +481 -0
  4. package/dist/client.mjs +437 -0
  5. package/dist/entrypoints/index.edge.d.ts +6 -0
  6. package/dist/entrypoints/index.edge.d.ts.map +1 -0
  7. package/dist/entrypoints/index.edge.js +89 -0
  8. package/dist/entrypoints/index.edge.mjs +12 -0
  9. package/dist/entrypoints/index.node.d.ts +6 -0
  10. package/dist/entrypoints/index.node.d.ts.map +1 -0
  11. package/dist/entrypoints/index.node.js +99 -0
  12. package/dist/entrypoints/index.node.mjs +16 -0
  13. package/dist/exports.d.ts +4 -0
  14. package/dist/exports.d.ts.map +1 -0
  15. package/dist/exports.js +78 -0
  16. package/dist/exports.mjs +3 -0
  17. package/dist/extensions/error-tracking/autocapture.d.ts +4 -0
  18. package/dist/extensions/error-tracking/autocapture.d.ts.map +1 -0
  19. package/dist/extensions/error-tracking/autocapture.js +68 -0
  20. package/dist/extensions/error-tracking/autocapture.mjs +31 -0
  21. package/dist/extensions/error-tracking/chunk-ids.d.ts +5 -0
  22. package/dist/extensions/error-tracking/chunk-ids.d.ts.map +1 -0
  23. package/dist/extensions/error-tracking/chunk-ids.js +68 -0
  24. package/dist/extensions/error-tracking/chunk-ids.mjs +34 -0
  25. package/dist/extensions/error-tracking/context-lines.node.d.ts +5 -0
  26. package/dist/extensions/error-tracking/context-lines.node.d.ts.map +1 -0
  27. package/dist/extensions/error-tracking/context-lines.node.js +227 -0
  28. package/dist/extensions/error-tracking/context-lines.node.mjs +187 -0
  29. package/dist/extensions/error-tracking/error-conversion.d.ts +4 -0
  30. package/dist/extensions/error-tracking/error-conversion.d.ts.map +1 -0
  31. package/dist/extensions/error-tracking/error-conversion.js +183 -0
  32. package/dist/extensions/error-tracking/error-conversion.mjs +146 -0
  33. package/dist/extensions/error-tracking/get-module.node.d.ts +3 -0
  34. package/dist/extensions/error-tracking/get-module.node.d.ts.map +1 -0
  35. package/dist/extensions/error-tracking/get-module.node.js +57 -0
  36. package/dist/extensions/error-tracking/get-module.node.mjs +23 -0
  37. package/dist/extensions/error-tracking/index.d.ts +20 -0
  38. package/dist/extensions/error-tracking/index.d.ts.map +1 -0
  39. package/dist/extensions/error-tracking/index.js +97 -0
  40. package/dist/extensions/error-tracking/index.mjs +63 -0
  41. package/dist/extensions/error-tracking/reduceable-cache.d.ts +13 -0
  42. package/dist/extensions/error-tracking/reduceable-cache.d.ts.map +1 -0
  43. package/dist/extensions/error-tracking/reduceable-cache.js +57 -0
  44. package/dist/extensions/error-tracking/reduceable-cache.mjs +23 -0
  45. package/dist/extensions/error-tracking/stack-parser.d.ts +3 -0
  46. package/dist/extensions/error-tracking/stack-parser.d.ts.map +1 -0
  47. package/dist/extensions/error-tracking/stack-parser.js +148 -0
  48. package/dist/extensions/error-tracking/stack-parser.mjs +114 -0
  49. package/dist/extensions/error-tracking/type-checking.d.ts +8 -0
  50. package/dist/extensions/error-tracking/type-checking.d.ts.map +1 -0
  51. package/dist/extensions/error-tracking/type-checking.js +80 -0
  52. package/dist/extensions/error-tracking/type-checking.mjs +31 -0
  53. package/dist/extensions/error-tracking/types.d.ts +61 -0
  54. package/dist/extensions/error-tracking/types.d.ts.map +1 -0
  55. package/dist/extensions/error-tracking/types.js +43 -0
  56. package/dist/extensions/error-tracking/types.mjs +9 -0
  57. package/dist/extensions/express.d.ts +17 -0
  58. package/dist/extensions/express.d.ts.map +1 -0
  59. package/dist/extensions/express.js +61 -0
  60. package/dist/extensions/express.mjs +17 -0
  61. package/dist/extensions/feature-flags/crypto-helpers.d.ts +3 -0
  62. package/dist/extensions/feature-flags/crypto-helpers.d.ts.map +1 -0
  63. package/dist/extensions/feature-flags/crypto-helpers.js +77 -0
  64. package/dist/extensions/feature-flags/crypto-helpers.mjs +22 -0
  65. package/dist/extensions/feature-flags/crypto.d.ts +2 -0
  66. package/dist/extensions/feature-flags/crypto.d.ts.map +1 -0
  67. package/dist/extensions/feature-flags/crypto.js +47 -0
  68. package/dist/extensions/feature-flags/crypto.mjs +13 -0
  69. package/dist/extensions/feature-flags/feature-flags.d.ts +89 -0
  70. package/dist/extensions/feature-flags/feature-flags.d.ts.map +1 -0
  71. package/dist/extensions/feature-flags/feature-flags.js +529 -0
  72. package/dist/extensions/feature-flags/feature-flags.mjs +483 -0
  73. package/dist/extensions/feature-flags/lazy.d.ts +24 -0
  74. package/dist/extensions/feature-flags/lazy.d.ts.map +1 -0
  75. package/dist/extensions/feature-flags/lazy.js +60 -0
  76. package/dist/extensions/feature-flags/lazy.mjs +26 -0
  77. package/dist/extensions/sentry-integration.d.ts +54 -0
  78. package/dist/extensions/sentry-integration.d.ts.map +1 -0
  79. package/dist/extensions/sentry-integration.js +113 -0
  80. package/dist/extensions/sentry-integration.mjs +73 -0
  81. package/dist/storage-memory.d.ts +7 -0
  82. package/dist/storage-memory.d.ts.map +1 -0
  83. package/dist/storage-memory.js +46 -0
  84. package/dist/storage-memory.mjs +12 -0
  85. package/dist/types.d.ts +253 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +18 -0
  88. package/dist/types.mjs +0 -0
  89. package/dist/utils/logger.d.ts +3 -0
  90. package/dist/utils/logger.d.ts.map +1 -0
  91. package/dist/utils/logger.js +63 -0
  92. package/dist/utils/logger.mjs +29 -0
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/dist/version.js +36 -0
  96. package/dist/version.mjs +2 -0
  97. package/package.json +32 -31
  98. package/src/client.ts +1532 -0
  99. package/src/entrypoints/index.edge.ts +15 -0
  100. package/src/entrypoints/index.node.ts +17 -0
  101. package/src/exports.ts +3 -0
  102. package/src/extensions/error-tracking/autocapture.ts +65 -0
  103. package/src/extensions/error-tracking/chunk-ids.ts +58 -0
  104. package/src/extensions/error-tracking/context-lines.node.ts +392 -0
  105. package/src/extensions/error-tracking/error-conversion.ts +291 -0
  106. package/src/extensions/error-tracking/get-module.node.ts +57 -0
  107. package/src/extensions/error-tracking/index.ts +103 -0
  108. package/src/extensions/error-tracking/reduceable-cache.ts +39 -0
  109. package/src/extensions/error-tracking/stack-parser.ts +212 -0
  110. package/src/extensions/error-tracking/type-checking.ts +40 -0
  111. package/src/extensions/error-tracking/types.ts +71 -0
  112. package/src/extensions/express.ts +39 -0
  113. package/src/extensions/feature-flags/crypto-helpers.ts +36 -0
  114. package/src/extensions/feature-flags/crypto.ts +22 -0
  115. package/src/extensions/feature-flags/feature-flags.ts +1003 -0
  116. package/src/extensions/feature-flags/lazy.ts +55 -0
  117. package/src/extensions/sentry-integration.ts +216 -0
  118. package/src/storage-memory.ts +13 -0
  119. package/src/types.ts +294 -0
  120. package/src/utils/logger.ts +39 -0
  121. package/src/version.ts +1 -0
  122. package/dist/edge/index.cjs +0 -3150
  123. package/dist/edge/index.cjs.map +0 -1
  124. package/dist/edge/index.mjs +0 -3144
  125. package/dist/edge/index.mjs.map +0 -1
  126. package/dist/node/index.cjs +0 -3556
  127. package/dist/node/index.cjs.map +0 -1
  128. package/dist/node/index.mjs +0 -3550
  129. package/dist/node/index.mjs.map +0 -1
package/src/client.ts ADDED
@@ -0,0 +1,1532 @@
1
+ import { version } from './version'
2
+
3
+ import {
4
+ JsonType,
5
+ PostHogCoreStateless,
6
+ PostHogFlagsResponse,
7
+ PostHogFetchOptions,
8
+ PostHogFetchResponse,
9
+ PostHogFlagsAndPayloadsResponse,
10
+ PostHogPersistedProperty,
11
+ Logger,
12
+ PostHogCaptureOptions,
13
+ } from '@posthog/core'
14
+ import {
15
+ EventMessage,
16
+ GroupIdentifyMessage,
17
+ IdentifyMessage,
18
+ IPostHog,
19
+ PostHogOptions,
20
+ SendFeatureFlagsOptions,
21
+ } from './types'
22
+ import { FeatureFlagDetail, FeatureFlagValue, getFeatureFlagValue } from '@posthog/core'
23
+ import { FeatureFlagsPoller } from './extensions/feature-flags/feature-flags'
24
+ import ErrorTracking from './extensions/error-tracking'
25
+ import { isPlainObject } from './extensions/error-tracking/type-checking'
26
+ import { safeSetTimeout, PostHogEventProperties } from '@posthog/core'
27
+ import { PostHogMemoryStorage } from './storage-memory'
28
+ import { createLogger } from './utils/logger'
29
+
30
+ // Standard local evaluation rate limit is 600 per minute (10 per second),
31
+ // so the fastest a poller should ever be set is 100ms.
32
+ const MINIMUM_POLLING_INTERVAL = 100
33
+ const THIRTY_SECONDS = 30 * 1000
34
+ const MAX_CACHE_SIZE = 50 * 1000
35
+
36
+ // The actual exported Nodejs API.
37
+ export abstract class PostHogBackendClient extends PostHogCoreStateless implements IPostHog {
38
+ private _memoryStorage = new PostHogMemoryStorage()
39
+
40
+ private featureFlagsPoller?: FeatureFlagsPoller
41
+ protected errorTracking: ErrorTracking
42
+ private maxCacheSize: number
43
+ private logger: Logger
44
+ public readonly options: PostHogOptions
45
+
46
+ distinctIdHasSentFlagCalls: Record<string, string[]>
47
+
48
+ /**
49
+ * Initialize a new PostHog client instance.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * // Basic initialization
54
+ * const client = new PostHogBackendClient(
55
+ * 'your-api-key',
56
+ * { host: 'https://app.posthog.com' }
57
+ * )
58
+ * ```
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * // With personal API key
63
+ * const client = new PostHogBackendClient(
64
+ * 'your-api-key',
65
+ * {
66
+ * host: 'https://app.posthog.com',
67
+ * personalApiKey: 'your-personal-api-key'
68
+ * }
69
+ * )
70
+ * ```
71
+ *
72
+ * {@label Initialization}
73
+ *
74
+ * @param apiKey - Your PostHog project API key
75
+ * @param options - Configuration options for the client
76
+ */
77
+ constructor(apiKey: string, options: PostHogOptions = {}) {
78
+ super(apiKey, options)
79
+
80
+ this.options = options
81
+
82
+ this.logger = createLogger(this.logMsgIfDebug.bind(this))
83
+
84
+ this.options.featureFlagsPollingInterval =
85
+ typeof options.featureFlagsPollingInterval === 'number'
86
+ ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL)
87
+ : THIRTY_SECONDS
88
+
89
+ if (options.personalApiKey) {
90
+ if (options.personalApiKey.includes('phc_')) {
91
+ throw new Error(
92
+ 'Your Personal API key is invalid. These keys are prefixed with "phx_" and can be created in PostHog project settings.'
93
+ )
94
+ }
95
+
96
+ // Only start the poller if local evaluation is enabled (defaults to true for backward compatibility)
97
+ const shouldEnableLocalEvaluation = options.enableLocalEvaluation !== false
98
+
99
+ if (shouldEnableLocalEvaluation) {
100
+ this.featureFlagsPoller = new FeatureFlagsPoller({
101
+ pollingInterval: this.options.featureFlagsPollingInterval,
102
+ personalApiKey: options.personalApiKey,
103
+ projectApiKey: apiKey,
104
+ timeout: options.requestTimeout ?? 10000, // 10 seconds
105
+ host: this.host,
106
+ fetch: options.fetch,
107
+ onError: (err: Error) => {
108
+ this._events.emit('error', err)
109
+ },
110
+ onLoad: (count: number) => {
111
+ this._events.emit('localEvaluationFlagsLoaded', count)
112
+ },
113
+ customHeaders: this.getCustomHeaders(),
114
+ })
115
+ }
116
+ }
117
+
118
+ this.errorTracking = new ErrorTracking(this, options, this.logger)
119
+ this.distinctIdHasSentFlagCalls = {}
120
+ this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE
121
+ }
122
+
123
+ /**
124
+ * Get a persisted property value from memory storage.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * // Get user ID
129
+ * const userId = client.getPersistedProperty('userId')
130
+ * ```
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * // Get session ID
135
+ * const sessionId = client.getPersistedProperty('sessionId')
136
+ * ```
137
+ *
138
+ * {@label Initialization}
139
+ *
140
+ * @param key - The property key to retrieve
141
+ * @returns The stored property value or undefined if not found
142
+ */
143
+ getPersistedProperty(key: PostHogPersistedProperty): any | undefined {
144
+ return this._memoryStorage.getProperty(key)
145
+ }
146
+
147
+ /**
148
+ * Set a persisted property value in memory storage.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * // Set user ID
153
+ * client.setPersistedProperty('userId', 'user_123')
154
+ * ```
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * // Set session ID
159
+ * client.setPersistedProperty('sessionId', 'session_456')
160
+ * ```
161
+ *
162
+ * {@label Initialization}
163
+ *
164
+ * @param key - The property key to set
165
+ * @param value - The value to store (null to remove)
166
+ */
167
+ setPersistedProperty(key: PostHogPersistedProperty, value: any | null): void {
168
+ return this._memoryStorage.setProperty(key, value)
169
+ }
170
+
171
+ /**
172
+ * Make an HTTP request using the configured fetch function or default fetch.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * // POST request
177
+ * const response = await client.fetch('/api/endpoint', {
178
+ * method: 'POST',
179
+ * headers: { 'Content-Type': 'application/json' },
180
+ * body: JSON.stringify(data)
181
+ * })
182
+ * ```
183
+ *
184
+ * @internal
185
+ *
186
+ * {@label Initialization}
187
+ *
188
+ * @param url - The URL to fetch
189
+ * @param options - Fetch options
190
+ * @returns Promise resolving to the fetch response
191
+ */
192
+ fetch(url: string, options: PostHogFetchOptions): Promise<PostHogFetchResponse> {
193
+ return this.options.fetch ? this.options.fetch(url, options) : fetch(url, options)
194
+ }
195
+
196
+ /**
197
+ * Get the library version from package.json.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * // Get version
202
+ * const version = client.getLibraryVersion()
203
+ * console.log(`Using PostHog SDK version: ${version}`)
204
+ * ```
205
+ *
206
+ * {@label Initialization}
207
+ *
208
+ * @returns The current library version string
209
+ */
210
+ getLibraryVersion(): string {
211
+ return version
212
+ }
213
+
214
+ /**
215
+ * Get the custom user agent string for this client.
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * // Get user agent
220
+ * const userAgent = client.getCustomUserAgent()
221
+ * // Returns: "posthog-node/5.7.0"
222
+ * ```
223
+ *
224
+ * {@label Identification}
225
+ *
226
+ * @returns The formatted user agent string
227
+ */
228
+ getCustomUserAgent(): string {
229
+ return `${this.getLibraryId()}/${this.getLibraryVersion()}`
230
+ }
231
+
232
+ /**
233
+ * Enable the PostHog client (opt-in).
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * // Enable client
238
+ * await client.enable()
239
+ * // Client is now enabled and will capture events
240
+ * ```
241
+ *
242
+ * {@label Privacy}
243
+ *
244
+ * @returns Promise that resolves when the client is enabled
245
+ */
246
+ enable(): Promise<void> {
247
+ return super.optIn()
248
+ }
249
+
250
+ /**
251
+ * Disable the PostHog client (opt-out).
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * // Disable client
256
+ * await client.disable()
257
+ * // Client is now disabled and will not capture events
258
+ * ```
259
+ *
260
+ * {@label Privacy}
261
+ *
262
+ * @returns Promise that resolves when the client is disabled
263
+ */
264
+ disable(): Promise<void> {
265
+ return super.optOut()
266
+ }
267
+
268
+ /**
269
+ * Enable or disable debug logging.
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * // Enable debug logging
274
+ * client.debug(true)
275
+ * ```
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * // Disable debug logging
280
+ * client.debug(false)
281
+ * ```
282
+ *
283
+ * {@label Initialization}
284
+ *
285
+ * @param enabled - Whether to enable debug logging
286
+ */
287
+ debug(enabled: boolean = true): void {
288
+ super.debug(enabled)
289
+ this.featureFlagsPoller?.debug(enabled)
290
+ }
291
+
292
+ /**
293
+ * Capture an event manually.
294
+ *
295
+ * @example
296
+ * ```ts
297
+ * // Basic capture
298
+ * client.capture({
299
+ * distinctId: 'user_123',
300
+ * event: 'button_clicked',
301
+ * properties: { button_color: 'red' }
302
+ * })
303
+ * ```
304
+ *
305
+ * {@label Capture}
306
+ *
307
+ * @param props - The event properties
308
+ * @returns void
309
+ */
310
+ capture(props: EventMessage): void {
311
+ if (typeof props === 'string') {
312
+ this.logMsgIfDebug(() =>
313
+ console.warn('Called capture() with a string as the first argument when an object was expected.')
314
+ )
315
+ }
316
+ this.addPendingPromise(
317
+ this.prepareEventMessage(props)
318
+ .then(({ distinctId, event, properties, options }) => {
319
+ return super.captureStateless(distinctId, event, properties, {
320
+ timestamp: options.timestamp,
321
+ disableGeoip: options.disableGeoip,
322
+ uuid: options.uuid,
323
+ })
324
+ })
325
+ .catch((err) => {
326
+ if (err) {
327
+ console.error(err)
328
+ }
329
+ })
330
+ )
331
+ }
332
+
333
+ /**
334
+ * Capture an event immediately (synchronously).
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * // Basic immediate capture
339
+ * await client.captureImmediate({
340
+ * distinctId: 'user_123',
341
+ * event: 'button_clicked',
342
+ * properties: { button_color: 'red' }
343
+ * })
344
+ * ```
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * // With feature flags
349
+ * await client.captureImmediate({
350
+ * distinctId: 'user_123',
351
+ * event: 'user_action',
352
+ * sendFeatureFlags: true
353
+ * })
354
+ * ```
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * // With custom feature flags options
359
+ * await client.captureImmediate({
360
+ * distinctId: 'user_123',
361
+ * event: 'user_action',
362
+ * sendFeatureFlags: {
363
+ * onlyEvaluateLocally: true,
364
+ * personProperties: { plan: 'premium' },
365
+ * groupProperties: { org: { tier: 'enterprise' } }
366
+ * flagKeys: ['flag1', 'flag2']
367
+ * }
368
+ * })
369
+ * ```
370
+ *
371
+ * {@label Capture}
372
+ *
373
+ * @param props - The event properties
374
+ * @returns Promise that resolves when the event is captured
375
+ */
376
+ async captureImmediate(props: EventMessage): Promise<void> {
377
+ if (typeof props === 'string') {
378
+ this.logMsgIfDebug(() =>
379
+ console.warn('Called captureImmediate() with a string as the first argument when an object was expected.')
380
+ )
381
+ }
382
+ return this.addPendingPromise(
383
+ this.prepareEventMessage(props)
384
+ .then(({ distinctId, event, properties, options }) => {
385
+ return super.captureStatelessImmediate(distinctId, event, properties, {
386
+ timestamp: options.timestamp,
387
+ disableGeoip: options.disableGeoip,
388
+ uuid: options.uuid,
389
+ })
390
+ })
391
+ .catch((err) => {
392
+ if (err) {
393
+ console.error(err)
394
+ }
395
+ })
396
+ )
397
+ }
398
+
399
+ /**
400
+ * Identify a user and set their properties.
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * // Basic identify with properties
405
+ * client.identify({
406
+ * distinctId: 'user_123',
407
+ * properties: {
408
+ * name: 'John Doe',
409
+ * email: 'john@example.com',
410
+ * plan: 'premium'
411
+ * }
412
+ * })
413
+ * ```
414
+ *
415
+ * @example
416
+ * ```ts
417
+ * // Using $set and $set_once
418
+ * client.identify({
419
+ * distinctId: 'user_123',
420
+ * properties: {
421
+ * $set: { name: 'John Doe', email: 'john@example.com' },
422
+ * $set_once: { first_login: new Date().toISOString() }
423
+ * }
424
+ * })
425
+ * ```
426
+ *
427
+ * {@label Identification}
428
+ *
429
+ * @param data - The identify data containing distinctId and properties
430
+ */
431
+ identify({ distinctId, properties, disableGeoip }: IdentifyMessage): void {
432
+ // Catch properties passed as $set and move them to the top level
433
+
434
+ // promote $set and $set_once to top level
435
+ const userPropsOnce = properties?.$set_once
436
+ delete properties?.$set_once
437
+
438
+ // if no $set is provided we assume all properties are $set
439
+ const userProps = properties?.$set || properties
440
+
441
+ super.identifyStateless(
442
+ distinctId,
443
+ {
444
+ $set: userProps,
445
+ $set_once: userPropsOnce,
446
+ },
447
+ { disableGeoip }
448
+ )
449
+ }
450
+
451
+ /**
452
+ * Identify a user and set their properties immediately (synchronously).
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * // Basic immediate identify
457
+ * await client.identifyImmediate({
458
+ * distinctId: 'user_123',
459
+ * properties: {
460
+ * name: 'John Doe',
461
+ * email: 'john@example.com'
462
+ * }
463
+ * })
464
+ * ```
465
+ *
466
+ * {@label Identification}
467
+ *
468
+ * @param data - The identify data containing distinctId and properties
469
+ * @returns Promise that resolves when the identify is processed
470
+ */
471
+ async identifyImmediate({ distinctId, properties, disableGeoip }: IdentifyMessage): Promise<void> {
472
+ // promote $set and $set_once to top level
473
+ const userPropsOnce = properties?.$set_once
474
+ delete properties?.$set_once
475
+
476
+ // if no $set is provided we assume all properties are $set
477
+ const userProps = properties?.$set || properties
478
+
479
+ await super.identifyStatelessImmediate(
480
+ distinctId,
481
+ {
482
+ $set: userProps,
483
+ $set_once: userPropsOnce,
484
+ },
485
+ { disableGeoip }
486
+ )
487
+ }
488
+
489
+ /**
490
+ * Create an alias to link two distinct IDs together.
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * // Link an anonymous user to an identified user
495
+ * client.alias({
496
+ * distinctId: 'anonymous_123',
497
+ * alias: 'user_456'
498
+ * })
499
+ * ```
500
+ *
501
+ * {@label Identification}
502
+ *
503
+ * @param data - The alias data containing distinctId and alias
504
+ */
505
+ alias(data: { distinctId: string; alias: string; disableGeoip?: boolean }): void {
506
+ super.aliasStateless(data.alias, data.distinctId, undefined, { disableGeoip: data.disableGeoip })
507
+ }
508
+
509
+ /**
510
+ * Create an alias to link two distinct IDs together immediately (synchronously).
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * // Link an anonymous user to an identified user immediately
515
+ * await client.aliasImmediate({
516
+ * distinctId: 'anonymous_123',
517
+ * alias: 'user_456'
518
+ * })
519
+ * ```
520
+ *
521
+ * {@label Identification}
522
+ *
523
+ * @param data - The alias data containing distinctId and alias
524
+ * @returns Promise that resolves when the alias is processed
525
+ */
526
+ async aliasImmediate(data: { distinctId: string; alias: string; disableGeoip?: boolean }): Promise<void> {
527
+ await super.aliasStatelessImmediate(data.alias, data.distinctId, undefined, { disableGeoip: data.disableGeoip })
528
+ }
529
+
530
+ /**
531
+ * Check if local evaluation of feature flags is ready.
532
+ *
533
+ * @example
534
+ * ```ts
535
+ * // Check if ready
536
+ * if (client.isLocalEvaluationReady()) {
537
+ * // Local evaluation is ready, can evaluate flags locally
538
+ * const flag = await client.getFeatureFlag('flag-key', 'user_123')
539
+ * } else {
540
+ * // Local evaluation not ready, will use remote evaluation
541
+ * const flag = await client.getFeatureFlag('flag-key', 'user_123')
542
+ * }
543
+ * ```
544
+ *
545
+ * {@label Feature flags}
546
+ *
547
+ * @returns true if local evaluation is ready, false otherwise
548
+ */
549
+ isLocalEvaluationReady(): boolean {
550
+ return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false
551
+ }
552
+
553
+ /**
554
+ * Wait for local evaluation of feature flags to be ready.
555
+ *
556
+ * @example
557
+ * ```ts
558
+ * // Wait for local evaluation
559
+ * const isReady = await client.waitForLocalEvaluationReady()
560
+ * if (isReady) {
561
+ * console.log('Local evaluation is ready')
562
+ * } else {
563
+ * console.log('Local evaluation timed out')
564
+ * }
565
+ * ```
566
+ *
567
+ * @example
568
+ * ```ts
569
+ * // Wait with custom timeout
570
+ * const isReady = await client.waitForLocalEvaluationReady(10000) // 10 seconds
571
+ * ```
572
+ *
573
+ * {@label Feature flags}
574
+ *
575
+ * @param timeoutMs - Timeout in milliseconds (default: 30000)
576
+ * @returns Promise that resolves to true if ready, false if timed out
577
+ */
578
+ async waitForLocalEvaluationReady(timeoutMs: number = THIRTY_SECONDS): Promise<boolean> {
579
+ if (this.isLocalEvaluationReady()) {
580
+ return true
581
+ }
582
+
583
+ if (this.featureFlagsPoller === undefined) {
584
+ return false
585
+ }
586
+
587
+ return new Promise((resolve) => {
588
+ const timeout = setTimeout(() => {
589
+ cleanup()
590
+ resolve(false)
591
+ }, timeoutMs)
592
+
593
+ const cleanup = this._events.on('localEvaluationFlagsLoaded', (count: number) => {
594
+ clearTimeout(timeout)
595
+ cleanup()
596
+ resolve(count > 0)
597
+ })
598
+ })
599
+ }
600
+
601
+ /**
602
+ * Get the value of a feature flag for a specific user.
603
+ *
604
+ * @example
605
+ * ```ts
606
+ * // Basic feature flag check
607
+ * const flagValue = await client.getFeatureFlag('new-feature', 'user_123')
608
+ * if (flagValue === 'variant-a') {
609
+ * // Show variant A
610
+ * } else if (flagValue === 'variant-b') {
611
+ * // Show variant B
612
+ * } else {
613
+ * // Flag is disabled or not found
614
+ * }
615
+ * ```
616
+ *
617
+ * @example
618
+ * ```ts
619
+ * // With groups and properties
620
+ * const flagValue = await client.getFeatureFlag('org-feature', 'user_123', {
621
+ * groups: { organization: 'acme-corp' },
622
+ * personProperties: { plan: 'enterprise' },
623
+ * groupProperties: { organization: { tier: 'premium' } }
624
+ * })
625
+ * ```
626
+ *
627
+ * @example
628
+ * ```ts
629
+ * // Only evaluate locally
630
+ * const flagValue = await client.getFeatureFlag('local-flag', 'user_123', {
631
+ * onlyEvaluateLocally: true
632
+ * })
633
+ * ```
634
+ *
635
+ * {@label Feature flags}
636
+ *
637
+ * @param key - The feature flag key
638
+ * @param distinctId - The user's distinct ID
639
+ * @param options - Optional configuration for flag evaluation
640
+ * @returns Promise that resolves to the flag value or undefined
641
+ */
642
+ async getFeatureFlag(
643
+ key: string,
644
+ distinctId: string,
645
+ options?: {
646
+ groups?: Record<string, string>
647
+ personProperties?: Record<string, string>
648
+ groupProperties?: Record<string, Record<string, string>>
649
+ onlyEvaluateLocally?: boolean
650
+ sendFeatureFlagEvents?: boolean
651
+ disableGeoip?: boolean
652
+ }
653
+ ): Promise<FeatureFlagValue | undefined> {
654
+ const { groups, disableGeoip } = options || {}
655
+ let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {}
656
+
657
+ const adjustedProperties = this.addLocalPersonAndGroupProperties(
658
+ distinctId,
659
+ groups,
660
+ personProperties,
661
+ groupProperties
662
+ )
663
+
664
+ personProperties = adjustedProperties.allPersonProperties
665
+ groupProperties = adjustedProperties.allGroupProperties
666
+
667
+ // set defaults
668
+ if (onlyEvaluateLocally == undefined) {
669
+ onlyEvaluateLocally = false
670
+ }
671
+ if (sendFeatureFlagEvents == undefined) {
672
+ sendFeatureFlagEvents = this.options.sendFeatureFlagEvent ?? true
673
+ }
674
+
675
+ let response = await this.featureFlagsPoller?.getFeatureFlag(
676
+ key,
677
+ distinctId,
678
+ groups,
679
+ personProperties,
680
+ groupProperties
681
+ )
682
+
683
+ const flagWasLocallyEvaluated = response !== undefined
684
+ let requestId = undefined
685
+ let flagDetail: FeatureFlagDetail | undefined = undefined
686
+ if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) {
687
+ const remoteResponse = await super.getFeatureFlagDetailStateless(
688
+ key,
689
+ distinctId,
690
+ groups,
691
+ personProperties,
692
+ groupProperties,
693
+ disableGeoip
694
+ )
695
+
696
+ if (remoteResponse === undefined) {
697
+ return undefined
698
+ }
699
+
700
+ flagDetail = remoteResponse.response
701
+ response = getFeatureFlagValue(flagDetail)
702
+ requestId = remoteResponse?.requestId
703
+ }
704
+
705
+ const featureFlagReportedKey = `${key}_${response}`
706
+
707
+ if (
708
+ sendFeatureFlagEvents &&
709
+ (!(distinctId in this.distinctIdHasSentFlagCalls) ||
710
+ !this.distinctIdHasSentFlagCalls[distinctId].includes(featureFlagReportedKey))
711
+ ) {
712
+ if (Object.keys(this.distinctIdHasSentFlagCalls).length >= this.maxCacheSize) {
713
+ this.distinctIdHasSentFlagCalls = {}
714
+ }
715
+ if (Array.isArray(this.distinctIdHasSentFlagCalls[distinctId])) {
716
+ this.distinctIdHasSentFlagCalls[distinctId].push(featureFlagReportedKey)
717
+ } else {
718
+ this.distinctIdHasSentFlagCalls[distinctId] = [featureFlagReportedKey]
719
+ }
720
+ this.capture({
721
+ distinctId,
722
+ event: '$feature_flag_called',
723
+ properties: {
724
+ $feature_flag: key,
725
+ $feature_flag_response: response,
726
+ $feature_flag_id: flagDetail?.metadata?.id,
727
+ $feature_flag_version: flagDetail?.metadata?.version,
728
+ $feature_flag_reason: flagDetail?.reason?.description ?? flagDetail?.reason?.code,
729
+ locally_evaluated: flagWasLocallyEvaluated,
730
+ [`$feature/${key}`]: response,
731
+ $feature_flag_request_id: requestId,
732
+ },
733
+ groups,
734
+ disableGeoip,
735
+ })
736
+ }
737
+ return response
738
+ }
739
+
740
+ /**
741
+ * Get the payload for a feature flag.
742
+ *
743
+ * @example
744
+ * ```ts
745
+ * // Get payload for a feature flag
746
+ * const payload = await client.getFeatureFlagPayload('flag-key', 'user_123')
747
+ * if (payload) {
748
+ * console.log('Flag payload:', payload)
749
+ * }
750
+ * ```
751
+ *
752
+ * @example
753
+ * ```ts
754
+ * // Get payload with specific match value
755
+ * const payload = await client.getFeatureFlagPayload('flag-key', 'user_123', 'variant-a')
756
+ * ```
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * // With groups and properties
761
+ * const payload = await client.getFeatureFlagPayload('org-flag', 'user_123', undefined, {
762
+ * groups: { organization: 'acme-corp' },
763
+ * personProperties: { plan: 'enterprise' }
764
+ * })
765
+ * ```
766
+ *
767
+ * {@label Feature flags}
768
+ *
769
+ * @param key - The feature flag key
770
+ * @param distinctId - The user's distinct ID
771
+ * @param matchValue - Optional match value to get payload for
772
+ * @param options - Optional configuration for flag evaluation
773
+ * @returns Promise that resolves to the flag payload or undefined
774
+ */
775
+ async getFeatureFlagPayload(
776
+ key: string,
777
+ distinctId: string,
778
+ matchValue?: FeatureFlagValue,
779
+ options?: {
780
+ groups?: Record<string, string>
781
+ personProperties?: Record<string, string>
782
+ groupProperties?: Record<string, Record<string, string>>
783
+ onlyEvaluateLocally?: boolean
784
+ /** @deprecated THIS OPTION HAS NO EFFECT, kept here for backwards compatibility reasons. */
785
+ sendFeatureFlagEvents?: boolean
786
+ disableGeoip?: boolean
787
+ }
788
+ ): Promise<JsonType | undefined> {
789
+ const { groups, disableGeoip } = options || {}
790
+ let { onlyEvaluateLocally, personProperties, groupProperties } = options || {}
791
+
792
+ const adjustedProperties = this.addLocalPersonAndGroupProperties(
793
+ distinctId,
794
+ groups,
795
+ personProperties,
796
+ groupProperties
797
+ )
798
+
799
+ personProperties = adjustedProperties.allPersonProperties
800
+ groupProperties = adjustedProperties.allGroupProperties
801
+
802
+ let response = undefined
803
+
804
+ const localEvaluationEnabled = this.featureFlagsPoller !== undefined
805
+ if (localEvaluationEnabled) {
806
+ // Ensure flags are loaded before checking for the specific flag
807
+ await this.featureFlagsPoller?.loadFeatureFlags()
808
+
809
+ const flag = this.featureFlagsPoller?.featureFlagsByKey[key]
810
+ if (flag) {
811
+ const result = await this.featureFlagsPoller?.computeFlagAndPayloadLocally(
812
+ flag,
813
+ distinctId,
814
+ groups,
815
+ personProperties,
816
+ groupProperties,
817
+ matchValue
818
+ )
819
+ if (result) {
820
+ matchValue = result.value
821
+ response = result.payload
822
+ }
823
+ }
824
+ }
825
+
826
+ // set defaults
827
+ if (onlyEvaluateLocally == undefined) {
828
+ onlyEvaluateLocally = false
829
+ }
830
+
831
+ const payloadWasLocallyEvaluated = response !== undefined
832
+
833
+ if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
834
+ response = await super.getFeatureFlagPayloadStateless(
835
+ key,
836
+ distinctId,
837
+ groups,
838
+ personProperties,
839
+ groupProperties,
840
+ disableGeoip
841
+ )
842
+ }
843
+ return response
844
+ }
845
+
846
+ /**
847
+ * Get the remote config payload for a feature flag.
848
+ *
849
+ * @example
850
+ * ```ts
851
+ * // Get remote config payload
852
+ * const payload = await client.getRemoteConfigPayload('flag-key')
853
+ * if (payload) {
854
+ * console.log('Remote config payload:', payload)
855
+ * }
856
+ * ```
857
+ *
858
+ * {@label Feature flags}
859
+ *
860
+ * @param flagKey - The feature flag key
861
+ * @returns Promise that resolves to the remote config payload or undefined
862
+ * @throws Error if personal API key is not provided
863
+ */
864
+ async getRemoteConfigPayload(flagKey: string): Promise<JsonType | undefined> {
865
+ if (!this.options.personalApiKey) {
866
+ throw new Error('Personal API key is required for remote config payload decryption')
867
+ }
868
+
869
+ const response = await this._requestRemoteConfigPayload(flagKey)
870
+ if (!response) {
871
+ return undefined
872
+ }
873
+
874
+ const parsed = await response.json()
875
+ // The payload from the endpoint is stored as a JSON encoded string. So when we return
876
+ // it, it's effectively double encoded. As far as we know, we should never get single-encoded
877
+ // JSON, but we'll be defensive here just in case.
878
+ if (typeof parsed === 'string') {
879
+ try {
880
+ // If the parsed value is a string, try parsing it again to handle double-encoded JSON
881
+ return JSON.parse(parsed)
882
+ } catch (e) {
883
+ // If second parse fails, return the string as is
884
+ return parsed
885
+ }
886
+ }
887
+ return parsed
888
+ }
889
+
890
+ /**
891
+ * Check if a feature flag is enabled for a specific user.
892
+ *
893
+ * @example
894
+ * ```ts
895
+ * // Basic feature flag check
896
+ * const isEnabled = await client.isFeatureEnabled('new-feature', 'user_123')
897
+ * if (isEnabled) {
898
+ * // Feature is enabled
899
+ * console.log('New feature is active')
900
+ * } else {
901
+ * // Feature is disabled
902
+ * console.log('New feature is not active')
903
+ * }
904
+ * ```
905
+ *
906
+ * @example
907
+ * ```ts
908
+ * // With groups and properties
909
+ * const isEnabled = await client.isFeatureEnabled('org-feature', 'user_123', {
910
+ * groups: { organization: 'acme-corp' },
911
+ * personProperties: { plan: 'enterprise' }
912
+ * })
913
+ * ```
914
+ *
915
+ * {@label Feature flags}
916
+ *
917
+ * @param key - The feature flag key
918
+ * @param distinctId - The user's distinct ID
919
+ * @param options - Optional configuration for flag evaluation
920
+ * @returns Promise that resolves to true if enabled, false if disabled, undefined if not found
921
+ */
922
+ async isFeatureEnabled(
923
+ key: string,
924
+ distinctId: string,
925
+ options?: {
926
+ groups?: Record<string, string>
927
+ personProperties?: Record<string, string>
928
+ groupProperties?: Record<string, Record<string, string>>
929
+ onlyEvaluateLocally?: boolean
930
+ sendFeatureFlagEvents?: boolean
931
+ disableGeoip?: boolean
932
+ }
933
+ ): Promise<boolean | undefined> {
934
+ const feat = await this.getFeatureFlag(key, distinctId, options)
935
+ if (feat === undefined) {
936
+ return undefined
937
+ }
938
+ return !!feat || false
939
+ }
940
+
941
+ /**
942
+ * Get all feature flag values for a specific user.
943
+ *
944
+ * @example
945
+ * ```ts
946
+ * // Get all flags for a user
947
+ * const allFlags = await client.getAllFlags('user_123')
948
+ * console.log('User flags:', allFlags)
949
+ * // Output: { 'flag-1': 'variant-a', 'flag-2': false, 'flag-3': 'variant-b' }
950
+ * ```
951
+ *
952
+ * @example
953
+ * ```ts
954
+ * // With specific flag keys
955
+ * const specificFlags = await client.getAllFlags('user_123', {
956
+ * flagKeys: ['flag-1', 'flag-2']
957
+ * })
958
+ * ```
959
+ *
960
+ * @example
961
+ * ```ts
962
+ * // With groups and properties
963
+ * const orgFlags = await client.getAllFlags('user_123', {
964
+ * groups: { organization: 'acme-corp' },
965
+ * personProperties: { plan: 'enterprise' }
966
+ * })
967
+ * ```
968
+ *
969
+ * {@label Feature flags}
970
+ *
971
+ * @param distinctId - The user's distinct ID
972
+ * @param options - Optional configuration for flag evaluation
973
+ * @returns Promise that resolves to a record of flag keys and their values
974
+ */
975
+ async getAllFlags(
976
+ distinctId: string,
977
+ options?: {
978
+ groups?: Record<string, string>
979
+ personProperties?: Record<string, string>
980
+ groupProperties?: Record<string, Record<string, string>>
981
+ onlyEvaluateLocally?: boolean
982
+ disableGeoip?: boolean
983
+ flagKeys?: string[]
984
+ }
985
+ ): Promise<Record<string, FeatureFlagValue>> {
986
+ const response = await this.getAllFlagsAndPayloads(distinctId, options)
987
+ return response.featureFlags || {}
988
+ }
989
+
990
+ /**
991
+ * Get all feature flag values and payloads for a specific user.
992
+ *
993
+ * @example
994
+ * ```ts
995
+ * // Get all flags and payloads for a user
996
+ * const result = await client.getAllFlagsAndPayloads('user_123')
997
+ * console.log('Flags:', result.featureFlags)
998
+ * console.log('Payloads:', result.featureFlagPayloads)
999
+ * ```
1000
+ *
1001
+ * @example
1002
+ * ```ts
1003
+ * // With specific flag keys
1004
+ * const result = await client.getAllFlagsAndPayloads('user_123', {
1005
+ * flagKeys: ['flag-1', 'flag-2']
1006
+ * })
1007
+ * ```
1008
+ *
1009
+ * @example
1010
+ * ```ts
1011
+ * // Only evaluate locally
1012
+ * const result = await client.getAllFlagsAndPayloads('user_123', {
1013
+ * onlyEvaluateLocally: true
1014
+ * })
1015
+ * ```
1016
+ *
1017
+ * {@label Feature flags}
1018
+ *
1019
+ * @param distinctId - The user's distinct ID
1020
+ * @param options - Optional configuration for flag evaluation
1021
+ * @returns Promise that resolves to flags and payloads
1022
+ */
1023
+ async getAllFlagsAndPayloads(
1024
+ distinctId: string,
1025
+ options?: {
1026
+ groups?: Record<string, string>
1027
+ personProperties?: Record<string, string>
1028
+ groupProperties?: Record<string, Record<string, string>>
1029
+ onlyEvaluateLocally?: boolean
1030
+ disableGeoip?: boolean
1031
+ flagKeys?: string[]
1032
+ }
1033
+ ): Promise<PostHogFlagsAndPayloadsResponse> {
1034
+ const { groups, disableGeoip, flagKeys } = options || {}
1035
+ let { onlyEvaluateLocally, personProperties, groupProperties } = options || {}
1036
+
1037
+ const adjustedProperties = this.addLocalPersonAndGroupProperties(
1038
+ distinctId,
1039
+ groups,
1040
+ personProperties,
1041
+ groupProperties
1042
+ )
1043
+
1044
+ personProperties = adjustedProperties.allPersonProperties
1045
+ groupProperties = adjustedProperties.allGroupProperties
1046
+
1047
+ // set defaults
1048
+ if (onlyEvaluateLocally == undefined) {
1049
+ onlyEvaluateLocally = false
1050
+ }
1051
+
1052
+ const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(
1053
+ distinctId,
1054
+ groups,
1055
+ personProperties,
1056
+ groupProperties,
1057
+ flagKeys
1058
+ )
1059
+
1060
+ let featureFlags = {}
1061
+ let featureFlagPayloads = {}
1062
+ let fallbackToFlags = true
1063
+ if (localEvaluationResult) {
1064
+ featureFlags = localEvaluationResult.response
1065
+ featureFlagPayloads = localEvaluationResult.payloads
1066
+ fallbackToFlags = localEvaluationResult.fallbackToFlags
1067
+ }
1068
+
1069
+ if (fallbackToFlags && !onlyEvaluateLocally) {
1070
+ const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(
1071
+ distinctId,
1072
+ groups,
1073
+ personProperties,
1074
+ groupProperties,
1075
+ disableGeoip,
1076
+ flagKeys
1077
+ )
1078
+ featureFlags = {
1079
+ ...featureFlags,
1080
+ ...(remoteEvaluationResult.flags || {}),
1081
+ }
1082
+ featureFlagPayloads = {
1083
+ ...featureFlagPayloads,
1084
+ ...(remoteEvaluationResult.payloads || {}),
1085
+ }
1086
+ }
1087
+
1088
+ return { featureFlags, featureFlagPayloads }
1089
+ }
1090
+
1091
+ /**
1092
+ * Create or update a group and its properties.
1093
+ *
1094
+ * @example
1095
+ * ```ts
1096
+ * // Create a company group
1097
+ * client.groupIdentify({
1098
+ * groupType: 'company',
1099
+ * groupKey: 'acme-corp',
1100
+ * properties: {
1101
+ * name: 'Acme Corporation',
1102
+ * industry: 'Technology',
1103
+ * employee_count: 500
1104
+ * },
1105
+ * distinctId: 'user_123'
1106
+ * })
1107
+ * ```
1108
+ *
1109
+ * @example
1110
+ * ```ts
1111
+ * // Update organization properties
1112
+ * client.groupIdentify({
1113
+ * groupType: 'organization',
1114
+ * groupKey: 'org-456',
1115
+ * properties: {
1116
+ * plan: 'enterprise',
1117
+ * region: 'US-West'
1118
+ * }
1119
+ * })
1120
+ * ```
1121
+ *
1122
+ * {@label Identification}
1123
+ *
1124
+ * @param data - The group identify data
1125
+ */
1126
+ groupIdentify({ groupType, groupKey, properties, distinctId, disableGeoip }: GroupIdentifyMessage): void {
1127
+ super.groupIdentifyStateless(groupType, groupKey, properties, { disableGeoip }, distinctId)
1128
+ }
1129
+
1130
+ /**
1131
+ * Reload feature flag definitions from the server for local evaluation.
1132
+ *
1133
+ * @example
1134
+ * ```ts
1135
+ * // Force reload of feature flags
1136
+ * await client.reloadFeatureFlags()
1137
+ * console.log('Feature flags reloaded')
1138
+ * ```
1139
+ *
1140
+ * @example
1141
+ * ```ts
1142
+ * // Reload before checking a specific flag
1143
+ * await client.reloadFeatureFlags()
1144
+ * const flag = await client.getFeatureFlag('flag-key', 'user_123')
1145
+ * ```
1146
+ *
1147
+ * {@label Feature flags}
1148
+ *
1149
+ * @returns Promise that resolves when flags are reloaded
1150
+ */
1151
+ async reloadFeatureFlags(): Promise<void> {
1152
+ await this.featureFlagsPoller?.loadFeatureFlags(true)
1153
+ }
1154
+
1155
+ /**
1156
+ * Shutdown the PostHog client gracefully.
1157
+ *
1158
+ * @example
1159
+ * ```ts
1160
+ * // Shutdown with default timeout
1161
+ * await client._shutdown()
1162
+ * ```
1163
+ *
1164
+ * @example
1165
+ * ```ts
1166
+ * // Shutdown with custom timeout
1167
+ * await client._shutdown(5000) // 5 seconds
1168
+ * ```
1169
+ *
1170
+ * {@label Shutdown}
1171
+ *
1172
+ * @param shutdownTimeoutMs - Timeout in milliseconds for shutdown
1173
+ * @returns Promise that resolves when shutdown is complete
1174
+ */
1175
+ async _shutdown(shutdownTimeoutMs?: number): Promise<void> {
1176
+ this.featureFlagsPoller?.stopPoller()
1177
+ this.errorTracking.shutdown()
1178
+ return super._shutdown(shutdownTimeoutMs)
1179
+ }
1180
+
1181
+ private async _requestRemoteConfigPayload(flagKey: string): Promise<PostHogFetchResponse | undefined> {
1182
+ if (!this.options.personalApiKey) {
1183
+ return undefined
1184
+ }
1185
+
1186
+ const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config?token=${encodeURIComponent(this.apiKey)}`
1187
+
1188
+ const options: PostHogFetchOptions = {
1189
+ method: 'GET',
1190
+ headers: {
1191
+ ...this.getCustomHeaders(),
1192
+ 'Content-Type': 'application/json',
1193
+ Authorization: `Bearer ${this.options.personalApiKey}`,
1194
+ },
1195
+ }
1196
+
1197
+ let abortTimeout = null
1198
+ if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
1199
+ const controller = new AbortController()
1200
+ abortTimeout = safeSetTimeout(() => {
1201
+ controller.abort()
1202
+ }, this.options.requestTimeout)
1203
+ options.signal = controller.signal
1204
+ }
1205
+
1206
+ try {
1207
+ return await this.fetch(url, options)
1208
+ } catch (error) {
1209
+ this._events.emit('error', error)
1210
+ return undefined
1211
+ } finally {
1212
+ if (abortTimeout) {
1213
+ clearTimeout(abortTimeout)
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ private extractPropertiesFromEvent(
1219
+ eventProperties?: Record<string | number, any>,
1220
+ groups?: Record<string, string | number>
1221
+ ): {
1222
+ personProperties: Record<string, string>
1223
+ groupProperties: Record<string, Record<string, string>>
1224
+ } {
1225
+ if (!eventProperties) {
1226
+ return { personProperties: {}, groupProperties: {} }
1227
+ }
1228
+
1229
+ const personProperties: Record<string, string> = {}
1230
+ const groupProperties: Record<string, Record<string, string>> = {}
1231
+
1232
+ for (const [key, value] of Object.entries(eventProperties)) {
1233
+ // If the value is a plain object and the key exists in groups, treat it as group properties
1234
+ if (isPlainObject(value) && groups && key in groups) {
1235
+ const groupProps: Record<string, string> = {}
1236
+ for (const [groupKey, groupValue] of Object.entries(value as Record<string, any>)) {
1237
+ groupProps[String(groupKey)] = String(groupValue)
1238
+ }
1239
+ groupProperties[String(key)] = groupProps
1240
+ } else {
1241
+ // Otherwise treat as person property
1242
+ personProperties[String(key)] = String(value)
1243
+ }
1244
+ }
1245
+
1246
+ return { personProperties, groupProperties }
1247
+ }
1248
+
1249
+ private async getFeatureFlagsForEvent(
1250
+ distinctId: string,
1251
+ groups?: Record<string, string | number>,
1252
+ disableGeoip?: boolean,
1253
+ sendFeatureFlagsOptions?: SendFeatureFlagsOptions
1254
+ ): Promise<PostHogFlagsResponse['featureFlags'] | undefined> {
1255
+ // Use properties directly from options if they exist
1256
+ const finalPersonProperties = sendFeatureFlagsOptions?.personProperties || {}
1257
+ const finalGroupProperties = sendFeatureFlagsOptions?.groupProperties || {}
1258
+ const flagKeys = sendFeatureFlagsOptions?.flagKeys
1259
+
1260
+ // Check if we should only evaluate locally
1261
+ const onlyEvaluateLocally = sendFeatureFlagsOptions?.onlyEvaluateLocally ?? false
1262
+
1263
+ // If onlyEvaluateLocally is true, only use local evaluation
1264
+ if (onlyEvaluateLocally) {
1265
+ if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
1266
+ const groupsWithStringValues: Record<string, string> = {}
1267
+ for (const [key, value] of Object.entries(groups || {})) {
1268
+ groupsWithStringValues[key] = String(value)
1269
+ }
1270
+
1271
+ return await this.getAllFlags(distinctId, {
1272
+ groups: groupsWithStringValues,
1273
+ personProperties: finalPersonProperties,
1274
+ groupProperties: finalGroupProperties,
1275
+ disableGeoip,
1276
+ onlyEvaluateLocally: true,
1277
+ flagKeys,
1278
+ })
1279
+ } else {
1280
+ // If onlyEvaluateLocally is true but we don't have local flags, return empty
1281
+ return {}
1282
+ }
1283
+ }
1284
+
1285
+ // Prefer local evaluation if available (default behavior; I'd rather not penalize users who haven't updated to the new API but still want to use local evaluation)
1286
+ if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
1287
+ const groupsWithStringValues: Record<string, string> = {}
1288
+ for (const [key, value] of Object.entries(groups || {})) {
1289
+ groupsWithStringValues[key] = String(value)
1290
+ }
1291
+
1292
+ return await this.getAllFlags(distinctId, {
1293
+ groups: groupsWithStringValues,
1294
+ personProperties: finalPersonProperties,
1295
+ groupProperties: finalGroupProperties,
1296
+ disableGeoip,
1297
+ onlyEvaluateLocally: true,
1298
+ flagKeys,
1299
+ })
1300
+ }
1301
+
1302
+ // Fall back to remote evaluation if local evaluation is not available
1303
+ return (
1304
+ await super.getFeatureFlagsStateless(
1305
+ distinctId,
1306
+ groups,
1307
+ finalPersonProperties,
1308
+ finalGroupProperties,
1309
+ disableGeoip
1310
+ )
1311
+ ).flags
1312
+ }
1313
+
1314
+ private addLocalPersonAndGroupProperties(
1315
+ distinctId: string,
1316
+ groups?: Record<string, string>,
1317
+ personProperties?: Record<string, string>,
1318
+ groupProperties?: Record<string, Record<string, string>>
1319
+ ): { allPersonProperties: Record<string, string>; allGroupProperties: Record<string, Record<string, string>> } {
1320
+ const allPersonProperties = { distinct_id: distinctId, ...(personProperties || {}) }
1321
+
1322
+ const allGroupProperties: Record<string, Record<string, string>> = {}
1323
+ if (groups) {
1324
+ for (const groupName of Object.keys(groups)) {
1325
+ allGroupProperties[groupName] = {
1326
+ $group_key: groups[groupName],
1327
+ ...(groupProperties?.[groupName] || {}),
1328
+ }
1329
+ }
1330
+ }
1331
+
1332
+ return { allPersonProperties, allGroupProperties }
1333
+ }
1334
+
1335
+ /**
1336
+ * Capture an error exception as an event.
1337
+ *
1338
+ * @example
1339
+ * ```ts
1340
+ * // Capture an error with user ID
1341
+ * try {
1342
+ * // Some risky operation
1343
+ * riskyOperation()
1344
+ * } catch (error) {
1345
+ * client.captureException(error, 'user_123')
1346
+ * }
1347
+ * ```
1348
+ *
1349
+ * @example
1350
+ * ```ts
1351
+ * // Capture with additional properties
1352
+ * try {
1353
+ * apiCall()
1354
+ * } catch (error) {
1355
+ * client.captureException(error, 'user_123', {
1356
+ * endpoint: '/api/users',
1357
+ * method: 'POST',
1358
+ * status_code: 500
1359
+ * })
1360
+ * }
1361
+ * ```
1362
+ *
1363
+ * {@label Error tracking}
1364
+ *
1365
+ * @param error - The error to capture
1366
+ * @param distinctId - Optional user distinct ID
1367
+ * @param additionalProperties - Optional additional properties to include
1368
+ */
1369
+ captureException(error: unknown, distinctId?: string, additionalProperties?: Record<string | number, any>): void {
1370
+ const syntheticException = new Error('PostHog syntheticException')
1371
+ this.addPendingPromise(
1372
+ ErrorTracking.buildEventMessage(error, { syntheticException }, distinctId, additionalProperties).then((msg) =>
1373
+ this.capture(msg)
1374
+ )
1375
+ )
1376
+ }
1377
+
1378
+ /**
1379
+ * Capture an error exception as an event immediately (synchronously).
1380
+ *
1381
+ * @example
1382
+ * ```ts
1383
+ * // Capture an error immediately with user ID
1384
+ * try {
1385
+ * // Some risky operation
1386
+ * riskyOperation()
1387
+ * } catch (error) {
1388
+ * await client.captureExceptionImmediate(error, 'user_123')
1389
+ * }
1390
+ * ```
1391
+ *
1392
+ * @example
1393
+ * ```ts
1394
+ * // Capture with additional properties
1395
+ * try {
1396
+ * apiCall()
1397
+ * } catch (error) {
1398
+ * await client.captureExceptionImmediate(error, 'user_123', {
1399
+ * endpoint: '/api/users',
1400
+ * method: 'POST',
1401
+ * status_code: 500
1402
+ * })
1403
+ * }
1404
+ * ```
1405
+ *
1406
+ * {@label Error tracking}
1407
+ *
1408
+ * @param error - The error to capture
1409
+ * @param distinctId - Optional user distinct ID
1410
+ * @param additionalProperties - Optional additional properties to include
1411
+ * @returns Promise that resolves when the error is captured
1412
+ */
1413
+ async captureExceptionImmediate(
1414
+ error: unknown,
1415
+ distinctId?: string,
1416
+ additionalProperties?: Record<string | number, any>
1417
+ ): Promise<void> {
1418
+ const syntheticException = new Error('PostHog syntheticException')
1419
+ this.addPendingPromise(
1420
+ ErrorTracking.buildEventMessage(error, { syntheticException }, distinctId, additionalProperties).then((msg) =>
1421
+ this.captureImmediate(msg)
1422
+ )
1423
+ )
1424
+ }
1425
+
1426
+ public async prepareEventMessage(props: EventMessage): Promise<{
1427
+ distinctId: string
1428
+ event: string
1429
+ properties: PostHogEventProperties
1430
+ options: PostHogCaptureOptions
1431
+ }> {
1432
+ const { distinctId, event, properties, groups, sendFeatureFlags, timestamp, disableGeoip, uuid }: EventMessage =
1433
+ props
1434
+
1435
+ // Run before_send if configured
1436
+ const eventMessage = this._runBeforeSend({
1437
+ distinctId,
1438
+ event,
1439
+ properties,
1440
+ groups,
1441
+ sendFeatureFlags,
1442
+ timestamp,
1443
+ disableGeoip,
1444
+ uuid,
1445
+ })
1446
+
1447
+ if (!eventMessage) {
1448
+ return Promise.reject(null)
1449
+ }
1450
+
1451
+ // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
1452
+ const eventProperties = await Promise.resolve()
1453
+ .then(async () => {
1454
+ if (sendFeatureFlags) {
1455
+ // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
1456
+ const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined
1457
+ return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions)
1458
+ }
1459
+
1460
+ if (event === '$feature_flag_called') {
1461
+ // 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.
1462
+ return {}
1463
+ }
1464
+ return {}
1465
+ })
1466
+ .then((flags) => {
1467
+ // Derive the relevant flag properties to add
1468
+ const additionalProperties: Record<string, any> = {}
1469
+ if (flags) {
1470
+ for (const [feature, variant] of Object.entries(flags)) {
1471
+ additionalProperties[`$feature/${feature}`] = variant
1472
+ }
1473
+ }
1474
+ const activeFlags = Object.keys(flags || {})
1475
+ .filter((flag) => flags?.[flag] !== false)
1476
+ .sort()
1477
+ if (activeFlags.length > 0) {
1478
+ additionalProperties['$active_feature_flags'] = activeFlags
1479
+ }
1480
+
1481
+ return additionalProperties
1482
+ })
1483
+ .catch(() => {
1484
+ // Something went wrong getting the flag info - we should capture the event anyways
1485
+ return {}
1486
+ })
1487
+ .then((additionalProperties) => {
1488
+ // No matter what - capture the event
1489
+ const props = {
1490
+ ...additionalProperties,
1491
+ ...(eventMessage.properties || {}),
1492
+ $groups: eventMessage.groups || groups,
1493
+ } as PostHogEventProperties
1494
+ return props
1495
+ })
1496
+
1497
+ return {
1498
+ distinctId: eventMessage.distinctId,
1499
+ event: eventMessage.event,
1500
+ properties: eventProperties,
1501
+ options: {
1502
+ timestamp: eventMessage.timestamp,
1503
+ disableGeoip: eventMessage.disableGeoip,
1504
+ uuid: eventMessage.uuid,
1505
+ },
1506
+ }
1507
+ }
1508
+
1509
+ private _runBeforeSend(eventMessage: EventMessage): EventMessage | null {
1510
+ const beforeSend = this.options.before_send
1511
+ if (!beforeSend) {
1512
+ return eventMessage
1513
+ }
1514
+
1515
+ const fns = Array.isArray(beforeSend) ? beforeSend : [beforeSend]
1516
+ let result: EventMessage | null = eventMessage
1517
+
1518
+ for (const fn of fns) {
1519
+ result = fn(result)
1520
+ if (!result) {
1521
+ this.logMsgIfDebug(() => console.info(`Event '${eventMessage.event}' was rejected in beforeSend function`))
1522
+ return null
1523
+ }
1524
+ if (!result.properties || Object.keys(result.properties).length === 0) {
1525
+ const message = `Event '${result.event}' has no properties after beforeSend function, this is likely an error.`
1526
+ this.logMsgIfDebug(() => console.warn(message))
1527
+ }
1528
+ }
1529
+
1530
+ return result
1531
+ }
1532
+ }