@posthog/core 1.1.0 → 1.2.1

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 (128) 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.d.ts +6 -6
  18. package/dist/error-tracking/error-properties-builder.d.ts.map +1 -1
  19. package/dist/error-tracking/error-properties-builder.js +17 -27
  20. package/dist/error-tracking/error-properties-builder.mjs +16 -26
  21. package/dist/error-tracking/parsers/index.js +2 -4
  22. package/dist/error-tracking/parsers/index.mjs +2 -4
  23. package/dist/error-tracking/parsers/node.js +3 -5
  24. package/dist/error-tracking/parsers/node.mjs +3 -5
  25. package/dist/error-tracking/utils.js +4 -4
  26. package/dist/error-tracking/utils.mjs +4 -4
  27. package/dist/eventemitter.js +4 -4
  28. package/dist/eventemitter.mjs +4 -4
  29. package/dist/featureFlagUtils.js +20 -45
  30. package/dist/featureFlagUtils.mjs +20 -45
  31. package/dist/gzip.js +1 -2
  32. package/dist/gzip.mjs +1 -2
  33. package/dist/index.d.ts +4 -366
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +54 -1225
  36. package/dist/index.mjs +5 -1190
  37. package/dist/posthog-core-stateless.d.ts +204 -0
  38. package/dist/posthog-core-stateless.d.ts.map +1 -0
  39. package/dist/posthog-core-stateless.js +675 -0
  40. package/dist/posthog-core-stateless.mjs +632 -0
  41. package/dist/posthog-core.d.ts +171 -0
  42. package/dist/posthog-core.d.ts.map +1 -0
  43. package/dist/posthog-core.js +554 -0
  44. package/dist/posthog-core.mjs +520 -0
  45. package/dist/testing/PostHogCoreTestClient.d.ts +2 -1
  46. package/dist/testing/PostHogCoreTestClient.d.ts.map +1 -1
  47. package/dist/testing/PostHogCoreTestClient.js +9 -11
  48. package/dist/testing/PostHogCoreTestClient.mjs +8 -10
  49. package/dist/testing/test-utils.js +1 -1
  50. package/dist/testing/test-utils.mjs +1 -1
  51. package/dist/utils/bucketed-rate-limiter.js +8 -12
  52. package/dist/utils/bucketed-rate-limiter.mjs +8 -12
  53. package/dist/utils/index.js +3 -3
  54. package/dist/utils/index.mjs +3 -3
  55. package/dist/utils/type-utils.js +1 -1
  56. package/dist/utils/type-utils.mjs +1 -1
  57. package/dist/vendor/uuidv7.js +12 -16
  58. package/dist/vendor/uuidv7.mjs +12 -16
  59. package/package.json +3 -2
  60. package/src/__tests__/featureFlagUtils.spec.ts +427 -0
  61. package/src/__tests__/gzip.spec.ts +69 -0
  62. package/src/__tests__/posthog.ai.spec.ts +110 -0
  63. package/src/__tests__/posthog.capture.spec.ts +91 -0
  64. package/src/__tests__/posthog.core.spec.ts +135 -0
  65. package/src/__tests__/posthog.debug.spec.ts +36 -0
  66. package/src/__tests__/posthog.enqueue.spec.ts +93 -0
  67. package/src/__tests__/posthog.featureflags.spec.ts +1106 -0
  68. package/src/__tests__/posthog.featureflags.v1.spec.ts +922 -0
  69. package/src/__tests__/posthog.flush.spec.ts +237 -0
  70. package/src/__tests__/posthog.gdpr.spec.ts +50 -0
  71. package/src/__tests__/posthog.groups.spec.ts +96 -0
  72. package/src/__tests__/posthog.identify.spec.ts +194 -0
  73. package/src/__tests__/posthog.init.spec.ts +110 -0
  74. package/src/__tests__/posthog.listeners.spec.ts +51 -0
  75. package/src/__tests__/posthog.register.spec.ts +47 -0
  76. package/src/__tests__/posthog.reset.spec.ts +76 -0
  77. package/src/__tests__/posthog.sessions.spec.ts +63 -0
  78. package/src/__tests__/posthog.setProperties.spec.ts +102 -0
  79. package/src/__tests__/posthog.shutdown.spec.ts +88 -0
  80. package/src/__tests__/utils.spec.ts +36 -0
  81. package/src/error-tracking/chunk-ids.ts +58 -0
  82. package/src/error-tracking/coercers/dom-exception-coercer.ts +38 -0
  83. package/src/error-tracking/coercers/error-coercer.ts +36 -0
  84. package/src/error-tracking/coercers/error-event-coercer.ts +24 -0
  85. package/src/error-tracking/coercers/event-coercer.ts +19 -0
  86. package/src/error-tracking/coercers/index.ts +8 -0
  87. package/src/error-tracking/coercers/object-coercer.ts +76 -0
  88. package/src/error-tracking/coercers/primitive-coercer.ts +19 -0
  89. package/src/error-tracking/coercers/promise-rejection-event.spec.ts +77 -0
  90. package/src/error-tracking/coercers/promise-rejection-event.ts +53 -0
  91. package/src/error-tracking/coercers/string-coercer.spec.ts +26 -0
  92. package/src/error-tracking/coercers/string-coercer.ts +31 -0
  93. package/src/error-tracking/coercers/utils.ts +33 -0
  94. package/src/error-tracking/error-properties-builder.coerce.spec.ts +202 -0
  95. package/src/error-tracking/error-properties-builder.parse.spec.ts +30 -0
  96. package/src/error-tracking/error-properties-builder.ts +167 -0
  97. package/src/error-tracking/index.ts +5 -0
  98. package/src/error-tracking/parsers/base.ts +29 -0
  99. package/src/error-tracking/parsers/chrome.ts +53 -0
  100. package/src/error-tracking/parsers/gecko.ts +38 -0
  101. package/src/error-tracking/parsers/index.ts +104 -0
  102. package/src/error-tracking/parsers/node.ts +111 -0
  103. package/src/error-tracking/parsers/opera.ts +18 -0
  104. package/src/error-tracking/parsers/react-native.ts +0 -0
  105. package/src/error-tracking/parsers/safari.ts +33 -0
  106. package/src/error-tracking/parsers/winjs.ts +12 -0
  107. package/src/error-tracking/types.ts +107 -0
  108. package/src/error-tracking/utils.ts +39 -0
  109. package/src/eventemitter.ts +27 -0
  110. package/src/featureFlagUtils.ts +192 -0
  111. package/src/gzip.ts +29 -0
  112. package/src/index.ts +8 -0
  113. package/src/posthog-core-stateless.ts +1226 -0
  114. package/src/posthog-core.ts +958 -0
  115. package/src/testing/PostHogCoreTestClient.ts +91 -0
  116. package/src/testing/index.ts +2 -0
  117. package/src/testing/test-utils.ts +47 -0
  118. package/src/types.ts +544 -0
  119. package/src/utils/bucketed-rate-limiter.spec.ts +33 -0
  120. package/src/utils/bucketed-rate-limiter.ts +85 -0
  121. package/src/utils/index.ts +98 -0
  122. package/src/utils/number-utils.spec.ts +89 -0
  123. package/src/utils/number-utils.ts +30 -0
  124. package/src/utils/promise-queue.spec.ts +55 -0
  125. package/src/utils/promise-queue.ts +30 -0
  126. package/src/utils/string-utils.ts +23 -0
  127. package/src/utils/type-utils.ts +134 -0
  128. package/src/vendor/uuidv7.ts +479 -0
@@ -0,0 +1,958 @@
1
+ import type {
2
+ PostHogAutocaptureElement,
3
+ PostHogFlagsResponse,
4
+ PostHogCoreOptions,
5
+ PostHogEventProperties,
6
+ PostHogCaptureOptions,
7
+ JsonType,
8
+ PostHogRemoteConfig,
9
+ FeatureFlagValue,
10
+ PostHogV2FlagsResponse,
11
+ PostHogV1FlagsResponse,
12
+ PostHogFeatureFlagDetails,
13
+ PostHogFlagsStorageFormat,
14
+ FeatureFlagDetail,
15
+ Survey,
16
+ SurveyResponse,
17
+ PostHogGroupProperties,
18
+ } from './types'
19
+ import {
20
+ createFlagsResponseFromFlagsAndPayloads,
21
+ getFeatureFlagValue,
22
+ getFlagValuesFromFlags,
23
+ getPayloadsFromFlags,
24
+ normalizeFlagsResponse,
25
+ updateFlagValue,
26
+ } from './featureFlagUtils'
27
+ import { Compression, PostHogPersistedProperty } from './types'
28
+ import { maybeAdd, PostHogCoreStateless, QuotaLimitedFeature } from './posthog-core-stateless'
29
+ import { uuidv7 } from './vendor/uuidv7'
30
+ import { isPlainError } from './utils'
31
+
32
+ export abstract class PostHogCore extends PostHogCoreStateless {
33
+ // options
34
+ private sendFeatureFlagEvent: boolean
35
+ private flagCallReported: { [key: string]: boolean } = {}
36
+
37
+ // internal
38
+ protected _flagsResponsePromise?: Promise<PostHogFlagsResponse | undefined> // TODO: come back to this, fix typing
39
+ protected _sessionExpirationTimeSeconds: number
40
+ private _sessionMaxLengthSeconds: number = 24 * 60 * 60 // 24 hours
41
+ protected sessionProps: PostHogEventProperties = {}
42
+
43
+ constructor(apiKey: string, options?: PostHogCoreOptions) {
44
+ // Default for stateful mode is to not disable geoip. Only override if explicitly set
45
+ const disableGeoipOption = options?.disableGeoip ?? false
46
+
47
+ // Default for stateful mode is to timeout at 10s. Only override if explicitly set
48
+ const featureFlagsRequestTimeoutMs = options?.featureFlagsRequestTimeoutMs ?? 10000 // 10 seconds
49
+
50
+ super(apiKey, { ...options, disableGeoip: disableGeoipOption, featureFlagsRequestTimeoutMs })
51
+
52
+ this.sendFeatureFlagEvent = options?.sendFeatureFlagEvent ?? true
53
+ this._sessionExpirationTimeSeconds = options?.sessionExpirationTimeSeconds ?? 1800 // 30 minutes
54
+ }
55
+
56
+ protected setupBootstrap(options?: Partial<PostHogCoreOptions>): void {
57
+ const bootstrap = options?.bootstrap
58
+ if (!bootstrap) {
59
+ return
60
+ }
61
+
62
+ // bootstrap options are only set if no persisted values are found
63
+ // this is to prevent overwriting existing values
64
+ if (bootstrap.distinctId) {
65
+ if (bootstrap.isIdentifiedId) {
66
+ const distinctId = this.getPersistedProperty(PostHogPersistedProperty.DistinctId)
67
+
68
+ if (!distinctId) {
69
+ this.setPersistedProperty(PostHogPersistedProperty.DistinctId, bootstrap.distinctId)
70
+ }
71
+ } else {
72
+ const anonymousId = this.getPersistedProperty(PostHogPersistedProperty.AnonymousId)
73
+
74
+ if (!anonymousId) {
75
+ this.setPersistedProperty(PostHogPersistedProperty.AnonymousId, bootstrap.distinctId)
76
+ }
77
+ }
78
+ }
79
+
80
+ const bootstrapFeatureFlags = bootstrap.featureFlags
81
+ const bootstrapFeatureFlagPayloads = bootstrap.featureFlagPayloads ?? {}
82
+ if (bootstrapFeatureFlags && Object.keys(bootstrapFeatureFlags).length) {
83
+ const normalizedBootstrapFeatureFlagDetails = createFlagsResponseFromFlagsAndPayloads(
84
+ bootstrapFeatureFlags,
85
+ bootstrapFeatureFlagPayloads
86
+ )
87
+
88
+ if (Object.keys(normalizedBootstrapFeatureFlagDetails.flags).length > 0) {
89
+ this.setBootstrappedFeatureFlagDetails(normalizedBootstrapFeatureFlagDetails)
90
+
91
+ const currentFeatureFlagDetails = this.getKnownFeatureFlagDetails() || { flags: {}, requestId: undefined }
92
+ const newFeatureFlagDetails = {
93
+ flags: {
94
+ ...normalizedBootstrapFeatureFlagDetails.flags,
95
+ ...currentFeatureFlagDetails.flags,
96
+ },
97
+ requestId: normalizedBootstrapFeatureFlagDetails.requestId,
98
+ }
99
+
100
+ this.setKnownFeatureFlagDetails(newFeatureFlagDetails)
101
+ }
102
+ }
103
+ }
104
+
105
+ private clearProps(): void {
106
+ this.props = undefined
107
+ this.sessionProps = {}
108
+ this.flagCallReported = {}
109
+ }
110
+
111
+ on(event: string, cb: (...args: any[]) => void): () => void {
112
+ return this._events.on(event, cb)
113
+ }
114
+
115
+ reset(propertiesToKeep?: PostHogPersistedProperty[]): void {
116
+ this.wrap(() => {
117
+ const allPropertiesToKeep = [PostHogPersistedProperty.Queue, ...(propertiesToKeep || [])]
118
+
119
+ // clean up props
120
+ this.clearProps()
121
+
122
+ for (const key of <(keyof typeof PostHogPersistedProperty)[]>Object.keys(PostHogPersistedProperty)) {
123
+ if (!allPropertiesToKeep.includes(PostHogPersistedProperty[key])) {
124
+ this.setPersistedProperty((PostHogPersistedProperty as any)[key], null)
125
+ }
126
+ }
127
+
128
+ this.reloadFeatureFlags()
129
+ })
130
+ }
131
+
132
+ protected getCommonEventProperties(): PostHogEventProperties {
133
+ const featureFlags = this.getFeatureFlags()
134
+
135
+ const featureVariantProperties: Record<string, FeatureFlagValue> = {}
136
+ if (featureFlags) {
137
+ for (const [feature, variant] of Object.entries(featureFlags)) {
138
+ featureVariantProperties[`$feature/${feature}`] = variant
139
+ }
140
+ }
141
+ return {
142
+ ...maybeAdd('$active_feature_flags', featureFlags ? Object.keys(featureFlags) : undefined),
143
+ ...featureVariantProperties,
144
+ ...super.getCommonEventProperties(),
145
+ }
146
+ }
147
+
148
+ private enrichProperties(properties?: PostHogEventProperties): PostHogEventProperties {
149
+ return {
150
+ ...this.props, // Persisted properties first
151
+ ...this.sessionProps, // Followed by session properties
152
+ ...(properties || {}), // Followed by user specified properties
153
+ ...this.getCommonEventProperties(), // Followed by FF props
154
+ $session_id: this.getSessionId(),
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Returns the current session_id.
160
+ *
161
+ * @remarks
162
+ * This should only be used for informative purposes.
163
+ * Any actual internal use case for the session_id should be handled by the sessionManager.
164
+ *
165
+ * @public
166
+ *
167
+ * @returns The stored session ID for the current session. This may be an empty string if the client is not yet fully initialized.
168
+ */
169
+ getSessionId(): string {
170
+ if (!this._isInitialized) {
171
+ return ''
172
+ }
173
+
174
+ let sessionId = this.getPersistedProperty<string>(PostHogPersistedProperty.SessionId)
175
+ const sessionLastTimestamp = this.getPersistedProperty<number>(PostHogPersistedProperty.SessionLastTimestamp) || 0
176
+ const sessionStartTimestamp = this.getPersistedProperty<number>(PostHogPersistedProperty.SessionStartTimestamp) || 0
177
+ const now = Date.now()
178
+ const sessionLastDif = now - sessionLastTimestamp
179
+ const sessionStartDif = now - sessionStartTimestamp
180
+ if (
181
+ !sessionId ||
182
+ sessionLastDif > this._sessionExpirationTimeSeconds * 1000 ||
183
+ sessionStartDif > this._sessionMaxLengthSeconds * 1000
184
+ ) {
185
+ sessionId = uuidv7()
186
+ this.setPersistedProperty(PostHogPersistedProperty.SessionId, sessionId)
187
+ this.setPersistedProperty(PostHogPersistedProperty.SessionStartTimestamp, now)
188
+ }
189
+ this.setPersistedProperty(PostHogPersistedProperty.SessionLastTimestamp, now)
190
+
191
+ return sessionId
192
+ }
193
+
194
+ resetSessionId(): void {
195
+ this.wrap(() => {
196
+ this.setPersistedProperty(PostHogPersistedProperty.SessionId, null)
197
+ this.setPersistedProperty(PostHogPersistedProperty.SessionLastTimestamp, null)
198
+ this.setPersistedProperty(PostHogPersistedProperty.SessionStartTimestamp, null)
199
+ })
200
+ }
201
+
202
+ /**
203
+ * Returns the current anonymous ID.
204
+ *
205
+ * This is the ID assigned to users before they are identified. It's used to track
206
+ * anonymous users and link them to identified users when they sign up.
207
+ *
208
+ * {@label Identification}
209
+ *
210
+ * @example
211
+ * ```js
212
+ * // get the anonymous ID
213
+ * const anonId = posthog.getAnonymousId()
214
+ * console.log('Anonymous ID:', anonId)
215
+ * ```
216
+ *
217
+ * @public
218
+ *
219
+ * @returns {string} The stored anonymous ID. This may be an empty string if the client is not yet fully initialized.
220
+ */
221
+ getAnonymousId(): string {
222
+ if (!this._isInitialized) {
223
+ return ''
224
+ }
225
+
226
+ let anonId = this.getPersistedProperty<string>(PostHogPersistedProperty.AnonymousId)
227
+ if (!anonId) {
228
+ anonId = uuidv7()
229
+ this.setPersistedProperty(PostHogPersistedProperty.AnonymousId, anonId)
230
+ }
231
+ return anonId
232
+ }
233
+
234
+ /**
235
+ * * @returns {string} The stored distinct ID. This may be an empty string if the client is not yet fully initialized.
236
+ */
237
+ getDistinctId(): string {
238
+ if (!this._isInitialized) {
239
+ return ''
240
+ }
241
+
242
+ return this.getPersistedProperty<string>(PostHogPersistedProperty.DistinctId) || this.getAnonymousId()
243
+ }
244
+
245
+ registerForSession(properties: PostHogEventProperties): void {
246
+ this.sessionProps = {
247
+ ...this.sessionProps,
248
+ ...properties,
249
+ }
250
+ }
251
+
252
+ unregisterForSession(property: string): void {
253
+ delete this.sessionProps[property]
254
+ }
255
+
256
+ /***
257
+ *** TRACKING
258
+ ***/
259
+
260
+ identify(distinctId?: string, properties?: PostHogEventProperties, options?: PostHogCaptureOptions): void {
261
+ this.wrap(() => {
262
+ const previousDistinctId = this.getDistinctId()
263
+ distinctId = distinctId || previousDistinctId
264
+
265
+ if (properties?.$groups) {
266
+ this.groups(properties.$groups as PostHogGroupProperties)
267
+ }
268
+
269
+ // promote $set and $set_once to top level
270
+ const userPropsOnce = properties?.$set_once
271
+ delete properties?.$set_once
272
+
273
+ // if no $set is provided we assume all properties are $set
274
+ const userProps = properties?.$set || properties
275
+
276
+ const allProperties = this.enrichProperties({
277
+ $anon_distinct_id: this.getAnonymousId(),
278
+ ...maybeAdd('$set', userProps),
279
+ ...maybeAdd('$set_once', userPropsOnce),
280
+ })
281
+
282
+ if (distinctId !== previousDistinctId) {
283
+ // We keep the AnonymousId to be used by flags calls and identify to link the previousId
284
+ this.setPersistedProperty(PostHogPersistedProperty.AnonymousId, previousDistinctId)
285
+ this.setPersistedProperty(PostHogPersistedProperty.DistinctId, distinctId)
286
+ this.reloadFeatureFlags()
287
+ }
288
+
289
+ super.identifyStateless(distinctId, allProperties, options)
290
+ })
291
+ }
292
+
293
+ capture(event: string, properties?: PostHogEventProperties, options?: PostHogCaptureOptions): void {
294
+ this.wrap(() => {
295
+ const distinctId = this.getDistinctId()
296
+
297
+ if (properties?.$groups) {
298
+ this.groups(properties.$groups as PostHogGroupProperties)
299
+ }
300
+
301
+ const allProperties = this.enrichProperties(properties)
302
+
303
+ super.captureStateless(distinctId, event, allProperties, options)
304
+ })
305
+ }
306
+
307
+ alias(alias: string): void {
308
+ this.wrap(() => {
309
+ const distinctId = this.getDistinctId()
310
+ const allProperties = this.enrichProperties({})
311
+
312
+ super.aliasStateless(alias, distinctId, allProperties)
313
+ })
314
+ }
315
+
316
+ autocapture(
317
+ eventType: string,
318
+ elements: PostHogAutocaptureElement[],
319
+ properties: PostHogEventProperties = {},
320
+ options?: PostHogCaptureOptions
321
+ ): void {
322
+ this.wrap(() => {
323
+ const distinctId = this.getDistinctId()
324
+ const payload = {
325
+ distinct_id: distinctId,
326
+ event: '$autocapture',
327
+ properties: {
328
+ ...this.enrichProperties(properties),
329
+ $event_type: eventType,
330
+ $elements: elements,
331
+ },
332
+ }
333
+
334
+ this.enqueue('autocapture', payload, options)
335
+ })
336
+ }
337
+
338
+ /***
339
+ *** GROUPS
340
+ ***/
341
+
342
+ groups(groups: PostHogGroupProperties): void {
343
+ this.wrap(() => {
344
+ // Get persisted groups
345
+ const existingGroups = this.props.$groups || {}
346
+
347
+ this.register({
348
+ $groups: {
349
+ ...(existingGroups as PostHogGroupProperties),
350
+ ...groups,
351
+ },
352
+ })
353
+
354
+ if (Object.keys(groups).find((type) => existingGroups[type as keyof typeof existingGroups] !== groups[type])) {
355
+ this.reloadFeatureFlags()
356
+ }
357
+ })
358
+ }
359
+
360
+ group(
361
+ groupType: string,
362
+ groupKey: string | number,
363
+ groupProperties?: PostHogEventProperties,
364
+ options?: PostHogCaptureOptions
365
+ ): void {
366
+ this.wrap(() => {
367
+ this.groups({
368
+ [groupType]: groupKey,
369
+ })
370
+
371
+ if (groupProperties) {
372
+ this.groupIdentify(groupType, groupKey, groupProperties, options)
373
+ }
374
+ })
375
+ }
376
+
377
+ groupIdentify(
378
+ groupType: string,
379
+ groupKey: string | number,
380
+ groupProperties?: PostHogEventProperties,
381
+ options?: PostHogCaptureOptions
382
+ ): void {
383
+ this.wrap(() => {
384
+ const distinctId = this.getDistinctId()
385
+ const eventProperties = this.enrichProperties({})
386
+ super.groupIdentifyStateless(groupType, groupKey, groupProperties, options, distinctId, eventProperties)
387
+ })
388
+ }
389
+
390
+ /***
391
+ * PROPERTIES
392
+ ***/
393
+ setPersonPropertiesForFlags(properties: { [type: string]: string }): void {
394
+ this.wrap(() => {
395
+ // Get persisted person properties
396
+ const existingProperties =
397
+ this.getPersistedProperty<Record<string, string>>(PostHogPersistedProperty.PersonProperties) || {}
398
+
399
+ this.setPersistedProperty<PostHogEventProperties>(PostHogPersistedProperty.PersonProperties, {
400
+ ...existingProperties,
401
+ ...properties,
402
+ })
403
+ })
404
+ }
405
+
406
+ resetPersonPropertiesForFlags(): void {
407
+ this.wrap(() => {
408
+ this.setPersistedProperty<PostHogEventProperties>(PostHogPersistedProperty.PersonProperties, null)
409
+ })
410
+ }
411
+
412
+ setGroupPropertiesForFlags(properties: { [type: string]: Record<string, string> }): void {
413
+ this.wrap(() => {
414
+ // Get persisted group properties
415
+ const existingProperties =
416
+ this.getPersistedProperty<Record<string, Record<string, string>>>(PostHogPersistedProperty.GroupProperties) ||
417
+ {}
418
+
419
+ if (Object.keys(existingProperties).length !== 0) {
420
+ Object.keys(existingProperties).forEach((groupType) => {
421
+ existingProperties[groupType] = {
422
+ ...existingProperties[groupType],
423
+ ...properties[groupType],
424
+ }
425
+ delete properties[groupType]
426
+ })
427
+ }
428
+
429
+ this.setPersistedProperty<PostHogEventProperties>(PostHogPersistedProperty.GroupProperties, {
430
+ ...existingProperties,
431
+ ...properties,
432
+ })
433
+ })
434
+ }
435
+
436
+ resetGroupPropertiesForFlags(): void {
437
+ this.wrap(() => {
438
+ this.setPersistedProperty<PostHogEventProperties>(PostHogPersistedProperty.GroupProperties, null)
439
+ })
440
+ }
441
+
442
+ private async remoteConfigAsync(): Promise<PostHogRemoteConfig | undefined> {
443
+ await this._initPromise
444
+ if (this._remoteConfigResponsePromise) {
445
+ return this._remoteConfigResponsePromise
446
+ }
447
+ return this._remoteConfigAsync()
448
+ }
449
+
450
+ /***
451
+ *** FEATURE FLAGS
452
+ ***/
453
+ private async flagsAsync(sendAnonDistinctId: boolean = true): Promise<PostHogFlagsResponse | undefined> {
454
+ await this._initPromise
455
+ if (this._flagsResponsePromise) {
456
+ return this._flagsResponsePromise
457
+ }
458
+ return this._flagsAsync(sendAnonDistinctId)
459
+ }
460
+
461
+ private cacheSessionReplay(source: string, response?: PostHogRemoteConfig): void {
462
+ const sessionReplay = response?.sessionRecording
463
+ if (sessionReplay) {
464
+ this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, sessionReplay)
465
+ this.logMsgIfDebug(() =>
466
+ console.log('PostHog Debug', `Session replay config from ${source}: `, JSON.stringify(sessionReplay))
467
+ )
468
+ } else if (typeof sessionReplay === 'boolean' && sessionReplay === false) {
469
+ // if session replay is disabled, we don't need to cache it
470
+ // we need to check for this because the response might be undefined (/flags does not return sessionRecording yet)
471
+ this.logMsgIfDebug(() => console.info('PostHog Debug', `Session replay config from ${source} disabled.`))
472
+ this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, null)
473
+ }
474
+ }
475
+
476
+ private async _remoteConfigAsync(): Promise<PostHogRemoteConfig | undefined> {
477
+ this._remoteConfigResponsePromise = this._initPromise
478
+ .then(() => {
479
+ let remoteConfig = this.getPersistedProperty<Omit<PostHogRemoteConfig, 'surveys'>>(
480
+ PostHogPersistedProperty.RemoteConfig
481
+ )
482
+
483
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached remote config: ', JSON.stringify(remoteConfig)))
484
+
485
+ return super.getRemoteConfig().then((response) => {
486
+ if (response) {
487
+ const remoteConfigWithoutSurveys = { ...response }
488
+ delete remoteConfigWithoutSurveys.surveys
489
+
490
+ this.logMsgIfDebug(() =>
491
+ console.log('PostHog Debug', 'Fetched remote config: ', JSON.stringify(remoteConfigWithoutSurveys))
492
+ )
493
+
494
+ if (this.disableSurveys === false) {
495
+ const surveys = response.surveys
496
+
497
+ let hasSurveys = true
498
+
499
+ if (!Array.isArray(surveys)) {
500
+ // If surveys is not an array, it means there are no surveys (its a boolean instead)
501
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'There are no surveys.'))
502
+ hasSurveys = false
503
+ } else {
504
+ this.logMsgIfDebug(() =>
505
+ console.log('PostHog Debug', 'Surveys fetched from remote config: ', JSON.stringify(surveys))
506
+ )
507
+ }
508
+
509
+ if (hasSurveys) {
510
+ this.setPersistedProperty<SurveyResponse['surveys']>(
511
+ PostHogPersistedProperty.Surveys,
512
+ surveys as Survey[]
513
+ )
514
+ } else {
515
+ this.setPersistedProperty<SurveyResponse['surveys']>(PostHogPersistedProperty.Surveys, null)
516
+ }
517
+ } else {
518
+ this.setPersistedProperty<SurveyResponse['surveys']>(PostHogPersistedProperty.Surveys, null)
519
+ }
520
+ // we cache the surveys in its own storage key
521
+ this.setPersistedProperty<Omit<PostHogRemoteConfig, 'surveys'>>(
522
+ PostHogPersistedProperty.RemoteConfig,
523
+ remoteConfigWithoutSurveys
524
+ )
525
+
526
+ this.cacheSessionReplay('remote config', response)
527
+
528
+ // we only dont load flags if the remote config has no feature flags
529
+ if (response.hasFeatureFlags === false) {
530
+ // resetting flags to empty object
531
+ this.setKnownFeatureFlagDetails({ flags: {} })
532
+
533
+ this.logMsgIfDebug(() => console.warn('Remote config has no feature flags, will not load feature flags.'))
534
+ } else if (this.preloadFeatureFlags !== false) {
535
+ this.reloadFeatureFlags()
536
+ }
537
+
538
+ if (!response.supportedCompression?.includes(Compression.GZipJS)) {
539
+ this.disableCompression = true
540
+ }
541
+
542
+ remoteConfig = response
543
+ }
544
+
545
+ return remoteConfig
546
+ })
547
+ })
548
+ .finally(() => {
549
+ this._remoteConfigResponsePromise = undefined
550
+ })
551
+ return this._remoteConfigResponsePromise
552
+ }
553
+
554
+ private async _flagsAsync(sendAnonDistinctId: boolean = true): Promise<PostHogFlagsResponse | undefined> {
555
+ this._flagsResponsePromise = this._initPromise
556
+ .then(async () => {
557
+ const distinctId = this.getDistinctId()
558
+ const groups = this.props.$groups || {}
559
+ const personProperties =
560
+ this.getPersistedProperty<Record<string, string>>(PostHogPersistedProperty.PersonProperties) || {}
561
+ const groupProperties =
562
+ this.getPersistedProperty<Record<string, Record<string, string>>>(PostHogPersistedProperty.GroupProperties) ||
563
+ {}
564
+
565
+ const extraProperties = {
566
+ $anon_distinct_id: sendAnonDistinctId ? this.getAnonymousId() : undefined,
567
+ }
568
+
569
+ const res = await super.getFlags(
570
+ distinctId,
571
+ groups as PostHogGroupProperties,
572
+ personProperties,
573
+ groupProperties,
574
+ extraProperties
575
+ )
576
+ // Add check for quota limitation on feature flags
577
+ if (res?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
578
+ // Unset all feature flags by setting to null
579
+ this.setKnownFeatureFlagDetails(null)
580
+ console.warn(
581
+ '[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts'
582
+ )
583
+ return res
584
+ }
585
+ if (res?.featureFlags) {
586
+ // clear flag call reported if we have new flags since they might have changed
587
+ if (this.sendFeatureFlagEvent) {
588
+ this.flagCallReported = {}
589
+ }
590
+
591
+ let newFeatureFlagDetails = res
592
+ if (res.errorsWhileComputingFlags) {
593
+ // if not all flags were computed, we upsert flags instead of replacing them
594
+ const currentFlagDetails = this.getKnownFeatureFlagDetails()
595
+ this.logMsgIfDebug(() =>
596
+ console.log('PostHog Debug', 'Cached feature flags: ', JSON.stringify(currentFlagDetails))
597
+ )
598
+
599
+ newFeatureFlagDetails = {
600
+ ...res,
601
+ flags: { ...currentFlagDetails?.flags, ...res.flags },
602
+ }
603
+ }
604
+ this.setKnownFeatureFlagDetails(newFeatureFlagDetails)
605
+ // Mark that we hit the /flags endpoint so we can capture this in the $feature_flag_called event
606
+ this.setPersistedProperty(PostHogPersistedProperty.FlagsEndpointWasHit, true)
607
+ this.cacheSessionReplay('flags', res)
608
+ }
609
+ return res
610
+ })
611
+ .finally(() => {
612
+ this._flagsResponsePromise = undefined
613
+ })
614
+ return this._flagsResponsePromise
615
+ }
616
+
617
+ // We only store the flags and request id in the feature flag details storage key
618
+ private setKnownFeatureFlagDetails(flagsResponse: PostHogFlagsStorageFormat | null): void {
619
+ this.wrap(() => {
620
+ this.setPersistedProperty<PostHogFlagsStorageFormat>(PostHogPersistedProperty.FeatureFlagDetails, flagsResponse)
621
+
622
+ this._events.emit('featureflags', getFlagValuesFromFlags(flagsResponse?.flags ?? {}))
623
+ })
624
+ }
625
+
626
+ private getKnownFeatureFlagDetails(): PostHogFeatureFlagDetails | undefined {
627
+ const storedDetails = this.getPersistedProperty<PostHogFlagsStorageFormat>(
628
+ PostHogPersistedProperty.FeatureFlagDetails
629
+ )
630
+ if (!storedDetails) {
631
+ // Rebuild from the stored feature flags and feature flag payloads
632
+ const featureFlags = this.getPersistedProperty<PostHogFlagsResponse['featureFlags']>(
633
+ PostHogPersistedProperty.FeatureFlags
634
+ )
635
+ const featureFlagPayloads = this.getPersistedProperty<PostHogFlagsResponse['featureFlagPayloads']>(
636
+ PostHogPersistedProperty.FeatureFlagPayloads
637
+ )
638
+
639
+ if (featureFlags === undefined && featureFlagPayloads === undefined) {
640
+ return undefined
641
+ }
642
+
643
+ return createFlagsResponseFromFlagsAndPayloads(featureFlags ?? {}, featureFlagPayloads ?? {})
644
+ }
645
+
646
+ return normalizeFlagsResponse(
647
+ storedDetails as PostHogV1FlagsResponse | PostHogV2FlagsResponse
648
+ ) as PostHogFeatureFlagDetails
649
+ }
650
+
651
+ protected getKnownFeatureFlags(): PostHogFlagsResponse['featureFlags'] | undefined {
652
+ const featureFlagDetails = this.getKnownFeatureFlagDetails()
653
+ if (!featureFlagDetails) {
654
+ return undefined
655
+ }
656
+ return getFlagValuesFromFlags(featureFlagDetails.flags)
657
+ }
658
+
659
+ private getKnownFeatureFlagPayloads(): PostHogFlagsResponse['featureFlagPayloads'] | undefined {
660
+ const featureFlagDetails = this.getKnownFeatureFlagDetails()
661
+ if (!featureFlagDetails) {
662
+ return undefined
663
+ }
664
+ return getPayloadsFromFlags(featureFlagDetails.flags)
665
+ }
666
+
667
+ private getBootstrappedFeatureFlagDetails(): PostHogFeatureFlagDetails | undefined {
668
+ const details = this.getPersistedProperty<PostHogFeatureFlagDetails>(
669
+ PostHogPersistedProperty.BootstrapFeatureFlagDetails
670
+ )
671
+ if (!details) {
672
+ return undefined
673
+ }
674
+ return details
675
+ }
676
+
677
+ private setBootstrappedFeatureFlagDetails(details: PostHogFeatureFlagDetails): void {
678
+ this.setPersistedProperty<PostHogFeatureFlagDetails>(PostHogPersistedProperty.BootstrapFeatureFlagDetails, details)
679
+ }
680
+
681
+ private getBootstrappedFeatureFlags(): PostHogFlagsResponse['featureFlags'] | undefined {
682
+ const details = this.getBootstrappedFeatureFlagDetails()
683
+ if (!details) {
684
+ return undefined
685
+ }
686
+ return getFlagValuesFromFlags(details.flags)
687
+ }
688
+
689
+ private getBootstrappedFeatureFlagPayloads(): PostHogFlagsResponse['featureFlagPayloads'] | undefined {
690
+ const details = this.getBootstrappedFeatureFlagDetails()
691
+ if (!details) {
692
+ return undefined
693
+ }
694
+ return getPayloadsFromFlags(details.flags)
695
+ }
696
+
697
+ getFeatureFlag(key: string): FeatureFlagValue | undefined {
698
+ const details = this.getFeatureFlagDetails()
699
+
700
+ if (!details) {
701
+ // If we haven't loaded flags yet, or errored out, we respond with undefined
702
+ return undefined
703
+ }
704
+
705
+ const featureFlag = details.flags[key]
706
+
707
+ let response = getFeatureFlagValue(featureFlag)
708
+
709
+ if (response === undefined) {
710
+ // For cases where the flag is unknown, return false
711
+ response = false
712
+ }
713
+
714
+ if (this.sendFeatureFlagEvent && !this.flagCallReported[key]) {
715
+ const bootstrappedResponse = this.getBootstrappedFeatureFlags()?.[key]
716
+ const bootstrappedPayload = this.getBootstrappedFeatureFlagPayloads()?.[key]
717
+
718
+ this.flagCallReported[key] = true
719
+ this.capture('$feature_flag_called', {
720
+ $feature_flag: key,
721
+ $feature_flag_response: response,
722
+ ...maybeAdd('$feature_flag_id', featureFlag?.metadata?.id),
723
+ ...maybeAdd('$feature_flag_version', featureFlag?.metadata?.version),
724
+ ...maybeAdd('$feature_flag_reason', featureFlag?.reason?.description ?? featureFlag?.reason?.code),
725
+ ...maybeAdd('$feature_flag_bootstrapped_response', bootstrappedResponse),
726
+ ...maybeAdd('$feature_flag_bootstrapped_payload', bootstrappedPayload),
727
+ // If we haven't yet received a response from the /flags endpoint, we must have used the bootstrapped value
728
+ $used_bootstrap_value: !this.getPersistedProperty(PostHogPersistedProperty.FlagsEndpointWasHit),
729
+ ...maybeAdd('$feature_flag_request_id', details.requestId),
730
+ })
731
+ }
732
+
733
+ // If we have flags we either return the value (true or string) or false
734
+ return response
735
+ }
736
+
737
+ getFeatureFlagPayload(key: string): JsonType | undefined {
738
+ const payloads = this.getFeatureFlagPayloads()
739
+
740
+ if (!payloads) {
741
+ return undefined
742
+ }
743
+
744
+ const response = payloads[key]
745
+
746
+ // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
747
+ if (response === undefined) {
748
+ return null
749
+ }
750
+
751
+ return response
752
+ }
753
+
754
+ getFeatureFlagPayloads(): PostHogFlagsResponse['featureFlagPayloads'] | undefined {
755
+ return this.getFeatureFlagDetails()?.featureFlagPayloads
756
+ }
757
+
758
+ getFeatureFlags(): PostHogFlagsResponse['featureFlags'] | undefined {
759
+ // NOTE: We don't check for _initPromise here as the function is designed to be
760
+ // callable before the state being loaded anyways
761
+ return this.getFeatureFlagDetails()?.featureFlags
762
+ }
763
+
764
+ getFeatureFlagDetails(): PostHogFeatureFlagDetails | undefined {
765
+ // NOTE: We don't check for _initPromise here as the function is designed to be
766
+ // callable before the state being loaded anyways
767
+ let details = this.getKnownFeatureFlagDetails()
768
+ const overriddenFlags = this.getPersistedProperty<PostHogFlagsResponse['featureFlags']>(
769
+ PostHogPersistedProperty.OverrideFeatureFlags
770
+ )
771
+
772
+ if (!overriddenFlags) {
773
+ return details
774
+ }
775
+
776
+ details = details ?? { featureFlags: {}, featureFlagPayloads: {}, flags: {} }
777
+
778
+ const flags: Record<string, FeatureFlagDetail> = details.flags ?? {}
779
+
780
+ for (const key in overriddenFlags) {
781
+ if (!overriddenFlags[key]) {
782
+ delete flags[key]
783
+ } else {
784
+ flags[key] = updateFlagValue(flags[key], overriddenFlags[key])
785
+ }
786
+ }
787
+
788
+ const result = {
789
+ ...details,
790
+ flags,
791
+ }
792
+
793
+ return normalizeFlagsResponse(result) as PostHogFeatureFlagDetails
794
+ }
795
+
796
+ getFeatureFlagsAndPayloads(): {
797
+ flags: PostHogFlagsResponse['featureFlags'] | undefined
798
+ payloads: PostHogFlagsResponse['featureFlagPayloads'] | undefined
799
+ } {
800
+ const flags = this.getFeatureFlags()
801
+ const payloads = this.getFeatureFlagPayloads()
802
+
803
+ return {
804
+ flags,
805
+ payloads,
806
+ }
807
+ }
808
+
809
+ isFeatureEnabled(key: string): boolean | undefined {
810
+ const response = this.getFeatureFlag(key)
811
+ if (response === undefined) {
812
+ return undefined
813
+ }
814
+ return !!response
815
+ }
816
+
817
+ // Used when we want to trigger the reload but we don't care about the result
818
+ reloadFeatureFlags(options?: { cb?: (err?: Error, flags?: PostHogFlagsResponse['featureFlags']) => void }): void {
819
+ this.flagsAsync(true)
820
+ .then((res) => {
821
+ options?.cb?.(undefined, res?.featureFlags)
822
+ })
823
+ .catch((e) => {
824
+ options?.cb?.(e, undefined)
825
+ if (!options?.cb) {
826
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'Error reloading feature flags', e))
827
+ }
828
+ })
829
+ }
830
+
831
+ async reloadRemoteConfigAsync(): Promise<PostHogRemoteConfig | undefined> {
832
+ return await this.remoteConfigAsync()
833
+ }
834
+
835
+ async reloadFeatureFlagsAsync(
836
+ sendAnonDistinctId?: boolean
837
+ ): Promise<PostHogFlagsResponse['featureFlags'] | undefined> {
838
+ return (await this.flagsAsync(sendAnonDistinctId ?? true))?.featureFlags
839
+ }
840
+
841
+ onFeatureFlags(cb: (flags: PostHogFlagsResponse['featureFlags']) => void): () => void {
842
+ return this.on('featureflags', async () => {
843
+ const flags = this.getFeatureFlags()
844
+ if (flags) {
845
+ cb(flags)
846
+ }
847
+ })
848
+ }
849
+
850
+ onFeatureFlag(key: string, cb: (value: FeatureFlagValue) => void): () => void {
851
+ return this.on('featureflags', async () => {
852
+ const flagResponse = this.getFeatureFlag(key)
853
+ if (flagResponse !== undefined) {
854
+ cb(flagResponse)
855
+ }
856
+ })
857
+ }
858
+
859
+ async overrideFeatureFlag(flags: PostHogFlagsResponse['featureFlags'] | null): Promise<void> {
860
+ this.wrap(() => {
861
+ if (flags === null) {
862
+ return this.setPersistedProperty(PostHogPersistedProperty.OverrideFeatureFlags, null)
863
+ }
864
+ return this.setPersistedProperty(PostHogPersistedProperty.OverrideFeatureFlags, flags)
865
+ })
866
+ }
867
+
868
+ /**
869
+ * Capture a caught exception manually
870
+ *
871
+ * {@label Error tracking}
872
+ *
873
+ * @public
874
+ *
875
+ * @example
876
+ * ```js
877
+ * // Capture a caught exception
878
+ * try {
879
+ * // something that might throw
880
+ * } catch (error) {
881
+ * posthog.captureException(error)
882
+ * }
883
+ * ```
884
+ *
885
+ * @example
886
+ * ```js
887
+ * // With additional properties
888
+ * posthog.captureException(error, {
889
+ * customProperty: 'value',
890
+ * anotherProperty: ['I', 'can be a list'],
891
+ * ...
892
+ * })
893
+ * ```
894
+ *
895
+ * @param {Error} error The error to capture
896
+ * @param {Object} [additionalProperties] Any additional properties to add to the error event
897
+ * @returns {CaptureResult} The result of the capture
898
+ */
899
+ captureException(error: unknown, additionalProperties?: PostHogEventProperties): void {
900
+ const properties: { [key: string]: any } = {
901
+ $exception_level: 'error',
902
+ $exception_list: [
903
+ {
904
+ type: isPlainError(error) ? error.name : 'Error',
905
+ value: isPlainError(error) ? error.message : error,
906
+ mechanism: {
907
+ handled: true,
908
+ synthetic: false,
909
+ },
910
+ },
911
+ ],
912
+ ...additionalProperties,
913
+ }
914
+
915
+ properties.$exception_personURL = new URL(
916
+ `/project/${this.apiKey}/person/${this.getDistinctId()}`,
917
+ this.host
918
+ ).toString()
919
+
920
+ this.capture('$exception', properties)
921
+ }
922
+
923
+ /**
924
+ * Capture written user feedback for a LLM trace. Numeric values are converted to strings.
925
+ *
926
+ * {@label LLM analytics}
927
+ *
928
+ * @public
929
+ *
930
+ * @param traceId The trace ID to capture feedback for.
931
+ * @param userFeedback The feedback to capture.
932
+ */
933
+ captureTraceFeedback(traceId: string | number, userFeedback: string): void {
934
+ this.capture('$ai_feedback', {
935
+ $ai_feedback_text: userFeedback,
936
+ $ai_trace_id: String(traceId),
937
+ })
938
+ }
939
+
940
+ /**
941
+ * Capture a metric for a LLM trace. Numeric values are converted to strings.
942
+ *
943
+ * {@label LLM analytics}
944
+ *
945
+ * @public
946
+ *
947
+ * @param traceId The trace ID to capture the metric for.
948
+ * @param metricName The name of the metric to capture.
949
+ * @param metricValue The value of the metric to capture.
950
+ */
951
+ captureTraceMetric(traceId: string | number, metricName: string, metricValue: string | number | boolean): void {
952
+ this.capture('$ai_metric', {
953
+ $ai_metric_name: metricName,
954
+ $ai_metric_value: String(metricValue),
955
+ $ai_trace_id: String(traceId),
956
+ })
957
+ }
958
+ }