@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.
- package/dist/error-tracking/chunk-ids.js +1 -1
- package/dist/error-tracking/chunk-ids.mjs +1 -1
- package/dist/error-tracking/coercers/error-event-coercer.js +4 -5
- package/dist/error-tracking/coercers/error-event-coercer.mjs +4 -5
- package/dist/error-tracking/coercers/event-coercer.js +1 -2
- package/dist/error-tracking/coercers/event-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/object-coercer.js +1 -2
- package/dist/error-tracking/coercers/object-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/primitive-coercer.js +1 -2
- package/dist/error-tracking/coercers/primitive-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/promise-rejection-event.js +4 -5
- package/dist/error-tracking/coercers/promise-rejection-event.mjs +4 -5
- package/dist/error-tracking/coercers/string-coercer.js +3 -4
- package/dist/error-tracking/coercers/string-coercer.mjs +3 -4
- package/dist/error-tracking/coercers/utils.js +2 -4
- package/dist/error-tracking/coercers/utils.mjs +2 -4
- package/dist/error-tracking/error-properties-builder.d.ts +6 -6
- package/dist/error-tracking/error-properties-builder.d.ts.map +1 -1
- package/dist/error-tracking/error-properties-builder.js +17 -27
- package/dist/error-tracking/error-properties-builder.mjs +16 -26
- package/dist/error-tracking/parsers/index.js +2 -4
- package/dist/error-tracking/parsers/index.mjs +2 -4
- package/dist/error-tracking/parsers/node.js +3 -5
- package/dist/error-tracking/parsers/node.mjs +3 -5
- package/dist/error-tracking/utils.js +4 -4
- package/dist/error-tracking/utils.mjs +4 -4
- package/dist/eventemitter.js +4 -4
- package/dist/eventemitter.mjs +4 -4
- package/dist/featureFlagUtils.js +20 -45
- package/dist/featureFlagUtils.mjs +20 -45
- package/dist/gzip.js +1 -2
- package/dist/gzip.mjs +1 -2
- package/dist/index.d.ts +4 -366
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -1225
- package/dist/index.mjs +5 -1190
- package/dist/posthog-core-stateless.d.ts +204 -0
- package/dist/posthog-core-stateless.d.ts.map +1 -0
- package/dist/posthog-core-stateless.js +675 -0
- package/dist/posthog-core-stateless.mjs +632 -0
- package/dist/posthog-core.d.ts +171 -0
- package/dist/posthog-core.d.ts.map +1 -0
- package/dist/posthog-core.js +554 -0
- package/dist/posthog-core.mjs +520 -0
- package/dist/testing/PostHogCoreTestClient.d.ts +2 -1
- package/dist/testing/PostHogCoreTestClient.d.ts.map +1 -1
- package/dist/testing/PostHogCoreTestClient.js +9 -11
- package/dist/testing/PostHogCoreTestClient.mjs +8 -10
- package/dist/testing/test-utils.js +1 -1
- package/dist/testing/test-utils.mjs +1 -1
- package/dist/utils/bucketed-rate-limiter.js +8 -12
- package/dist/utils/bucketed-rate-limiter.mjs +8 -12
- package/dist/utils/index.js +3 -3
- package/dist/utils/index.mjs +3 -3
- package/dist/utils/type-utils.js +1 -1
- package/dist/utils/type-utils.mjs +1 -1
- package/dist/vendor/uuidv7.js +12 -16
- package/dist/vendor/uuidv7.mjs +12 -16
- package/package.json +3 -2
- package/src/__tests__/featureFlagUtils.spec.ts +427 -0
- package/src/__tests__/gzip.spec.ts +69 -0
- package/src/__tests__/posthog.ai.spec.ts +110 -0
- package/src/__tests__/posthog.capture.spec.ts +91 -0
- package/src/__tests__/posthog.core.spec.ts +135 -0
- package/src/__tests__/posthog.debug.spec.ts +36 -0
- package/src/__tests__/posthog.enqueue.spec.ts +93 -0
- package/src/__tests__/posthog.featureflags.spec.ts +1106 -0
- package/src/__tests__/posthog.featureflags.v1.spec.ts +922 -0
- package/src/__tests__/posthog.flush.spec.ts +237 -0
- package/src/__tests__/posthog.gdpr.spec.ts +50 -0
- package/src/__tests__/posthog.groups.spec.ts +96 -0
- package/src/__tests__/posthog.identify.spec.ts +194 -0
- package/src/__tests__/posthog.init.spec.ts +110 -0
- package/src/__tests__/posthog.listeners.spec.ts +51 -0
- package/src/__tests__/posthog.register.spec.ts +47 -0
- package/src/__tests__/posthog.reset.spec.ts +76 -0
- package/src/__tests__/posthog.sessions.spec.ts +63 -0
- package/src/__tests__/posthog.setProperties.spec.ts +102 -0
- package/src/__tests__/posthog.shutdown.spec.ts +88 -0
- package/src/__tests__/utils.spec.ts +36 -0
- package/src/error-tracking/chunk-ids.ts +58 -0
- package/src/error-tracking/coercers/dom-exception-coercer.ts +38 -0
- package/src/error-tracking/coercers/error-coercer.ts +36 -0
- package/src/error-tracking/coercers/error-event-coercer.ts +24 -0
- package/src/error-tracking/coercers/event-coercer.ts +19 -0
- package/src/error-tracking/coercers/index.ts +8 -0
- package/src/error-tracking/coercers/object-coercer.ts +76 -0
- package/src/error-tracking/coercers/primitive-coercer.ts +19 -0
- package/src/error-tracking/coercers/promise-rejection-event.spec.ts +77 -0
- package/src/error-tracking/coercers/promise-rejection-event.ts +53 -0
- package/src/error-tracking/coercers/string-coercer.spec.ts +26 -0
- package/src/error-tracking/coercers/string-coercer.ts +31 -0
- package/src/error-tracking/coercers/utils.ts +33 -0
- package/src/error-tracking/error-properties-builder.coerce.spec.ts +202 -0
- package/src/error-tracking/error-properties-builder.parse.spec.ts +30 -0
- package/src/error-tracking/error-properties-builder.ts +167 -0
- package/src/error-tracking/index.ts +5 -0
- package/src/error-tracking/parsers/base.ts +29 -0
- package/src/error-tracking/parsers/chrome.ts +53 -0
- package/src/error-tracking/parsers/gecko.ts +38 -0
- package/src/error-tracking/parsers/index.ts +104 -0
- package/src/error-tracking/parsers/node.ts +111 -0
- package/src/error-tracking/parsers/opera.ts +18 -0
- package/src/error-tracking/parsers/react-native.ts +0 -0
- package/src/error-tracking/parsers/safari.ts +33 -0
- package/src/error-tracking/parsers/winjs.ts +12 -0
- package/src/error-tracking/types.ts +107 -0
- package/src/error-tracking/utils.ts +39 -0
- package/src/eventemitter.ts +27 -0
- package/src/featureFlagUtils.ts +192 -0
- package/src/gzip.ts +29 -0
- package/src/index.ts +8 -0
- package/src/posthog-core-stateless.ts +1226 -0
- package/src/posthog-core.ts +958 -0
- package/src/testing/PostHogCoreTestClient.ts +91 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/test-utils.ts +47 -0
- package/src/types.ts +544 -0
- package/src/utils/bucketed-rate-limiter.spec.ts +33 -0
- package/src/utils/bucketed-rate-limiter.ts +85 -0
- package/src/utils/index.ts +98 -0
- package/src/utils/number-utils.spec.ts +89 -0
- package/src/utils/number-utils.ts +30 -0
- package/src/utils/promise-queue.spec.ts +55 -0
- package/src/utils/promise-queue.ts +30 -0
- package/src/utils/string-utils.ts +23 -0
- package/src/utils/type-utils.ts +134 -0
- 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
|
+
}
|