posthog-node 5.32.1 → 5.33.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.
@@ -0,0 +1,321 @@
1
+ import { FeatureFlagValue, JsonType } from '@posthog/core'
2
+
3
+ import { FeatureFlagError } from './types'
4
+
5
+ /**
6
+ * Internal per-flag record stored by a {@link FeatureFlagEvaluations} instance.
7
+ * Not part of the public API.
8
+ *
9
+ * @internal
10
+ */
11
+ export type EvaluatedFlagRecord = {
12
+ key: string
13
+ enabled: boolean
14
+ variant: string | undefined
15
+ payload: JsonType | undefined
16
+ id: number | undefined
17
+ version: number | undefined
18
+ reason: string | undefined
19
+ locallyEvaluated: boolean
20
+ }
21
+
22
+ /**
23
+ * Parameters passed to the host when a `$feature_flag_called` event should be captured.
24
+ *
25
+ * @internal
26
+ */
27
+ export type FlagCalledEventParams = {
28
+ distinctId: string
29
+ key: string
30
+ response: FeatureFlagValue | undefined
31
+ groups: Record<string, string | number> | undefined
32
+ disableGeoip: boolean | undefined
33
+ properties: Record<string, any>
34
+ }
35
+
36
+ /**
37
+ * Thin interface the evaluations object uses to talk back to the PostHog client.
38
+ * Keeps the class decoupled from the full client surface area.
39
+ *
40
+ * @internal
41
+ */
42
+ export interface FeatureFlagEvaluationsHost {
43
+ captureFlagCalledEventIfNeeded(params: FlagCalledEventParams): void
44
+ logWarning(message: string): void
45
+ }
46
+
47
+ /**
48
+ * A snapshot of feature flag evaluations for a single distinctId at a point in time.
49
+ *
50
+ * Returned by {@link IPostHog.evaluateFlags} — branch on `isEnabled()` / `getFlag()`
51
+ * and pass the same object to `capture()` via the `flags` option so the captured event
52
+ * carries the exact flag values the code branched on.
53
+ *
54
+ * ```ts
55
+ * const flags = await posthog.evaluateFlags(distinctId, { personProperties: { plan: 'enterprise' } })
56
+ *
57
+ * if (flags.isEnabled('new-dashboard')) {
58
+ * renderNewDashboard()
59
+ * }
60
+ *
61
+ * posthog.capture({ distinctId, event: 'page_viewed', flags })
62
+ * ```
63
+ *
64
+ * To narrow the set of flags that get attached to a captured event, use the in-memory
65
+ * helpers `only([...])` and `onlyAccessed()`. To narrow the set of flags requested from
66
+ * the server in the first place, pass `flagKeys` to `evaluateFlags()`.
67
+ */
68
+ export class FeatureFlagEvaluations {
69
+ private readonly _host: FeatureFlagEvaluationsHost
70
+ private readonly _distinctId: string
71
+ private readonly _groups: Record<string, string | number> | undefined
72
+ private readonly _disableGeoip: boolean | undefined
73
+ private readonly _flags: Record<string, EvaluatedFlagRecord>
74
+ private readonly _requestId: string | undefined
75
+ private readonly _evaluatedAt: number | undefined
76
+ private readonly _flagDefinitionsLoadedAt: number | undefined
77
+ private readonly _errorsWhileComputing: boolean
78
+ private readonly _quotaLimited: boolean
79
+ private readonly _accessed: Set<string>
80
+ // True for snapshots produced by `only()` / `onlyAccessed()` — used to suppress
81
+ // misleading `flag_missing` events when branching is performed on a filtered slice.
82
+ private readonly _isSlice: boolean
83
+
84
+ /**
85
+ * @internal — instances are created by the SDK via `posthog.evaluateFlags()`.
86
+ */
87
+ constructor(init: {
88
+ host: FeatureFlagEvaluationsHost
89
+ distinctId: string
90
+ groups?: Record<string, string | number>
91
+ disableGeoip?: boolean
92
+ flags: Record<string, EvaluatedFlagRecord>
93
+ requestId?: string
94
+ evaluatedAt?: number
95
+ flagDefinitionsLoadedAt?: number
96
+ errorsWhileComputing?: boolean
97
+ quotaLimited?: boolean
98
+ accessed?: Set<string>
99
+ isSlice?: boolean
100
+ }) {
101
+ this._host = init.host
102
+ this._distinctId = init.distinctId
103
+ this._groups = init.groups
104
+ this._disableGeoip = init.disableGeoip
105
+ this._flags = init.flags
106
+ this._requestId = init.requestId
107
+ this._evaluatedAt = init.evaluatedAt
108
+ this._flagDefinitionsLoadedAt = init.flagDefinitionsLoadedAt
109
+ this._errorsWhileComputing = init.errorsWhileComputing ?? false
110
+ this._quotaLimited = init.quotaLimited ?? false
111
+ this._accessed = init.accessed ?? new Set()
112
+ this._isSlice = init.isSlice ?? false
113
+ }
114
+
115
+ /**
116
+ * Check whether a feature flag is enabled. Fires a `$feature_flag_called` event
117
+ * on the first access per (distinctId, flag, value) tuple, deduped via the SDK's
118
+ * existing cache.
119
+ *
120
+ * Flags that were not returned from the underlying evaluation are treated as
121
+ * disabled (returns `false`).
122
+ */
123
+ isEnabled(key: string): boolean {
124
+ const flag = this._flags[key]
125
+ this._recordAccess(key)
126
+ return flag?.enabled ?? false
127
+ }
128
+
129
+ /**
130
+ * Get the evaluated value of a feature flag. Fires a `$feature_flag_called` event
131
+ * on the first access per (distinctId, flag, value) tuple.
132
+ *
133
+ * Returns the variant string for multivariate flags, `true` for enabled flags
134
+ * without a variant, `false` for disabled flags, and `undefined` for flags that
135
+ * were not returned by the evaluation.
136
+ */
137
+ getFlag(key: string): FeatureFlagValue | undefined {
138
+ const flag = this._flags[key]
139
+ this._recordAccess(key)
140
+ if (!flag) {
141
+ return undefined
142
+ }
143
+ if (!flag.enabled) {
144
+ return false
145
+ }
146
+ return flag.variant ?? true
147
+ }
148
+
149
+ /**
150
+ * Get the payload associated with a feature flag. Does not count as an access
151
+ * for `onlyAccessed()` and does not fire any event.
152
+ */
153
+ getFlagPayload(key: string): JsonType | undefined {
154
+ return this._flags[key]?.payload
155
+ }
156
+
157
+ /**
158
+ * Return a filtered copy containing only flags that have been accessed via
159
+ * `isEnabled()` or `getFlag()` before this call.
160
+ *
161
+ * Order-dependent: if nothing has been accessed yet, the returned snapshot is
162
+ * empty. The method honors its name — pre-access if you want a populated result.
163
+ *
164
+ * **Note:** the returned snapshot is intended for `capture()`, not for further
165
+ * branching. Calling `isEnabled()` / `getFlag()` on it for a key that was filtered
166
+ * out is a no-op (no event is fired) — the flag wasn't actually missing, it was
167
+ * excluded from the slice.
168
+ */
169
+ onlyAccessed(): FeatureFlagEvaluations {
170
+ const filtered: Record<string, EvaluatedFlagRecord> = {}
171
+ for (const key of this._accessed) {
172
+ const flag = this._flags[key]
173
+ if (flag) {
174
+ filtered[key] = flag
175
+ }
176
+ }
177
+ return this._cloneWith(filtered)
178
+ }
179
+
180
+ /**
181
+ * Return a filtered copy containing only flags with the given keys. Keys that
182
+ * are not present in the evaluation are dropped and logged as a warning.
183
+ *
184
+ * **Note:** like `onlyAccessed()`, the returned snapshot is intended for `capture()`.
185
+ * Branching on a filtered key that was excluded from the slice is a no-op.
186
+ */
187
+ only(keys: string[]): FeatureFlagEvaluations {
188
+ const filtered: Record<string, EvaluatedFlagRecord> = {}
189
+ const missing: string[] = []
190
+ for (const key of keys) {
191
+ const flag = this._flags[key]
192
+ if (flag) {
193
+ filtered[key] = flag
194
+ } else {
195
+ missing.push(key)
196
+ }
197
+ }
198
+ if (missing.length > 0) {
199
+ this._host.logWarning(
200
+ `FeatureFlagEvaluations.only() was called with flag keys that are not in the evaluation set and will be dropped: ${missing.join(', ')}`
201
+ )
202
+ }
203
+ return this._cloneWith(filtered)
204
+ }
205
+
206
+ /**
207
+ * Returns the flag keys that are part of this evaluation.
208
+ */
209
+ get keys(): string[] {
210
+ return Object.keys(this._flags)
211
+ }
212
+
213
+ /**
214
+ * Build the `$feature/*` and `$active_feature_flags` event properties derived
215
+ * from the current flag set. Called by `capture()` when an event is captured
216
+ * with `flags: ...`.
217
+ *
218
+ * @internal
219
+ */
220
+ _getEventProperties(): Record<string, any> {
221
+ const properties: Record<string, any> = {}
222
+ const activeFlags: string[] = []
223
+ for (const [key, flag] of Object.entries(this._flags)) {
224
+ const value = flag.enabled === false ? false : (flag.variant ?? true)
225
+ properties[`$feature/${key}`] = value
226
+ if (flag.enabled) {
227
+ activeFlags.push(key)
228
+ }
229
+ }
230
+ if (activeFlags.length > 0) {
231
+ activeFlags.sort()
232
+ properties['$active_feature_flags'] = activeFlags
233
+ }
234
+ return properties
235
+ }
236
+
237
+ private _cloneWith(flags: Record<string, EvaluatedFlagRecord>): FeatureFlagEvaluations {
238
+ return new FeatureFlagEvaluations({
239
+ host: this._host,
240
+ distinctId: this._distinctId,
241
+ groups: this._groups,
242
+ disableGeoip: this._disableGeoip,
243
+ flags,
244
+ requestId: this._requestId,
245
+ evaluatedAt: this._evaluatedAt,
246
+ flagDefinitionsLoadedAt: this._flagDefinitionsLoadedAt,
247
+ errorsWhileComputing: this._errorsWhileComputing,
248
+ quotaLimited: this._quotaLimited,
249
+ // Copy the accessed set so the child can track further access independently
250
+ // of the parent. Callers expect `onlyAccessed()` on the parent to reflect
251
+ // only what the parent saw, not what happened on filtered views.
252
+ accessed: new Set(this._accessed),
253
+ isSlice: true,
254
+ })
255
+ }
256
+
257
+ private _recordAccess(key: string): void {
258
+ this._accessed.add(key)
259
+
260
+ // Empty snapshots (no resolvable distinctId) are returned by `evaluateFlags()` as a
261
+ // safety fallback. Firing $feature_flag_called for them would emit events with an
262
+ // empty distinct_id, polluting analytics — short-circuit here instead.
263
+ if (this._distinctId === '') {
264
+ return
265
+ }
266
+
267
+ // On filtered slices (returned by `only()` / `onlyAccessed()`), a key absent from
268
+ // the slice doesn't mean the flag is missing from PostHog — it was filtered out.
269
+ // Don't fire a misleading `flag_missing` event; slices are intended for `capture()`,
270
+ // not for further branching.
271
+ if (this._isSlice && !(key in this._flags)) {
272
+ return
273
+ }
274
+
275
+ const flag = this._flags[key]
276
+ const response: FeatureFlagValue | undefined =
277
+ flag === undefined ? undefined : flag.enabled === false ? false : (flag.variant ?? true)
278
+
279
+ const properties: Record<string, any> = {
280
+ $feature_flag: key,
281
+ $feature_flag_response: response,
282
+ $feature_flag_id: flag?.id,
283
+ $feature_flag_version: flag?.version,
284
+ $feature_flag_reason: flag?.reason,
285
+ locally_evaluated: flag?.locallyEvaluated ?? false,
286
+ [`$feature/${key}`]: response,
287
+ $feature_flag_request_id: this._requestId,
288
+ $feature_flag_evaluated_at: flag?.locallyEvaluated ? Date.now() : this._evaluatedAt,
289
+ }
290
+
291
+ if (flag?.locallyEvaluated && this._flagDefinitionsLoadedAt !== undefined) {
292
+ properties.$feature_flag_definitions_loaded_at = this._flagDefinitionsLoadedAt
293
+ }
294
+
295
+ // Build the comma-joined `$feature_flag_error` matching the single-flag path's
296
+ // granularity: response-level errors (errors-while-computing, quota-limited) are
297
+ // combined with per-flag errors (flag-missing) so consumers can filter by type.
298
+ const errors: string[] = []
299
+ if (this._errorsWhileComputing) {
300
+ errors.push(FeatureFlagError.ERRORS_WHILE_COMPUTING)
301
+ }
302
+ if (this._quotaLimited) {
303
+ errors.push(FeatureFlagError.QUOTA_LIMITED)
304
+ }
305
+ if (flag === undefined) {
306
+ errors.push(FeatureFlagError.FLAG_MISSING)
307
+ }
308
+ if (errors.length > 0) {
309
+ properties.$feature_flag_error = errors.join(',')
310
+ }
311
+
312
+ this._host.captureFlagCalledEventIfNeeded({
313
+ distinctId: this._distinctId,
314
+ key,
315
+ response,
316
+ groups: this._groups,
317
+ disableGeoip: this._disableGeoip,
318
+ properties,
319
+ })
320
+ }
321
+ }
package/src/types.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  } from '@posthog/core'
9
9
  import { ContextData, ContextOptions } from './extensions/context/types'
10
10
 
11
+ import type { FeatureFlagEvaluations } from './feature-flag-evaluations'
11
12
  import type { FlagDefinitionCacheProvider } from './extensions/feature-flags/cache'
12
13
 
13
14
  export type IdentifyMessage = {
@@ -27,6 +28,17 @@ export type EventMessage = Omit<IdentifyMessage, 'distinctId'> & {
27
28
  distinctId?: string // Optional - can be provided via context
28
29
  event: string
29
30
  groups?: Record<string, string | number> // Mapping of group type to group id
31
+ /**
32
+ * Attach feature flag values evaluated via `posthog.evaluateFlags()` to this event.
33
+ * Prefer this over `sendFeatureFlags` — it guarantees the event carries the exact
34
+ * values the code branched on and avoids a hidden `/flags` request on every capture.
35
+ */
36
+ flags?: FeatureFlagEvaluations
37
+ /**
38
+ * @deprecated Use the `flags` option with a `FeatureFlagEvaluations` object obtained
39
+ * from `posthog.evaluateFlags()` instead. `sendFeatureFlags` fires a separate `/flags`
40
+ * request on capture and may return different values than the ones the code branched on.
41
+ */
30
42
  sendFeatureFlags?: boolean | SendFeatureFlagsOptions
31
43
  timestamp?: Date
32
44
  uuid?: string
@@ -216,6 +228,14 @@ export type PostHogOptions = Omit<PostHogCoreOptions, 'before_send'> & {
216
228
  * @default false
217
229
  */
218
230
  strictLocalEvaluation?: boolean
231
+ /**
232
+ * Controls whether `FeatureFlagEvaluations` filter helpers (`onlyAccessed()` and
233
+ * `only()`) log warnings when their input is unexpected — for example, calling
234
+ * `onlyAccessed()` before accessing any flags, or passing unknown keys to `only()`.
235
+ *
236
+ * @default true
237
+ */
238
+ featureFlagsLogWarnings?: boolean
219
239
  /**
220
240
  * Provides the API to extend the lifetime of a serverless invocation until
221
241
  * background work (like flushing analytics events) completes after the response
@@ -312,9 +332,10 @@ export interface IPostHog {
312
332
  * @param event We recommend using [verb] [noun], like movie played or movie updated to easily identify what your events mean later on.
313
333
  * @param properties OPTIONAL | which can be a object with any information you'd like to add
314
334
  * @param groups OPTIONAL | object of what groups are related to this event, example: { company: 'id:5' }. Can be used to analyze companies instead of users.
315
- * @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event.
335
+ * @param flags OPTIONAL | A `FeatureFlagEvaluations` snapshot from `evaluateFlags()`. Attaches those exact flag values to the event with no extra network call.
336
+ * @param sendFeatureFlags OPTIONAL | Deprecated — prefer `flags`. Fires a hidden `/flags` request on capture to enrich the event with flag values.
316
337
  */
317
- capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessage): void
338
+ capture({ distinctId, event, properties, groups, flags, sendFeatureFlags }: EventMessage): void
318
339
 
319
340
  /**
320
341
  * @description Capture an event immediately. Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
@@ -322,9 +343,10 @@ export interface IPostHog {
322
343
  * @param event We recommend using [verb] [noun], like movie played or movie updated to easily identify what your events mean later on.
323
344
  * @param properties OPTIONAL | which can be a object with any information you'd like to add
324
345
  * @param groups OPTIONAL | object of what groups are related to this event, example: { company: 'id:5' }. Can be used to analyze companies instead of users.
325
- * @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event.
346
+ * @param flags OPTIONAL | A `FeatureFlagEvaluations` snapshot from `evaluateFlags()`. Attaches those exact flag values to the event with no extra network call.
347
+ * @param sendFeatureFlags OPTIONAL | Deprecated — prefer `flags`. Fires a hidden `/flags` request on capture to enrich the event with flag values.
326
348
  */
327
- captureImmediate({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessage): Promise<void>
349
+ captureImmediate({ distinctId, event, properties, groups, flags, sendFeatureFlags }: EventMessage): Promise<void>
328
350
 
329
351
  /**
330
352
  * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog,
@@ -379,6 +401,9 @@ export interface IPostHog {
379
401
  * @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true.
380
402
  *
381
403
  * @returns true if the flag is on, false if the flag is off, undefined if there was an error.
404
+ *
405
+ * @deprecated Use {@link IPostHog.evaluateFlags} and call `flags.isEnabled(key)` on the
406
+ * returned snapshot. Will be removed in the next major version.
382
407
  */
383
408
  isFeatureEnabled(
384
409
  key: string,
@@ -407,6 +432,9 @@ export interface IPostHog {
407
432
  * @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true.
408
433
  *
409
434
  * @returns true or string(for multivariates) if the flag is on, false if the flag is off, undefined if there was an error.
435
+ *
436
+ * @deprecated Use {@link IPostHog.evaluateFlags} and call `flags.getFlag(key)` on the
437
+ * returned snapshot. Will be removed in the next major version.
410
438
  */
411
439
  getFeatureFlag(
412
440
  key: string,
@@ -445,6 +473,9 @@ export interface IPostHog {
445
473
  * @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false.
446
474
  *
447
475
  * @returns payload of a json type object
476
+ *
477
+ * @deprecated Use {@link IPostHog.evaluateFlags} and call `flags.getFlagPayload(key)` on
478
+ * the returned snapshot. Will be removed in the next major version.
448
479
  */
449
480
  getFeatureFlagPayload(
450
481
  key: string,
@@ -513,6 +544,36 @@ export interface IPostHog {
513
544
  options?: FlagEvaluationOptions
514
545
  ): Promise<FeatureFlagResult | undefined>
515
546
 
547
+ /**
548
+ * @description Evaluate all feature flags for a user in a single call and return a
549
+ * {@link FeatureFlagEvaluations} snapshot. Branch on `.isEnabled()` / `.getFlag()`,
550
+ * then pass the same snapshot to `capture()` via the `flags` option so events carry
551
+ * the exact flag values the code branched on.
552
+ *
553
+ * Prefer this over calling `isFeatureEnabled()` / `getFeatureFlag()` repeatedly and
554
+ * over `capture({ sendFeatureFlags: true })` — it avoids multiple `/flags` requests
555
+ * per incoming request.
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * const flags = await posthog.evaluateFlags('user_123', { personProperties: { plan: 'enterprise' } })
560
+ * if (flags.isEnabled('new-dashboard')) {
561
+ * renderNewDashboard()
562
+ * }
563
+ * posthog.capture({ distinctId: 'user_123', event: 'page_viewed', flags })
564
+ * ```
565
+ *
566
+ * @param options - Optional configuration for flag evaluation. Pass `flagKeys` to scope the underlying `/flags` request to a subset of flags.
567
+ */
568
+ evaluateFlags(options?: AllFlagsOptions): Promise<FeatureFlagEvaluations>
569
+ /**
570
+ * @description Evaluate all feature flags for a specific user.
571
+ *
572
+ * @param distinctId - The user's distinct ID
573
+ * @param options - Optional configuration for flag evaluation. Pass `flagKeys` to scope the underlying `/flags` request to a subset of flags.
574
+ */
575
+ evaluateFlags(distinctId: string, options?: AllFlagsOptions): Promise<FeatureFlagEvaluations>
576
+
516
577
  /**
517
578
  * @description Sets a groups properties, which allows asking questions like "Who are the most active companies"
518
579
  * using my product in PostHog.
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '5.32.1'
1
+ export const version = '5.33.1'