@posthog/core 1.1.0 → 1.2.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 (126) hide show
  1. package/dist/error-tracking/chunk-ids.js +1 -1
  2. package/dist/error-tracking/chunk-ids.mjs +1 -1
  3. package/dist/error-tracking/coercers/error-event-coercer.js +4 -5
  4. package/dist/error-tracking/coercers/error-event-coercer.mjs +4 -5
  5. package/dist/error-tracking/coercers/event-coercer.js +1 -2
  6. package/dist/error-tracking/coercers/event-coercer.mjs +1 -2
  7. package/dist/error-tracking/coercers/object-coercer.js +1 -2
  8. package/dist/error-tracking/coercers/object-coercer.mjs +1 -2
  9. package/dist/error-tracking/coercers/primitive-coercer.js +1 -2
  10. package/dist/error-tracking/coercers/primitive-coercer.mjs +1 -2
  11. package/dist/error-tracking/coercers/promise-rejection-event.js +4 -5
  12. package/dist/error-tracking/coercers/promise-rejection-event.mjs +4 -5
  13. package/dist/error-tracking/coercers/string-coercer.js +3 -4
  14. package/dist/error-tracking/coercers/string-coercer.mjs +3 -4
  15. package/dist/error-tracking/coercers/utils.js +2 -4
  16. package/dist/error-tracking/coercers/utils.mjs +2 -4
  17. package/dist/error-tracking/error-properties-builder.js +11 -15
  18. package/dist/error-tracking/error-properties-builder.mjs +11 -15
  19. package/dist/error-tracking/parsers/index.js +2 -4
  20. package/dist/error-tracking/parsers/index.mjs +2 -4
  21. package/dist/error-tracking/parsers/node.js +3 -5
  22. package/dist/error-tracking/parsers/node.mjs +3 -5
  23. package/dist/error-tracking/utils.js +4 -4
  24. package/dist/error-tracking/utils.mjs +4 -4
  25. package/dist/eventemitter.js +4 -4
  26. package/dist/eventemitter.mjs +4 -4
  27. package/dist/featureFlagUtils.js +20 -45
  28. package/dist/featureFlagUtils.mjs +20 -45
  29. package/dist/gzip.js +1 -2
  30. package/dist/gzip.mjs +1 -2
  31. package/dist/index.d.ts +4 -366
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +54 -1225
  34. package/dist/index.mjs +5 -1190
  35. package/dist/posthog-core-stateless.d.ts +204 -0
  36. package/dist/posthog-core-stateless.d.ts.map +1 -0
  37. package/dist/posthog-core-stateless.js +675 -0
  38. package/dist/posthog-core-stateless.mjs +632 -0
  39. package/dist/posthog-core.d.ts +171 -0
  40. package/dist/posthog-core.d.ts.map +1 -0
  41. package/dist/posthog-core.js +554 -0
  42. package/dist/posthog-core.mjs +520 -0
  43. package/dist/testing/PostHogCoreTestClient.d.ts +2 -1
  44. package/dist/testing/PostHogCoreTestClient.d.ts.map +1 -1
  45. package/dist/testing/PostHogCoreTestClient.js +9 -11
  46. package/dist/testing/PostHogCoreTestClient.mjs +8 -10
  47. package/dist/testing/test-utils.js +1 -1
  48. package/dist/testing/test-utils.mjs +1 -1
  49. package/dist/utils/bucketed-rate-limiter.js +8 -12
  50. package/dist/utils/bucketed-rate-limiter.mjs +8 -12
  51. package/dist/utils/index.js +3 -3
  52. package/dist/utils/index.mjs +3 -3
  53. package/dist/utils/type-utils.js +1 -1
  54. package/dist/utils/type-utils.mjs +1 -1
  55. package/dist/vendor/uuidv7.js +12 -16
  56. package/dist/vendor/uuidv7.mjs +12 -16
  57. package/package.json +3 -2
  58. package/src/__tests__/featureFlagUtils.spec.ts +427 -0
  59. package/src/__tests__/gzip.spec.ts +69 -0
  60. package/src/__tests__/posthog.ai.spec.ts +110 -0
  61. package/src/__tests__/posthog.capture.spec.ts +91 -0
  62. package/src/__tests__/posthog.core.spec.ts +135 -0
  63. package/src/__tests__/posthog.debug.spec.ts +36 -0
  64. package/src/__tests__/posthog.enqueue.spec.ts +93 -0
  65. package/src/__tests__/posthog.featureflags.spec.ts +1106 -0
  66. package/src/__tests__/posthog.featureflags.v1.spec.ts +922 -0
  67. package/src/__tests__/posthog.flush.spec.ts +237 -0
  68. package/src/__tests__/posthog.gdpr.spec.ts +50 -0
  69. package/src/__tests__/posthog.groups.spec.ts +96 -0
  70. package/src/__tests__/posthog.identify.spec.ts +194 -0
  71. package/src/__tests__/posthog.init.spec.ts +110 -0
  72. package/src/__tests__/posthog.listeners.spec.ts +51 -0
  73. package/src/__tests__/posthog.register.spec.ts +47 -0
  74. package/src/__tests__/posthog.reset.spec.ts +76 -0
  75. package/src/__tests__/posthog.sessions.spec.ts +63 -0
  76. package/src/__tests__/posthog.setProperties.spec.ts +102 -0
  77. package/src/__tests__/posthog.shutdown.spec.ts +88 -0
  78. package/src/__tests__/utils.spec.ts +36 -0
  79. package/src/error-tracking/chunk-ids.ts +58 -0
  80. package/src/error-tracking/coercers/dom-exception-coercer.ts +38 -0
  81. package/src/error-tracking/coercers/error-coercer.ts +36 -0
  82. package/src/error-tracking/coercers/error-event-coercer.ts +24 -0
  83. package/src/error-tracking/coercers/event-coercer.ts +19 -0
  84. package/src/error-tracking/coercers/index.ts +8 -0
  85. package/src/error-tracking/coercers/object-coercer.ts +76 -0
  86. package/src/error-tracking/coercers/primitive-coercer.ts +19 -0
  87. package/src/error-tracking/coercers/promise-rejection-event.spec.ts +77 -0
  88. package/src/error-tracking/coercers/promise-rejection-event.ts +53 -0
  89. package/src/error-tracking/coercers/string-coercer.spec.ts +26 -0
  90. package/src/error-tracking/coercers/string-coercer.ts +31 -0
  91. package/src/error-tracking/coercers/utils.ts +33 -0
  92. package/src/error-tracking/error-properties-builder.coerce.spec.ts +202 -0
  93. package/src/error-tracking/error-properties-builder.parse.spec.ts +30 -0
  94. package/src/error-tracking/error-properties-builder.ts +169 -0
  95. package/src/error-tracking/index.ts +5 -0
  96. package/src/error-tracking/parsers/base.ts +29 -0
  97. package/src/error-tracking/parsers/chrome.ts +53 -0
  98. package/src/error-tracking/parsers/gecko.ts +38 -0
  99. package/src/error-tracking/parsers/index.ts +104 -0
  100. package/src/error-tracking/parsers/node.ts +111 -0
  101. package/src/error-tracking/parsers/opera.ts +18 -0
  102. package/src/error-tracking/parsers/react-native.ts +0 -0
  103. package/src/error-tracking/parsers/safari.ts +33 -0
  104. package/src/error-tracking/parsers/winjs.ts +12 -0
  105. package/src/error-tracking/types.ts +107 -0
  106. package/src/error-tracking/utils.ts +39 -0
  107. package/src/eventemitter.ts +27 -0
  108. package/src/featureFlagUtils.ts +192 -0
  109. package/src/gzip.ts +29 -0
  110. package/src/index.ts +8 -0
  111. package/src/posthog-core-stateless.ts +1226 -0
  112. package/src/posthog-core.ts +958 -0
  113. package/src/testing/PostHogCoreTestClient.ts +91 -0
  114. package/src/testing/index.ts +2 -0
  115. package/src/testing/test-utils.ts +47 -0
  116. package/src/types.ts +544 -0
  117. package/src/utils/bucketed-rate-limiter.spec.ts +33 -0
  118. package/src/utils/bucketed-rate-limiter.ts +85 -0
  119. package/src/utils/index.ts +98 -0
  120. package/src/utils/number-utils.spec.ts +89 -0
  121. package/src/utils/number-utils.ts +30 -0
  122. package/src/utils/promise-queue.spec.ts +55 -0
  123. package/src/utils/promise-queue.ts +30 -0
  124. package/src/utils/string-utils.ts +23 -0
  125. package/src/utils/type-utils.ts +134 -0
  126. package/src/vendor/uuidv7.ts +479 -0
@@ -0,0 +1,1226 @@
1
+ import { SimpleEventEmitter } from './eventemitter'
2
+ import { getFeatureFlagValue, normalizeFlagsResponse } from './featureFlagUtils'
3
+ import { gzipCompress, isGzipSupported } from './gzip'
4
+ import {
5
+ PostHogFlagsResponse,
6
+ PostHogCoreOptions,
7
+ PostHogEventProperties,
8
+ PostHogCaptureOptions,
9
+ JsonType,
10
+ PostHogRemoteConfig,
11
+ FeatureFlagValue,
12
+ PostHogV2FlagsResponse,
13
+ PostHogV1FlagsResponse,
14
+ PostHogFeatureFlagDetails,
15
+ FeatureFlagDetail,
16
+ SurveyResponse,
17
+ PostHogFetchResponse,
18
+ PostHogFetchOptions,
19
+ PostHogPersistedProperty,
20
+ PostHogQueueItem,
21
+ } from './types'
22
+ import {
23
+ allSettled,
24
+ assert,
25
+ currentISOTime,
26
+ PromiseQueue,
27
+ removeTrailingSlash,
28
+ retriable,
29
+ RetriableOptions,
30
+ safeSetTimeout,
31
+ STRING_FORMAT,
32
+ } from './utils'
33
+ import { uuidv7 } from './vendor/uuidv7'
34
+
35
+ class PostHogFetchHttpError extends Error {
36
+ name = 'PostHogFetchHttpError'
37
+
38
+ constructor(
39
+ public response: PostHogFetchResponse,
40
+ public reqByteLength: number
41
+ ) {
42
+ super('HTTP error while fetching PostHog: status=' + response.status + ', reqByteLength=' + reqByteLength)
43
+ }
44
+
45
+ get status(): number {
46
+ return this.response.status
47
+ }
48
+
49
+ get text(): Promise<string> {
50
+ return this.response.text()
51
+ }
52
+
53
+ get json(): Promise<any> {
54
+ return this.response.json()
55
+ }
56
+ }
57
+
58
+ class PostHogFetchNetworkError extends Error {
59
+ name = 'PostHogFetchNetworkError'
60
+
61
+ constructor(public error: unknown) {
62
+ // TRICKY: "cause" is a newer property but is just ignored otherwise. Cast to any to ignore the type issue.
63
+ // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
64
+ // @ts-ignore
65
+ super('Network error while fetching PostHog', error instanceof Error ? { cause: error } : {})
66
+ }
67
+ }
68
+
69
+ export const maybeAdd = (key: string, value: JsonType | undefined): Record<string, JsonType> =>
70
+ value !== undefined ? { [key]: value } : {}
71
+
72
+ export async function logFlushError(err: any): Promise<void> {
73
+ if (err instanceof PostHogFetchHttpError) {
74
+ let text = ''
75
+ try {
76
+ text = await err.text
77
+ } catch {}
78
+
79
+ console.error(`Error while flushing PostHog: message=${err.message}, response body=${text}`, err)
80
+ } else {
81
+ console.error('Error while flushing PostHog', err)
82
+ }
83
+ return Promise.resolve()
84
+ }
85
+
86
+ function isPostHogFetchError(err: unknown): err is PostHogFetchHttpError | PostHogFetchNetworkError {
87
+ return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError)
88
+ }
89
+
90
+ function isPostHogFetchContentTooLargeError(err: unknown): err is PostHogFetchHttpError & { status: 413 } {
91
+ return typeof err === 'object' && err instanceof PostHogFetchHttpError && err.status === 413
92
+ }
93
+
94
+ export enum QuotaLimitedFeature {
95
+ FeatureFlags = 'feature_flags',
96
+ Recordings = 'recordings',
97
+ }
98
+
99
+ export abstract class PostHogCoreStateless {
100
+ // options
101
+ readonly apiKey: string
102
+ readonly host: string
103
+ readonly flushAt: number
104
+ readonly preloadFeatureFlags: boolean
105
+ readonly disableSurveys: boolean
106
+ private maxBatchSize: number
107
+ private maxQueueSize: number
108
+ private flushInterval: number
109
+ private flushPromise: Promise<any> | null = null
110
+ private shutdownPromise: Promise<void> | null = null
111
+ private requestTimeout: number
112
+ private featureFlagsRequestTimeoutMs: number
113
+ private remoteConfigRequestTimeoutMs: number
114
+ private removeDebugCallback?: () => void
115
+ private disableGeoip: boolean
116
+ private historicalMigration: boolean
117
+ protected disabled
118
+ protected disableCompression: boolean
119
+
120
+ private defaultOptIn: boolean
121
+ private promiseQueue: PromiseQueue = new PromiseQueue()
122
+
123
+ // internal
124
+ protected _events = new SimpleEventEmitter()
125
+ protected _flushTimer?: any
126
+ protected _retryOptions: RetriableOptions
127
+ protected _initPromise: Promise<void>
128
+ protected _isInitialized: boolean = false
129
+ protected _remoteConfigResponsePromise?: Promise<PostHogRemoteConfig | undefined>
130
+
131
+ // Abstract methods to be overridden by implementations
132
+ abstract fetch(url: string, options: PostHogFetchOptions): Promise<PostHogFetchResponse>
133
+ abstract getLibraryId(): string
134
+ abstract getLibraryVersion(): string
135
+ abstract getCustomUserAgent(): string | void
136
+
137
+ // This is our abstracted storage. Each implementation should handle its own
138
+ abstract getPersistedProperty<T>(key: PostHogPersistedProperty): T | undefined
139
+ abstract setPersistedProperty<T>(key: PostHogPersistedProperty, value: T | null): void
140
+
141
+ constructor(apiKey: string, options?: PostHogCoreOptions) {
142
+ assert(apiKey, "You must pass your PostHog project's api key.")
143
+
144
+ this.apiKey = apiKey
145
+ this.host = removeTrailingSlash(options?.host || 'https://us.i.posthog.com')
146
+ this.flushAt = options?.flushAt ? Math.max(options?.flushAt, 1) : 20
147
+ this.maxBatchSize = Math.max(this.flushAt, options?.maxBatchSize ?? 100)
148
+ this.maxQueueSize = Math.max(this.flushAt, options?.maxQueueSize ?? 1000)
149
+ this.flushInterval = options?.flushInterval ?? 10000
150
+ this.preloadFeatureFlags = options?.preloadFeatureFlags ?? true
151
+ // If enable is explicitly set to false we override the optout
152
+ this.defaultOptIn = options?.defaultOptIn ?? true
153
+ this.disableSurveys = options?.disableSurveys ?? false
154
+
155
+ this._retryOptions = {
156
+ retryCount: options?.fetchRetryCount ?? 3,
157
+ retryDelay: options?.fetchRetryDelay ?? 3000, // 3 seconds
158
+ retryCheck: isPostHogFetchError,
159
+ }
160
+ this.requestTimeout = options?.requestTimeout ?? 10000 // 10 seconds
161
+ this.featureFlagsRequestTimeoutMs = options?.featureFlagsRequestTimeoutMs ?? 3000 // 3 seconds
162
+ this.remoteConfigRequestTimeoutMs = options?.remoteConfigRequestTimeoutMs ?? 3000 // 3 seconds
163
+ this.disableGeoip = options?.disableGeoip ?? true
164
+ this.disabled = options?.disabled ?? false
165
+ this.historicalMigration = options?.historicalMigration ?? false
166
+ // Init promise allows the derived class to block calls until it is ready
167
+ this._initPromise = Promise.resolve()
168
+ this._isInitialized = true
169
+ this.disableCompression = !isGzipSupported() || (options?.disableCompression ?? false)
170
+ }
171
+
172
+ protected logMsgIfDebug(fn: () => void): void {
173
+ if (this.isDebug) {
174
+ fn()
175
+ }
176
+ }
177
+
178
+ protected wrap(fn: () => void): void {
179
+ if (this.disabled) {
180
+ this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'))
181
+ return
182
+ }
183
+
184
+ if (this._isInitialized) {
185
+ // NOTE: We could also check for the "opt in" status here...
186
+ return fn()
187
+ }
188
+
189
+ this._initPromise.then(() => fn())
190
+ }
191
+
192
+ protected getCommonEventProperties(): PostHogEventProperties {
193
+ return {
194
+ $lib: this.getLibraryId(),
195
+ $lib_version: this.getLibraryVersion(),
196
+ }
197
+ }
198
+
199
+ public get optedOut(): boolean {
200
+ return this.getPersistedProperty(PostHogPersistedProperty.OptedOut) ?? !this.defaultOptIn
201
+ }
202
+
203
+ async optIn(): Promise<void> {
204
+ this.wrap(() => {
205
+ this.setPersistedProperty(PostHogPersistedProperty.OptedOut, false)
206
+ })
207
+ }
208
+
209
+ async optOut(): Promise<void> {
210
+ this.wrap(() => {
211
+ this.setPersistedProperty(PostHogPersistedProperty.OptedOut, true)
212
+ })
213
+ }
214
+
215
+ on(event: string, cb: (...args: any[]) => void): () => void {
216
+ return this._events.on(event, cb)
217
+ }
218
+
219
+ /**
220
+ * Enables or disables debug mode for detailed logging.
221
+ *
222
+ * @remarks
223
+ * Debug mode logs all PostHog calls to the console for troubleshooting.
224
+ * This is useful during development to understand what data is being sent.
225
+ *
226
+ * {@label Initialization}
227
+ *
228
+ * @example
229
+ * ```js
230
+ * // enable debug mode
231
+ * posthog.debug(true)
232
+ * ```
233
+ *
234
+ * @example
235
+ * ```js
236
+ * // disable debug mode
237
+ * posthog.debug(false)
238
+ * ```
239
+ *
240
+ * @public
241
+ *
242
+ * @param {boolean} [debug] If true, will enable debug mode.
243
+ */
244
+ debug(enabled: boolean = true): void {
245
+ this.removeDebugCallback?.()
246
+
247
+ if (enabled) {
248
+ const removeDebugCallback = this.on('*', (event, payload) => console.log('PostHog Debug', event, payload))
249
+ this.removeDebugCallback = () => {
250
+ removeDebugCallback()
251
+ this.removeDebugCallback = undefined
252
+ }
253
+ }
254
+ }
255
+
256
+ get isDebug(): boolean {
257
+ return !!this.removeDebugCallback
258
+ }
259
+
260
+ get isDisabled(): boolean {
261
+ return this.disabled
262
+ }
263
+
264
+ private buildPayload(payload: {
265
+ distinct_id: string
266
+ event: string
267
+ properties?: PostHogEventProperties
268
+ }): PostHogEventProperties {
269
+ return {
270
+ distinct_id: payload.distinct_id,
271
+ event: payload.event,
272
+ properties: {
273
+ ...(payload.properties || {}),
274
+ ...this.getCommonEventProperties(), // Common PH props
275
+ },
276
+ }
277
+ }
278
+
279
+ public addPendingPromise<T>(promise: Promise<T>): Promise<T> {
280
+ return this.promiseQueue.add(promise)
281
+ }
282
+
283
+ /***
284
+ *** TRACKING
285
+ ***/
286
+ protected identifyStateless(
287
+ distinctId: string,
288
+ properties?: PostHogEventProperties,
289
+ options?: PostHogCaptureOptions
290
+ ): void {
291
+ this.wrap(() => {
292
+ // The properties passed to identifyStateless are event properties.
293
+ // To add person properties, pass in all person properties to the `$set` and `$set_once` keys.
294
+
295
+ const payload = {
296
+ ...this.buildPayload({
297
+ distinct_id: distinctId,
298
+ event: '$identify',
299
+ properties,
300
+ }),
301
+ }
302
+
303
+ this.enqueue('identify', payload, options)
304
+ })
305
+ }
306
+
307
+ protected async identifyStatelessImmediate(
308
+ distinctId: string,
309
+ properties?: PostHogEventProperties,
310
+ options?: PostHogCaptureOptions
311
+ ): Promise<void> {
312
+ const payload = {
313
+ ...this.buildPayload({
314
+ distinct_id: distinctId,
315
+ event: '$identify',
316
+ properties,
317
+ }),
318
+ }
319
+
320
+ await this.sendImmediate('identify', payload, options)
321
+ }
322
+
323
+ protected captureStateless(
324
+ distinctId: string,
325
+ event: string,
326
+ properties?: PostHogEventProperties,
327
+ options?: PostHogCaptureOptions
328
+ ): void {
329
+ this.wrap(() => {
330
+ const payload = this.buildPayload({ distinct_id: distinctId, event, properties })
331
+ this.enqueue('capture', payload, options)
332
+ })
333
+ }
334
+
335
+ protected async captureStatelessImmediate(
336
+ distinctId: string,
337
+ event: string,
338
+ properties?: PostHogEventProperties,
339
+ options?: PostHogCaptureOptions
340
+ ): Promise<void> {
341
+ const payload = this.buildPayload({ distinct_id: distinctId, event, properties })
342
+ await this.sendImmediate('capture', payload, options)
343
+ }
344
+
345
+ protected aliasStateless(
346
+ alias: string,
347
+ distinctId: string,
348
+ properties?: PostHogEventProperties,
349
+ options?: PostHogCaptureOptions
350
+ ): void {
351
+ this.wrap(() => {
352
+ const payload = this.buildPayload({
353
+ event: '$create_alias',
354
+ distinct_id: distinctId,
355
+ properties: {
356
+ ...(properties || {}),
357
+ distinct_id: distinctId,
358
+ alias,
359
+ },
360
+ })
361
+
362
+ this.enqueue('alias', payload, options)
363
+ })
364
+ }
365
+
366
+ protected async aliasStatelessImmediate(
367
+ alias: string,
368
+ distinctId: string,
369
+ properties?: PostHogEventProperties,
370
+ options?: PostHogCaptureOptions
371
+ ): Promise<void> {
372
+ const payload = this.buildPayload({
373
+ event: '$create_alias',
374
+ distinct_id: distinctId,
375
+ properties: {
376
+ ...(properties || {}),
377
+ distinct_id: distinctId,
378
+ alias,
379
+ },
380
+ })
381
+
382
+ await this.sendImmediate('alias', payload, options)
383
+ }
384
+
385
+ /***
386
+ *** GROUPS
387
+ ***/
388
+ protected groupIdentifyStateless(
389
+ groupType: string,
390
+ groupKey: string | number,
391
+ groupProperties?: PostHogEventProperties,
392
+ options?: PostHogCaptureOptions,
393
+ distinctId?: string,
394
+ eventProperties?: PostHogEventProperties
395
+ ): void {
396
+ this.wrap(() => {
397
+ const payload = this.buildPayload({
398
+ distinct_id: distinctId || `$${groupType}_${groupKey}`,
399
+ event: '$groupidentify',
400
+ properties: {
401
+ $group_type: groupType,
402
+ $group_key: groupKey,
403
+ $group_set: groupProperties || {},
404
+ ...(eventProperties || {}),
405
+ },
406
+ })
407
+
408
+ this.enqueue('capture', payload, options)
409
+ })
410
+ }
411
+
412
+ protected async getRemoteConfig(): Promise<PostHogRemoteConfig | undefined> {
413
+ await this._initPromise
414
+
415
+ let host = this.host
416
+
417
+ if (host === 'https://us.i.posthog.com') {
418
+ host = 'https://us-assets.i.posthog.com'
419
+ } else if (host === 'https://eu.i.posthog.com') {
420
+ host = 'https://eu-assets.i.posthog.com'
421
+ }
422
+
423
+ const url = `${host}/array/${this.apiKey}/config`
424
+ const fetchOptions: PostHogFetchOptions = {
425
+ method: 'GET',
426
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
427
+ }
428
+ // Don't retry remote config API calls
429
+ return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.remoteConfigRequestTimeoutMs)
430
+ .then((response) => response.json() as Promise<PostHogRemoteConfig>)
431
+ .catch((error) => {
432
+ this.logMsgIfDebug(() => console.error('Remote config could not be loaded', error))
433
+ this._events.emit('error', error)
434
+ return undefined
435
+ })
436
+ }
437
+
438
+ /***
439
+ *** FEATURE FLAGS
440
+ ***/
441
+
442
+ protected async getFlags(
443
+ distinctId: string,
444
+ groups: Record<string, string | number> = {},
445
+ personProperties: Record<string, string> = {},
446
+ groupProperties: Record<string, Record<string, string>> = {},
447
+ extraPayload: Record<string, any> = {}
448
+ ): Promise<PostHogFlagsResponse | undefined> {
449
+ await this._initPromise
450
+
451
+ const url = `${this.host}/flags/?v=2&config=true`
452
+ const fetchOptions: PostHogFetchOptions = {
453
+ method: 'POST',
454
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
455
+ body: JSON.stringify({
456
+ token: this.apiKey,
457
+ distinct_id: distinctId,
458
+ groups,
459
+ person_properties: personProperties,
460
+ group_properties: groupProperties,
461
+ ...extraPayload,
462
+ }),
463
+ }
464
+
465
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'Flags URL', url))
466
+
467
+ // Don't retry /flags API calls
468
+ return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
469
+ .then((response) => response.json() as Promise<PostHogV1FlagsResponse | PostHogV2FlagsResponse>)
470
+ .then((response) => normalizeFlagsResponse(response))
471
+ .catch((error) => {
472
+ this._events.emit('error', error)
473
+ return undefined
474
+ }) as Promise<PostHogFlagsResponse | undefined>
475
+ }
476
+
477
+ protected async getFeatureFlagStateless(
478
+ key: string,
479
+ distinctId: string,
480
+ groups: Record<string, string> = {},
481
+ personProperties: Record<string, string> = {},
482
+ groupProperties: Record<string, Record<string, string>> = {},
483
+ disableGeoip?: boolean
484
+ ): Promise<{
485
+ response: FeatureFlagValue | undefined
486
+ requestId: string | undefined
487
+ }> {
488
+ await this._initPromise
489
+
490
+ const flagDetailResponse = await this.getFeatureFlagDetailStateless(
491
+ key,
492
+ distinctId,
493
+ groups,
494
+ personProperties,
495
+ groupProperties,
496
+ disableGeoip
497
+ )
498
+
499
+ if (flagDetailResponse === undefined) {
500
+ // If we haven't loaded flags yet, or errored out, we respond with undefined
501
+ return {
502
+ response: undefined,
503
+ requestId: undefined,
504
+ }
505
+ }
506
+
507
+ let response = getFeatureFlagValue(flagDetailResponse.response)
508
+
509
+ if (response === undefined) {
510
+ // For cases where the flag is unknown, return false
511
+ response = false
512
+ }
513
+
514
+ // If we have flags we either return the value (true or string) or false
515
+ return {
516
+ response,
517
+ requestId: flagDetailResponse.requestId,
518
+ }
519
+ }
520
+
521
+ protected async getFeatureFlagDetailStateless(
522
+ key: string,
523
+ distinctId: string,
524
+ groups: Record<string, string> = {},
525
+ personProperties: Record<string, string> = {},
526
+ groupProperties: Record<string, Record<string, string>> = {},
527
+ disableGeoip?: boolean
528
+ ): Promise<
529
+ | {
530
+ response: FeatureFlagDetail | undefined
531
+ requestId: string | undefined
532
+ }
533
+ | undefined
534
+ > {
535
+ await this._initPromise
536
+
537
+ const flagsResponse = await this.getFeatureFlagDetailsStateless(
538
+ distinctId,
539
+ groups,
540
+ personProperties,
541
+ groupProperties,
542
+ disableGeoip,
543
+ [key]
544
+ )
545
+
546
+ if (flagsResponse === undefined) {
547
+ return undefined
548
+ }
549
+
550
+ const featureFlags = flagsResponse.flags
551
+
552
+ const flagDetail = featureFlags[key]
553
+
554
+ return {
555
+ response: flagDetail,
556
+ requestId: flagsResponse.requestId,
557
+ }
558
+ }
559
+
560
+ protected async getFeatureFlagPayloadStateless(
561
+ key: string,
562
+ distinctId: string,
563
+ groups: Record<string, string> = {},
564
+ personProperties: Record<string, string> = {},
565
+ groupProperties: Record<string, Record<string, string>> = {},
566
+ disableGeoip?: boolean
567
+ ): Promise<JsonType | undefined> {
568
+ await this._initPromise
569
+
570
+ const payloads = await this.getFeatureFlagPayloadsStateless(
571
+ distinctId,
572
+ groups,
573
+ personProperties,
574
+ groupProperties,
575
+ disableGeoip,
576
+ [key]
577
+ )
578
+
579
+ if (!payloads) {
580
+ return undefined
581
+ }
582
+
583
+ const response = payloads[key]
584
+
585
+ // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
586
+ if (response === undefined) {
587
+ return null
588
+ }
589
+
590
+ return response
591
+ }
592
+
593
+ protected async getFeatureFlagPayloadsStateless(
594
+ distinctId: string,
595
+ groups: Record<string, string> = {},
596
+ personProperties: Record<string, string> = {},
597
+ groupProperties: Record<string, Record<string, string>> = {},
598
+ disableGeoip?: boolean,
599
+ flagKeysToEvaluate?: string[]
600
+ ): Promise<PostHogFlagsResponse['featureFlagPayloads'] | undefined> {
601
+ await this._initPromise
602
+
603
+ const payloads = (
604
+ await this.getFeatureFlagsAndPayloadsStateless(
605
+ distinctId,
606
+ groups,
607
+ personProperties,
608
+ groupProperties,
609
+ disableGeoip,
610
+ flagKeysToEvaluate
611
+ )
612
+ ).payloads
613
+
614
+ return payloads
615
+ }
616
+
617
+ protected async getFeatureFlagsStateless(
618
+ distinctId: string,
619
+ groups: Record<string, string | number> = {},
620
+ personProperties: Record<string, string> = {},
621
+ groupProperties: Record<string, Record<string, string>> = {},
622
+ disableGeoip?: boolean,
623
+ flagKeysToEvaluate?: string[]
624
+ ): Promise<{
625
+ flags: PostHogFlagsResponse['featureFlags'] | undefined
626
+ payloads: PostHogFlagsResponse['featureFlagPayloads'] | undefined
627
+ requestId: PostHogFlagsResponse['requestId'] | undefined
628
+ }> {
629
+ await this._initPromise
630
+
631
+ return await this.getFeatureFlagsAndPayloadsStateless(
632
+ distinctId,
633
+ groups,
634
+ personProperties,
635
+ groupProperties,
636
+ disableGeoip,
637
+ flagKeysToEvaluate
638
+ )
639
+ }
640
+
641
+ protected async getFeatureFlagsAndPayloadsStateless(
642
+ distinctId: string,
643
+ groups: Record<string, string | number> = {},
644
+ personProperties: Record<string, string> = {},
645
+ groupProperties: Record<string, Record<string, string>> = {},
646
+ disableGeoip?: boolean,
647
+ flagKeysToEvaluate?: string[]
648
+ ): Promise<{
649
+ flags: PostHogFlagsResponse['featureFlags'] | undefined
650
+ payloads: PostHogFlagsResponse['featureFlagPayloads'] | undefined
651
+ requestId: PostHogFlagsResponse['requestId'] | undefined
652
+ }> {
653
+ await this._initPromise
654
+
655
+ const featureFlagDetails = await this.getFeatureFlagDetailsStateless(
656
+ distinctId,
657
+ groups,
658
+ personProperties,
659
+ groupProperties,
660
+ disableGeoip,
661
+ flagKeysToEvaluate
662
+ )
663
+
664
+ if (!featureFlagDetails) {
665
+ return {
666
+ flags: undefined,
667
+ payloads: undefined,
668
+ requestId: undefined,
669
+ }
670
+ }
671
+
672
+ return {
673
+ flags: featureFlagDetails.featureFlags,
674
+ payloads: featureFlagDetails.featureFlagPayloads,
675
+ requestId: featureFlagDetails.requestId,
676
+ }
677
+ }
678
+
679
+ protected async getFeatureFlagDetailsStateless(
680
+ distinctId: string,
681
+ groups: Record<string, string | number> = {},
682
+ personProperties: Record<string, string> = {},
683
+ groupProperties: Record<string, Record<string, string>> = {},
684
+ disableGeoip?: boolean,
685
+ flagKeysToEvaluate?: string[]
686
+ ): Promise<PostHogFeatureFlagDetails | undefined> {
687
+ await this._initPromise
688
+
689
+ const extraPayload: Record<string, any> = {}
690
+ if (disableGeoip ?? this.disableGeoip) {
691
+ extraPayload['geoip_disable'] = true
692
+ }
693
+ if (flagKeysToEvaluate) {
694
+ extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate
695
+ }
696
+ const flagsResponse = await this.getFlags(distinctId, groups, personProperties, groupProperties, extraPayload)
697
+
698
+ if (flagsResponse === undefined) {
699
+ // We probably errored out, so return undefined
700
+ return undefined
701
+ }
702
+
703
+ // if there's an error on the flagsResponse, log a console error, but don't throw an error
704
+ if (flagsResponse.errorsWhileComputingFlags) {
705
+ console.error(
706
+ '[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'
707
+ )
708
+ }
709
+
710
+ // Add check for quota limitation on feature flags
711
+ if (flagsResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
712
+ console.warn(
713
+ '[FEATURE FLAGS] Feature flags quota limit exceeded - feature flags unavailable. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts'
714
+ )
715
+ return {
716
+ flags: {},
717
+ featureFlags: {},
718
+ featureFlagPayloads: {},
719
+ requestId: flagsResponse?.requestId,
720
+ }
721
+ }
722
+
723
+ return flagsResponse
724
+ }
725
+
726
+ /***
727
+ *** SURVEYS
728
+ ***/
729
+
730
+ public async getSurveysStateless(): Promise<SurveyResponse['surveys']> {
731
+ await this._initPromise
732
+
733
+ if (this.disableSurveys === true) {
734
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'Loading surveys is disabled.'))
735
+ return []
736
+ }
737
+
738
+ const url = `${this.host}/api/surveys/?token=${this.apiKey}`
739
+ const fetchOptions: PostHogFetchOptions = {
740
+ method: 'GET',
741
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
742
+ }
743
+
744
+ const response = await this.fetchWithRetry(url, fetchOptions)
745
+ .then((response) => {
746
+ if (response.status !== 200 || !response.json) {
747
+ const msg = `Surveys API could not be loaded: ${response.status}`
748
+ const error = new Error(msg)
749
+ this.logMsgIfDebug(() => console.error(error))
750
+
751
+ this._events.emit('error', new Error(msg))
752
+ return undefined
753
+ }
754
+
755
+ return response.json() as Promise<SurveyResponse>
756
+ })
757
+ .catch((error) => {
758
+ this.logMsgIfDebug(() => console.error('Surveys API could not be loaded', error))
759
+
760
+ this._events.emit('error', error)
761
+ return undefined
762
+ })
763
+
764
+ const newSurveys = response?.surveys
765
+
766
+ if (newSurveys) {
767
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'Surveys fetched from API: ', JSON.stringify(newSurveys)))
768
+ }
769
+
770
+ return newSurveys ?? []
771
+ }
772
+
773
+ /***
774
+ *** SUPER PROPERTIES
775
+ ***/
776
+ private _props: PostHogEventProperties | undefined
777
+
778
+ protected get props(): PostHogEventProperties {
779
+ if (!this._props) {
780
+ this._props = this.getPersistedProperty<PostHogEventProperties>(PostHogPersistedProperty.Props)
781
+ }
782
+ return this._props || {}
783
+ }
784
+
785
+ protected set props(val: PostHogEventProperties | undefined) {
786
+ this._props = val
787
+ }
788
+
789
+ async register(properties: PostHogEventProperties): Promise<void> {
790
+ this.wrap(() => {
791
+ this.props = {
792
+ ...this.props,
793
+ ...properties,
794
+ }
795
+ this.setPersistedProperty<PostHogEventProperties>(PostHogPersistedProperty.Props, this.props)
796
+ })
797
+ }
798
+
799
+ async unregister(property: string): Promise<void> {
800
+ this.wrap(() => {
801
+ delete this.props[property]
802
+ this.setPersistedProperty<PostHogEventProperties>(PostHogPersistedProperty.Props, this.props)
803
+ })
804
+ }
805
+
806
+ /***
807
+ *** QUEUEING AND FLUSHING
808
+ ***/
809
+ protected enqueue(type: string, _message: any, options?: PostHogCaptureOptions): void {
810
+ this.wrap(() => {
811
+ if (this.optedOut) {
812
+ this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`)
813
+ return
814
+ }
815
+
816
+ const message = this.prepareMessage(type, _message, options)
817
+
818
+ const queue = this.getPersistedProperty<PostHogQueueItem[]>(PostHogPersistedProperty.Queue) || []
819
+
820
+ if (queue.length >= this.maxQueueSize) {
821
+ queue.shift()
822
+ this.logMsgIfDebug(() => console.info('Queue is full, the oldest event is dropped.'))
823
+ }
824
+
825
+ queue.push({ message })
826
+ this.setPersistedProperty<PostHogQueueItem[]>(PostHogPersistedProperty.Queue, queue)
827
+
828
+ this._events.emit(type, message)
829
+
830
+ // Flush queued events if we meet the flushAt length
831
+ if (queue.length >= this.flushAt) {
832
+ this.flushBackground()
833
+ }
834
+
835
+ if (this.flushInterval && !this._flushTimer) {
836
+ this._flushTimer = safeSetTimeout(() => this.flushBackground(), this.flushInterval)
837
+ }
838
+ })
839
+ }
840
+
841
+ protected async sendImmediate(type: string, _message: any, options?: PostHogCaptureOptions): Promise<void> {
842
+ if (this.disabled) {
843
+ this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'))
844
+ return
845
+ }
846
+
847
+ if (!this._isInitialized) {
848
+ await this._initPromise
849
+ }
850
+
851
+ if (this.optedOut) {
852
+ this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`)
853
+ return
854
+ }
855
+
856
+ const data: Record<string, any> = {
857
+ api_key: this.apiKey,
858
+ batch: [this.prepareMessage(type, _message, options)],
859
+ sent_at: currentISOTime(),
860
+ }
861
+
862
+ if (this.historicalMigration) {
863
+ data.historical_migration = true
864
+ }
865
+
866
+ const payload = JSON.stringify(data)
867
+
868
+ const url = `${this.host}/batch/`
869
+
870
+ const gzippedPayload = !this.disableCompression ? await gzipCompress(payload, this.isDebug) : null
871
+ const fetchOptions: PostHogFetchOptions = {
872
+ method: 'POST',
873
+ headers: {
874
+ ...this.getCustomHeaders(),
875
+ 'Content-Type': 'application/json',
876
+ ...(gzippedPayload !== null && { 'Content-Encoding': 'gzip' }),
877
+ },
878
+ body: gzippedPayload || payload,
879
+ }
880
+
881
+ try {
882
+ await this.fetchWithRetry(url, fetchOptions)
883
+ } catch (err) {
884
+ this._events.emit('error', err)
885
+ }
886
+ }
887
+
888
+ private prepareMessage(type: string, _message: any, options?: PostHogCaptureOptions): PostHogEventProperties {
889
+ const message = {
890
+ ..._message,
891
+ type: type,
892
+ library: this.getLibraryId(),
893
+ library_version: this.getLibraryVersion(),
894
+ timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
895
+ uuid: options?.uuid ? options.uuid : uuidv7(),
896
+ }
897
+
898
+ const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip
899
+ if (addGeoipDisableProperty) {
900
+ if (!message.properties) {
901
+ message.properties = {}
902
+ }
903
+ message['properties']['$geoip_disable'] = true
904
+ }
905
+
906
+ if (message.distinctId) {
907
+ message.distinct_id = message.distinctId
908
+ delete message.distinctId
909
+ }
910
+
911
+ return message
912
+ }
913
+
914
+ private clearFlushTimer(): void {
915
+ if (this._flushTimer) {
916
+ clearTimeout(this._flushTimer)
917
+ this._flushTimer = undefined
918
+ }
919
+ }
920
+
921
+ /**
922
+ * Helper for flushing the queue in the background
923
+ * Avoids unnecessary promise errors
924
+ */
925
+ private flushBackground(): void {
926
+ void this.flush().catch(async (err) => {
927
+ await logFlushError(err)
928
+ })
929
+ }
930
+
931
+ /**
932
+ * Flushes the queue of pending events.
933
+ *
934
+ * This function will return a promise that will resolve when the flush is complete,
935
+ * or reject if there was an error (for example if the server or network is down).
936
+ *
937
+ * If there is already a flush in progress, this function will wait for that flush to complete.
938
+ *
939
+ * It's recommended to do error handling in the callback of the promise.
940
+ *
941
+ * {@label Initialization}
942
+ *
943
+ * @example
944
+ * ```js
945
+ * // flush with error handling
946
+ * posthog.flush().then(() => {
947
+ * console.log('Flush complete')
948
+ * }).catch((err) => {
949
+ * console.error('Flush failed', err)
950
+ * })
951
+ * ```
952
+ *
953
+ * @public
954
+ *
955
+ * @throws PostHogFetchHttpError
956
+ * @throws PostHogFetchNetworkError
957
+ * @throws Error
958
+ */
959
+ async flush(): Promise<void> {
960
+ // Wait for the current flush operation to finish (regardless of success or failure), then try to flush again.
961
+ // Use allSettled instead of finally to be defensive around flush throwing errors immediately rather than rejecting.
962
+ // Use a custom allSettled implementation to avoid issues with patching Promise on RN
963
+ const nextFlushPromise = allSettled([this.flushPromise]).then(() => {
964
+ return this._flush()
965
+ })
966
+
967
+ this.flushPromise = nextFlushPromise
968
+ void this.addPendingPromise(nextFlushPromise)
969
+
970
+ allSettled([nextFlushPromise]).then(() => {
971
+ // If there are no others waiting to flush, clear the promise.
972
+ // We don't strictly need to do this, but it could make debugging easier
973
+ if (this.flushPromise === nextFlushPromise) {
974
+ this.flushPromise = null
975
+ }
976
+ })
977
+
978
+ return nextFlushPromise
979
+ }
980
+
981
+ protected getCustomHeaders(): { [key: string]: string } {
982
+ // Don't set the user agent if we're not on a browser. The latest spec allows
983
+ // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
984
+ // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
985
+ // but browsers such as Chrome and Safari have not caught up.
986
+ const customUserAgent = this.getCustomUserAgent()
987
+ const headers: { [key: string]: string } = {}
988
+ if (customUserAgent && customUserAgent !== '') {
989
+ headers['User-Agent'] = customUserAgent
990
+ }
991
+ return headers
992
+ }
993
+
994
+ private async _flush(): Promise<void> {
995
+ this.clearFlushTimer()
996
+ await this._initPromise
997
+
998
+ let queue = this.getPersistedProperty<PostHogQueueItem[]>(PostHogPersistedProperty.Queue) || []
999
+
1000
+ if (!queue.length) {
1001
+ return
1002
+ }
1003
+
1004
+ const sentMessages: any[] = []
1005
+ const originalQueueLength = queue.length
1006
+
1007
+ while (queue.length > 0 && sentMessages.length < originalQueueLength) {
1008
+ const batchItems = queue.slice(0, this.maxBatchSize)
1009
+ const batchMessages = batchItems.map((item) => item.message)
1010
+
1011
+ const persistQueueChange = (): void => {
1012
+ const refreshedQueue = this.getPersistedProperty<PostHogQueueItem[]>(PostHogPersistedProperty.Queue) || []
1013
+ const newQueue = refreshedQueue.slice(batchItems.length)
1014
+ this.setPersistedProperty<PostHogQueueItem[]>(PostHogPersistedProperty.Queue, newQueue)
1015
+ queue = newQueue
1016
+ }
1017
+
1018
+ const data: Record<string, any> = {
1019
+ api_key: this.apiKey,
1020
+ batch: batchMessages,
1021
+ sent_at: currentISOTime(),
1022
+ }
1023
+
1024
+ if (this.historicalMigration) {
1025
+ data.historical_migration = true
1026
+ }
1027
+
1028
+ const payload = JSON.stringify(data)
1029
+
1030
+ const url = `${this.host}/batch/`
1031
+
1032
+ const gzippedPayload = !this.disableCompression ? await gzipCompress(payload, this.isDebug) : null
1033
+ const fetchOptions: PostHogFetchOptions = {
1034
+ method: 'POST',
1035
+ headers: {
1036
+ ...this.getCustomHeaders(),
1037
+ 'Content-Type': 'application/json',
1038
+ ...(gzippedPayload !== null && { 'Content-Encoding': 'gzip' }),
1039
+ },
1040
+ body: gzippedPayload || payload,
1041
+ }
1042
+
1043
+ const retryOptions: Partial<RetriableOptions> = {
1044
+ retryCheck: (err) => {
1045
+ // don't automatically retry on 413 errors, we want to reduce the batch size first
1046
+ if (isPostHogFetchContentTooLargeError(err)) {
1047
+ return false
1048
+ }
1049
+ // otherwise, retry on network errors
1050
+ return isPostHogFetchError(err)
1051
+ },
1052
+ }
1053
+
1054
+ try {
1055
+ await this.fetchWithRetry(url, fetchOptions, retryOptions)
1056
+ } catch (err) {
1057
+ if (isPostHogFetchContentTooLargeError(err) && batchMessages.length > 1) {
1058
+ // if we get a 413 error, we want to reduce the batch size and try again
1059
+ this.maxBatchSize = Math.max(1, Math.floor(batchMessages.length / 2))
1060
+ this.logMsgIfDebug(() =>
1061
+ console.warn(
1062
+ `Received 413 when sending batch of size ${batchMessages.length}, reducing batch size to ${this.maxBatchSize}`
1063
+ )
1064
+ )
1065
+ // do not persist the queue change, we want to retry the same batch
1066
+ continue
1067
+ }
1068
+
1069
+ // depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
1070
+ // and this will be an endless loop, in this case, if the error isn't a network issue, we always remove the items from the queue
1071
+ if (!(err instanceof PostHogFetchNetworkError)) {
1072
+ persistQueueChange()
1073
+ }
1074
+ this._events.emit('error', err)
1075
+
1076
+ throw err
1077
+ }
1078
+
1079
+ persistQueueChange()
1080
+
1081
+ sentMessages.push(...batchMessages)
1082
+ }
1083
+ this._events.emit('flush', sentMessages)
1084
+ }
1085
+
1086
+ private async fetchWithRetry(
1087
+ url: string,
1088
+ options: PostHogFetchOptions,
1089
+ retryOptions?: Partial<RetriableOptions>,
1090
+ requestTimeout?: number
1091
+ ): Promise<PostHogFetchResponse> {
1092
+ ;(AbortSignal as any).timeout ??= function timeout(ms: number) {
1093
+ const ctrl = new AbortController()
1094
+ setTimeout(() => ctrl.abort(), ms)
1095
+ return ctrl.signal
1096
+ }
1097
+
1098
+ const body = options.body ? options.body : ''
1099
+ let reqByteLength = -1
1100
+ try {
1101
+ if (body instanceof Blob) {
1102
+ reqByteLength = body.size
1103
+ } else {
1104
+ reqByteLength = Buffer.byteLength(body, STRING_FORMAT)
1105
+ }
1106
+ } catch {
1107
+ if (body instanceof Blob) {
1108
+ reqByteLength = body.size
1109
+ } else {
1110
+ const encoded = new TextEncoder().encode(body)
1111
+ reqByteLength = encoded.length
1112
+ }
1113
+ }
1114
+
1115
+ return await retriable(
1116
+ async () => {
1117
+ let res: PostHogFetchResponse | null = null
1118
+ try {
1119
+ res = await this.fetch(url, {
1120
+ signal: (AbortSignal as any).timeout(requestTimeout ?? this.requestTimeout),
1121
+ ...options,
1122
+ })
1123
+ } catch (e) {
1124
+ // fetch will only throw on network errors or on timeouts
1125
+ throw new PostHogFetchNetworkError(e)
1126
+ }
1127
+ // If we're in no-cors mode, we can't access the response status
1128
+ // We only throw on HTTP errors if we're not in no-cors mode
1129
+ // https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
1130
+ const isNoCors = options.mode === 'no-cors'
1131
+ if (!isNoCors && (res.status < 200 || res.status >= 400)) {
1132
+ throw new PostHogFetchHttpError(res, reqByteLength)
1133
+ }
1134
+ return res
1135
+ },
1136
+ { ...this._retryOptions, ...retryOptions }
1137
+ )
1138
+ }
1139
+
1140
+ async _shutdown(shutdownTimeoutMs: number = 30000): Promise<void> {
1141
+ // A little tricky - we want to have a max shutdown time and enforce it, even if that means we have some
1142
+ // dangling promises. We'll keep track of the timeout and resolve/reject based on that.
1143
+
1144
+ await this._initPromise
1145
+ let hasTimedOut = false
1146
+ this.clearFlushTimer()
1147
+
1148
+ const doShutdown = async (): Promise<void> => {
1149
+ try {
1150
+ await this.promiseQueue.join()
1151
+
1152
+ while (true) {
1153
+ const queue = this.getPersistedProperty<PostHogQueueItem[]>(PostHogPersistedProperty.Queue) || []
1154
+
1155
+ if (queue.length === 0) {
1156
+ break
1157
+ }
1158
+
1159
+ // flush again to make sure we send all events, some of which might've been added
1160
+ // while we were waiting for the pending promises to resolve
1161
+ // For example, see sendFeatureFlags in posthog-node/src/posthog-node.ts::capture
1162
+ await this.flush()
1163
+
1164
+ if (hasTimedOut) {
1165
+ break
1166
+ }
1167
+ }
1168
+ } catch (e) {
1169
+ if (!isPostHogFetchError(e)) {
1170
+ throw e
1171
+ }
1172
+
1173
+ await logFlushError(e)
1174
+ }
1175
+ }
1176
+
1177
+ return Promise.race([
1178
+ new Promise<void>((_, reject) => {
1179
+ safeSetTimeout(() => {
1180
+ this.logMsgIfDebug(() => console.error('Timed out while shutting down PostHog'))
1181
+ hasTimedOut = true
1182
+ reject('Timeout while shutting down PostHog. Some events may not have been sent.')
1183
+ }, shutdownTimeoutMs)
1184
+ }),
1185
+ doShutdown(),
1186
+ ])
1187
+ }
1188
+
1189
+ /**
1190
+ * Shuts down the PostHog instance and ensures all events are sent.
1191
+ *
1192
+ * Call shutdown() once before the process exits to ensure that all events have been sent and all promises
1193
+ * have resolved. Do not use this function if you intend to keep using this PostHog instance after calling it.
1194
+ * Use flush() for per-request cleanup instead.
1195
+ *
1196
+ * {@label Initialization}
1197
+ *
1198
+ * @example
1199
+ * ```js
1200
+ * // shutdown before process exit
1201
+ * process.on('SIGINT', async () => {
1202
+ * await posthog.shutdown()
1203
+ * process.exit(0)
1204
+ * })
1205
+ * ```
1206
+ *
1207
+ * @public
1208
+ *
1209
+ * @param {number} [shutdownTimeoutMs=30000] Maximum time to wait for shutdown in milliseconds
1210
+ * @returns {Promise<void>} A promise that resolves when shutdown is complete
1211
+ */
1212
+ async shutdown(shutdownTimeoutMs: number = 30000): Promise<void> {
1213
+ if (this.shutdownPromise) {
1214
+ this.logMsgIfDebug(() =>
1215
+ console.warn(
1216
+ 'shutdown() called while already shutting down. shutdown() is meant to be called once before process exit - use flush() for per-request cleanup'
1217
+ )
1218
+ )
1219
+ } else {
1220
+ this.shutdownPromise = this._shutdown(shutdownTimeoutMs).finally(() => {
1221
+ this.shutdownPromise = null
1222
+ })
1223
+ }
1224
+ return this.shutdownPromise
1225
+ }
1226
+ }