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