posthog-node 5.25.0 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +24 -25
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +55 -14
- package/dist/client.mjs +55 -14
- package/dist/extensions/context/context.d.ts +2 -0
- package/dist/extensions/context/context.d.ts.map +1 -1
- package/dist/extensions/context/context.js +16 -14
- package/dist/extensions/context/context.mjs +16 -14
- package/dist/extensions/context/types.d.ts +1 -0
- package/dist/extensions/context/types.d.ts.map +1 -1
- package/dist/extensions/feature-flags/feature-flags.d.ts +16 -4
- package/dist/extensions/feature-flags/feature-flags.d.ts.map +1 -1
- package/dist/extensions/feature-flags/feature-flags.js +44 -22
- package/dist/extensions/feature-flags/feature-flags.mjs +44 -22
- package/dist/types.d.ts +51 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +1 -1
- package/src/client.ts +124 -57
- package/src/extensions/context/context.ts +19 -14
- package/src/extensions/context/types.ts +1 -0
- package/src/extensions/feature-flags/feature-flags.ts +70 -104
- package/src/types.ts +59 -8
- package/src/version.ts +1 -1
package/src/client.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { version } from './version'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
FeatureFlagDetail,
|
|
5
4
|
FeatureFlagValue,
|
|
6
5
|
isBlockedUA,
|
|
7
6
|
isPlainObject,
|
|
@@ -26,9 +25,12 @@ import {
|
|
|
26
25
|
OverrideFeatureFlagsOptions,
|
|
27
26
|
PostHogOptions,
|
|
28
27
|
SendFeatureFlagsOptions,
|
|
28
|
+
FlagEvaluationOptions,
|
|
29
|
+
AllFlagsOptions,
|
|
29
30
|
} from './types'
|
|
30
31
|
import {
|
|
31
32
|
FeatureFlagsPoller,
|
|
33
|
+
type FeatureFlagEvaluationContext,
|
|
32
34
|
RequiresServerEvaluation,
|
|
33
35
|
InconclusiveMatchError,
|
|
34
36
|
} from './extensions/feature-flags/feature-flags'
|
|
@@ -611,6 +613,16 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
611
613
|
})
|
|
612
614
|
}
|
|
613
615
|
|
|
616
|
+
private _resolveDistinctId<T>(
|
|
617
|
+
distinctIdOrOptions: string | T | undefined,
|
|
618
|
+
options: T | undefined
|
|
619
|
+
): { distinctId: string | undefined; options: T | undefined } {
|
|
620
|
+
if (typeof distinctIdOrOptions === 'string') {
|
|
621
|
+
return { distinctId: distinctIdOrOptions, options }
|
|
622
|
+
}
|
|
623
|
+
return { distinctId: this.context?.get()?.distinctId, options: distinctIdOrOptions }
|
|
624
|
+
}
|
|
625
|
+
|
|
614
626
|
/**
|
|
615
627
|
* Internal method that handles feature flag evaluation with full details.
|
|
616
628
|
* Used by getFeatureFlag, getFeatureFlagPayload, and getFeatureFlagResult.
|
|
@@ -663,6 +675,12 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
663
675
|
|
|
664
676
|
personProperties = adjustedProperties.allPersonProperties
|
|
665
677
|
groupProperties = adjustedProperties.allGroupProperties
|
|
678
|
+
const evaluationContext = this.createFeatureFlagEvaluationContext(
|
|
679
|
+
distinctId,
|
|
680
|
+
groups,
|
|
681
|
+
personProperties,
|
|
682
|
+
groupProperties
|
|
683
|
+
)
|
|
666
684
|
|
|
667
685
|
// set defaults
|
|
668
686
|
if (onlyEvaluateLocally == undefined) {
|
|
@@ -687,14 +705,9 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
687
705
|
const flag = this.featureFlagsPoller?.featureFlagsByKey[key]
|
|
688
706
|
if (flag) {
|
|
689
707
|
try {
|
|
690
|
-
const localResult = await this.featureFlagsPoller?.computeFlagAndPayloadLocally(
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
groups,
|
|
694
|
-
personProperties,
|
|
695
|
-
groupProperties,
|
|
696
|
-
matchValue
|
|
697
|
-
)
|
|
708
|
+
const localResult = await this.featureFlagsPoller?.computeFlagAndPayloadLocally(flag, evaluationContext, {
|
|
709
|
+
matchValue,
|
|
710
|
+
})
|
|
698
711
|
if (localResult) {
|
|
699
712
|
flagWasLocallyEvaluated = true
|
|
700
713
|
const value = localResult.value
|
|
@@ -721,10 +734,10 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
721
734
|
// Fall back to remote evaluation if needed
|
|
722
735
|
if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) {
|
|
723
736
|
const flagsResponse = await super.getFeatureFlagDetailsStateless(
|
|
724
|
-
distinctId,
|
|
725
|
-
groups,
|
|
726
|
-
personProperties,
|
|
727
|
-
groupProperties,
|
|
737
|
+
evaluationContext.distinctId,
|
|
738
|
+
evaluationContext.groups,
|
|
739
|
+
evaluationContext.personProperties,
|
|
740
|
+
evaluationContext.groupProperties,
|
|
728
741
|
disableGeoip,
|
|
729
742
|
[key]
|
|
730
743
|
)
|
|
@@ -1008,21 +1021,30 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1008
1021
|
* @param options - Optional configuration for flag evaluation
|
|
1009
1022
|
* @returns Promise that resolves to the flag result or undefined
|
|
1010
1023
|
*/
|
|
1024
|
+
async getFeatureFlagResult(key: string, options?: FlagEvaluationOptions): Promise<FeatureFlagResult | undefined>
|
|
1011
1025
|
async getFeatureFlagResult(
|
|
1012
1026
|
key: string,
|
|
1013
1027
|
distinctId: string,
|
|
1014
|
-
options?:
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
disableGeoip?: boolean
|
|
1021
|
-
}
|
|
1028
|
+
options?: FlagEvaluationOptions
|
|
1029
|
+
): Promise<FeatureFlagResult | undefined>
|
|
1030
|
+
async getFeatureFlagResult(
|
|
1031
|
+
key: string,
|
|
1032
|
+
distinctIdOrOptions?: string | FlagEvaluationOptions,
|
|
1033
|
+
options?: FlagEvaluationOptions
|
|
1022
1034
|
): Promise<FeatureFlagResult | undefined> {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1035
|
+
const { distinctId: resolvedDistinctId, options: resolvedOptions } = this._resolveDistinctId(
|
|
1036
|
+
distinctIdOrOptions,
|
|
1037
|
+
options
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
if (!resolvedDistinctId) {
|
|
1041
|
+
this._logger.warn('[PostHog] distinctId is required — pass it explicitly or use withContext()')
|
|
1042
|
+
return undefined
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return this._getFeatureFlagResult(key, resolvedDistinctId, {
|
|
1046
|
+
...resolvedOptions,
|
|
1047
|
+
sendFeatureFlagEvents: resolvedOptions?.sendFeatureFlagEvents ?? this.options.sendFeatureFlagEvent ?? true,
|
|
1026
1048
|
})
|
|
1027
1049
|
}
|
|
1028
1050
|
|
|
@@ -1155,18 +1177,24 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1155
1177
|
* @param options - Optional configuration for flag evaluation
|
|
1156
1178
|
* @returns Promise that resolves to a record of flag keys and their values
|
|
1157
1179
|
*/
|
|
1180
|
+
async getAllFlags(options?: AllFlagsOptions): Promise<Record<string, FeatureFlagValue>>
|
|
1181
|
+
async getAllFlags(distinctId: string, options?: AllFlagsOptions): Promise<Record<string, FeatureFlagValue>>
|
|
1158
1182
|
async getAllFlags(
|
|
1159
|
-
|
|
1160
|
-
options?:
|
|
1161
|
-
groups?: Record<string, string>
|
|
1162
|
-
personProperties?: Record<string, string>
|
|
1163
|
-
groupProperties?: Record<string, Record<string, string>>
|
|
1164
|
-
onlyEvaluateLocally?: boolean
|
|
1165
|
-
disableGeoip?: boolean
|
|
1166
|
-
flagKeys?: string[]
|
|
1167
|
-
}
|
|
1183
|
+
distinctIdOrOptions?: string | AllFlagsOptions,
|
|
1184
|
+
options?: AllFlagsOptions
|
|
1168
1185
|
): Promise<Record<string, FeatureFlagValue>> {
|
|
1169
|
-
const
|
|
1186
|
+
const { distinctId: resolvedDistinctId, options: resolvedOptions } = this._resolveDistinctId(
|
|
1187
|
+
distinctIdOrOptions,
|
|
1188
|
+
options
|
|
1189
|
+
)
|
|
1190
|
+
if (!resolvedDistinctId) {
|
|
1191
|
+
this._logger.warn(
|
|
1192
|
+
'[PostHog] distinctId is required to get feature flags — pass it explicitly or use withContext()'
|
|
1193
|
+
)
|
|
1194
|
+
return {}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const response = await this.getAllFlagsAndPayloads(resolvedDistinctId, resolvedOptions)
|
|
1170
1198
|
return response.featureFlags || {}
|
|
1171
1199
|
}
|
|
1172
1200
|
|
|
@@ -1203,22 +1231,28 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1203
1231
|
* @param options - Optional configuration for flag evaluation
|
|
1204
1232
|
* @returns Promise that resolves to flags and payloads
|
|
1205
1233
|
*/
|
|
1234
|
+
async getAllFlagsAndPayloads(options?: AllFlagsOptions): Promise<PostHogFlagsAndPayloadsResponse>
|
|
1235
|
+
async getAllFlagsAndPayloads(distinctId: string, options?: AllFlagsOptions): Promise<PostHogFlagsAndPayloadsResponse>
|
|
1206
1236
|
async getAllFlagsAndPayloads(
|
|
1207
|
-
|
|
1208
|
-
options?:
|
|
1209
|
-
groups?: Record<string, string>
|
|
1210
|
-
personProperties?: Record<string, string>
|
|
1211
|
-
groupProperties?: Record<string, Record<string, string>>
|
|
1212
|
-
onlyEvaluateLocally?: boolean
|
|
1213
|
-
disableGeoip?: boolean
|
|
1214
|
-
flagKeys?: string[]
|
|
1215
|
-
}
|
|
1237
|
+
distinctIdOrOptions?: string | AllFlagsOptions,
|
|
1238
|
+
options?: AllFlagsOptions
|
|
1216
1239
|
): Promise<PostHogFlagsAndPayloadsResponse> {
|
|
1217
|
-
const {
|
|
1218
|
-
|
|
1240
|
+
const { distinctId: resolvedDistinctId, options: resolvedOptions } = this._resolveDistinctId(
|
|
1241
|
+
distinctIdOrOptions,
|
|
1242
|
+
options
|
|
1243
|
+
)
|
|
1244
|
+
if (!resolvedDistinctId) {
|
|
1245
|
+
this._logger.warn(
|
|
1246
|
+
'[PostHog] distinctId is required to get feature flags and payloads — pass it explicitly or use withContext()'
|
|
1247
|
+
)
|
|
1248
|
+
return { featureFlags: {}, featureFlagPayloads: {} }
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const { groups, disableGeoip, flagKeys } = resolvedOptions || {}
|
|
1252
|
+
let { onlyEvaluateLocally, personProperties, groupProperties } = resolvedOptions || {}
|
|
1219
1253
|
|
|
1220
1254
|
const adjustedProperties = this.addLocalPersonAndGroupProperties(
|
|
1221
|
-
|
|
1255
|
+
resolvedDistinctId,
|
|
1222
1256
|
groups,
|
|
1223
1257
|
personProperties,
|
|
1224
1258
|
groupProperties
|
|
@@ -1226,19 +1260,19 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1226
1260
|
|
|
1227
1261
|
personProperties = adjustedProperties.allPersonProperties
|
|
1228
1262
|
groupProperties = adjustedProperties.allGroupProperties
|
|
1263
|
+
const evaluationContext = this.createFeatureFlagEvaluationContext(
|
|
1264
|
+
resolvedDistinctId,
|
|
1265
|
+
groups,
|
|
1266
|
+
personProperties,
|
|
1267
|
+
groupProperties
|
|
1268
|
+
)
|
|
1229
1269
|
|
|
1230
1270
|
// set defaults
|
|
1231
1271
|
if (onlyEvaluateLocally == undefined) {
|
|
1232
1272
|
onlyEvaluateLocally = this.options.strictLocalEvaluation ?? false
|
|
1233
1273
|
}
|
|
1234
1274
|
|
|
1235
|
-
const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(
|
|
1236
|
-
distinctId,
|
|
1237
|
-
groups,
|
|
1238
|
-
personProperties,
|
|
1239
|
-
groupProperties,
|
|
1240
|
-
flagKeys
|
|
1241
|
-
)
|
|
1275
|
+
const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(evaluationContext, flagKeys)
|
|
1242
1276
|
|
|
1243
1277
|
let featureFlags = {}
|
|
1244
1278
|
let featureFlagPayloads = {}
|
|
@@ -1251,10 +1285,10 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1251
1285
|
|
|
1252
1286
|
if (fallbackToFlags && !onlyEvaluateLocally) {
|
|
1253
1287
|
const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(
|
|
1254
|
-
distinctId,
|
|
1255
|
-
groups,
|
|
1256
|
-
personProperties,
|
|
1257
|
-
groupProperties,
|
|
1288
|
+
evaluationContext.distinctId,
|
|
1289
|
+
evaluationContext.groups,
|
|
1290
|
+
evaluationContext.personProperties,
|
|
1291
|
+
evaluationContext.groupProperties,
|
|
1258
1292
|
disableGeoip,
|
|
1259
1293
|
flagKeys
|
|
1260
1294
|
)
|
|
@@ -1510,6 +1544,24 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1510
1544
|
return this.context?.get()
|
|
1511
1545
|
}
|
|
1512
1546
|
|
|
1547
|
+
/**
|
|
1548
|
+
* Set context without a callback wrapper.
|
|
1549
|
+
*
|
|
1550
|
+
* Uses `AsyncLocalStorage.enterWith()` to attach context to the current
|
|
1551
|
+
* async execution context. The context lives until that async context ends.
|
|
1552
|
+
*
|
|
1553
|
+
* Must be called in the same async scope that makes PostHog calls.
|
|
1554
|
+
* Calling this outside a request-scoped async context will leak context
|
|
1555
|
+
* across unrelated work. Prefer `withContext()` when you can wrap code
|
|
1556
|
+
* in a callback — it creates an isolated scope that cleans up automatically.
|
|
1557
|
+
*
|
|
1558
|
+
* @param data - Context data to apply (distinctId, sessionId, properties)
|
|
1559
|
+
* @param options - Context options (fresh: true to start with clean context instead of inheriting)
|
|
1560
|
+
*/
|
|
1561
|
+
enterContext(data: Partial<ContextData>, options?: ContextOptions): void {
|
|
1562
|
+
this.context?.enter(data as ContextData, options)
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1513
1565
|
/**
|
|
1514
1566
|
* Shutdown the PostHog client gracefully.
|
|
1515
1567
|
*
|
|
@@ -1691,6 +1743,21 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1691
1743
|
return { allPersonProperties, allGroupProperties }
|
|
1692
1744
|
}
|
|
1693
1745
|
|
|
1746
|
+
private createFeatureFlagEvaluationContext(
|
|
1747
|
+
distinctId: string,
|
|
1748
|
+
groups?: Record<string, string>,
|
|
1749
|
+
personProperties?: Record<string, any>,
|
|
1750
|
+
groupProperties?: Record<string, Record<string, any>>
|
|
1751
|
+
): FeatureFlagEvaluationContext {
|
|
1752
|
+
return {
|
|
1753
|
+
distinctId,
|
|
1754
|
+
groups: groups || {},
|
|
1755
|
+
personProperties: personProperties || {},
|
|
1756
|
+
groupProperties: groupProperties || {},
|
|
1757
|
+
evaluationCache: {},
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1694
1761
|
/**
|
|
1695
1762
|
* Capture an error exception as an event.
|
|
1696
1763
|
*
|
|
@@ -13,21 +13,26 @@ export class PostHogContext implements IPostHogContext {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
run<T>(context: ContextData, fn: () => T, options?: ContextOptions): T {
|
|
16
|
-
|
|
16
|
+
return this.storage.run(this.resolve(context, options), fn)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
enter(context: ContextData, options?: ContextOptions): void {
|
|
20
|
+
this.storage.enterWith(this.resolve(context, options))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private resolve(context: ContextData, options?: ContextOptions): ContextData {
|
|
24
|
+
if (options?.fresh === true) {
|
|
25
|
+
return context
|
|
26
|
+
}
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
...(currentContext.properties || {}),
|
|
27
|
-
...(context.properties || {}),
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
return this.storage.run(mergedContext, fn)
|
|
28
|
+
const current = this.get() || {}
|
|
29
|
+
return {
|
|
30
|
+
distinctId: context.distinctId ?? current.distinctId,
|
|
31
|
+
sessionId: context.sessionId ?? current.sessionId,
|
|
32
|
+
properties: {
|
|
33
|
+
...(current.properties || {}),
|
|
34
|
+
...(context.properties || {}),
|
|
35
|
+
},
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
}
|
|
@@ -58,6 +58,19 @@ type FeatureFlagsPollerOptions = {
|
|
|
58
58
|
strictLocalEvaluation?: boolean
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export type FeatureFlagEvaluationContext = {
|
|
62
|
+
distinctId: string
|
|
63
|
+
groups: Record<string, string>
|
|
64
|
+
personProperties: Record<string, any>
|
|
65
|
+
groupProperties: Record<string, Record<string, any>>
|
|
66
|
+
evaluationCache: Record<string, FeatureFlagValue>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ComputeFlagAndPayloadOptions = {
|
|
70
|
+
matchValue?: FeatureFlagValue
|
|
71
|
+
skipLoadCheck?: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
class FeatureFlagsPoller {
|
|
62
75
|
pollingInterval: number
|
|
63
76
|
personalApiKey: string
|
|
@@ -122,6 +135,22 @@ class FeatureFlagsPoller {
|
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
137
|
|
|
138
|
+
private createEvaluationContext(
|
|
139
|
+
distinctId: string,
|
|
140
|
+
groups: Record<string, string> = {},
|
|
141
|
+
personProperties: Record<string, any> = {},
|
|
142
|
+
groupProperties: Record<string, Record<string, any>> = {},
|
|
143
|
+
evaluationCache: Record<string, FeatureFlagValue> = {}
|
|
144
|
+
): FeatureFlagEvaluationContext {
|
|
145
|
+
return {
|
|
146
|
+
distinctId,
|
|
147
|
+
groups,
|
|
148
|
+
personProperties,
|
|
149
|
+
groupProperties,
|
|
150
|
+
evaluationCache,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
125
154
|
async getFeatureFlag(
|
|
126
155
|
key: string,
|
|
127
156
|
distinctId: string,
|
|
@@ -141,14 +170,9 @@ class FeatureFlagsPoller {
|
|
|
141
170
|
featureFlag = this.featureFlagsByKey[key]
|
|
142
171
|
|
|
143
172
|
if (featureFlag !== undefined) {
|
|
173
|
+
const evaluationContext = this.createEvaluationContext(distinctId, groups, personProperties, groupProperties)
|
|
144
174
|
try {
|
|
145
|
-
const result = await this.computeFlagAndPayloadLocally(
|
|
146
|
-
featureFlag,
|
|
147
|
-
distinctId,
|
|
148
|
-
groups,
|
|
149
|
-
personProperties,
|
|
150
|
-
groupProperties
|
|
151
|
-
)
|
|
175
|
+
const result = await this.computeFlagAndPayloadLocally(featureFlag, evaluationContext)
|
|
152
176
|
response = result.value
|
|
153
177
|
this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`))
|
|
154
178
|
} catch (e) {
|
|
@@ -164,10 +188,7 @@ class FeatureFlagsPoller {
|
|
|
164
188
|
}
|
|
165
189
|
|
|
166
190
|
async getAllFlagsAndPayloads(
|
|
167
|
-
|
|
168
|
-
groups: Record<string, string> = {},
|
|
169
|
-
personProperties: Record<string, any> = {},
|
|
170
|
-
groupProperties: Record<string, Record<string, any>> = {},
|
|
191
|
+
evaluationContext: FeatureFlagEvaluationContext,
|
|
171
192
|
flagKeysToExplicitlyEvaluate?: string[]
|
|
172
193
|
): Promise<{
|
|
173
194
|
response: Record<string, FeatureFlagValue>
|
|
@@ -184,20 +205,17 @@ class FeatureFlagsPoller {
|
|
|
184
205
|
? flagKeysToExplicitlyEvaluate.map((key) => this.featureFlagsByKey[key]).filter(Boolean)
|
|
185
206
|
: this.featureFlags
|
|
186
207
|
|
|
187
|
-
|
|
188
|
-
|
|
208
|
+
const sharedEvaluationContext = {
|
|
209
|
+
...evaluationContext,
|
|
210
|
+
evaluationCache: evaluationContext.evaluationCache ?? {},
|
|
211
|
+
}
|
|
189
212
|
|
|
190
213
|
await Promise.all(
|
|
191
214
|
flagsToEvaluate.map(async (flag) => {
|
|
192
215
|
try {
|
|
193
216
|
const { value: matchValue, payload: matchPayload } = await this.computeFlagAndPayloadLocally(
|
|
194
217
|
flag,
|
|
195
|
-
|
|
196
|
-
groups,
|
|
197
|
-
personProperties,
|
|
198
|
-
groupProperties,
|
|
199
|
-
undefined /* matchValue */,
|
|
200
|
-
sharedEvaluationCache
|
|
218
|
+
sharedEvaluationContext
|
|
201
219
|
)
|
|
202
220
|
response[flag.key] = matchValue
|
|
203
221
|
if (matchPayload) {
|
|
@@ -219,17 +237,14 @@ class FeatureFlagsPoller {
|
|
|
219
237
|
|
|
220
238
|
async computeFlagAndPayloadLocally(
|
|
221
239
|
flag: PostHogFeatureFlag,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
personProperties: Record<string, any> = {},
|
|
225
|
-
groupProperties: Record<string, Record<string, any>> = {},
|
|
226
|
-
matchValue?: FeatureFlagValue,
|
|
227
|
-
evaluationCache?: Record<string, FeatureFlagValue>,
|
|
228
|
-
skipLoadCheck: boolean = false
|
|
240
|
+
evaluationContext: FeatureFlagEvaluationContext,
|
|
241
|
+
options: ComputeFlagAndPayloadOptions = {}
|
|
229
242
|
): Promise<{
|
|
230
243
|
value: FeatureFlagValue
|
|
231
244
|
payload: JsonType | null
|
|
232
245
|
}> {
|
|
246
|
+
const { matchValue, skipLoadCheck = false } = options
|
|
247
|
+
|
|
233
248
|
// Only load flags if not already loaded and not skipping the check
|
|
234
249
|
if (!skipLoadCheck) {
|
|
235
250
|
await this.loadFeatureFlags()
|
|
@@ -245,14 +260,7 @@ class FeatureFlagsPoller {
|
|
|
245
260
|
if (matchValue !== undefined) {
|
|
246
261
|
flagValue = matchValue
|
|
247
262
|
} else {
|
|
248
|
-
flagValue = await this.computeFlagValueLocally(
|
|
249
|
-
flag,
|
|
250
|
-
distinctId,
|
|
251
|
-
groups,
|
|
252
|
-
personProperties,
|
|
253
|
-
groupProperties,
|
|
254
|
-
evaluationCache
|
|
255
|
-
)
|
|
263
|
+
flagValue = await this.computeFlagValueLocally(flag, evaluationContext)
|
|
256
264
|
}
|
|
257
265
|
|
|
258
266
|
// Always compute payload based on the final flagValue (whether provided or computed)
|
|
@@ -263,12 +271,10 @@ class FeatureFlagsPoller {
|
|
|
263
271
|
|
|
264
272
|
private async computeFlagValueLocally(
|
|
265
273
|
flag: PostHogFeatureFlag,
|
|
266
|
-
|
|
267
|
-
groups: Record<string, string> = {},
|
|
268
|
-
personProperties: Record<string, any> = {},
|
|
269
|
-
groupProperties: Record<string, Record<string, any>> = {},
|
|
270
|
-
evaluationCache: Record<string, FeatureFlagValue> = {}
|
|
274
|
+
evaluationContext: FeatureFlagEvaluationContext
|
|
271
275
|
): Promise<FeatureFlagValue> {
|
|
276
|
+
const { distinctId, groups, personProperties, groupProperties } = evaluationContext
|
|
277
|
+
|
|
272
278
|
if (flag.ensure_experience_continuity) {
|
|
273
279
|
throw new InconclusiveMatchError('Flag has experience continuity enabled')
|
|
274
280
|
}
|
|
@@ -299,32 +305,30 @@ class FeatureFlagsPoller {
|
|
|
299
305
|
return false
|
|
300
306
|
}
|
|
301
307
|
|
|
308
|
+
if (
|
|
309
|
+
flag.bucketing_identifier === 'device_id' &&
|
|
310
|
+
(personProperties?.$device_id === undefined ||
|
|
311
|
+
personProperties?.$device_id === null ||
|
|
312
|
+
personProperties?.$device_id === '')
|
|
313
|
+
) {
|
|
314
|
+
this.logMsgIfDebug(() =>
|
|
315
|
+
console.warn(`[FEATURE FLAGS] Ignoring bucketing_identifier for group flag: ${flag.key}`)
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
302
319
|
const focusedGroupProperties = groupProperties[groupName]
|
|
303
|
-
return await this.matchFeatureFlagProperties(
|
|
304
|
-
flag,
|
|
305
|
-
groups[groupName],
|
|
306
|
-
focusedGroupProperties,
|
|
307
|
-
evaluationCache,
|
|
308
|
-
distinctId,
|
|
309
|
-
groups,
|
|
310
|
-
personProperties,
|
|
311
|
-
groupProperties
|
|
312
|
-
)
|
|
320
|
+
return await this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties, evaluationContext)
|
|
313
321
|
} else {
|
|
314
322
|
const bucketingValue = this.getBucketingValueForFlag(flag, distinctId, personProperties)
|
|
315
323
|
if (bucketingValue === undefined) {
|
|
324
|
+
this.logMsgIfDebug(() =>
|
|
325
|
+
console.warn(
|
|
326
|
+
`[FEATURE FLAGS] Can't compute feature flag: ${flag.key} without $device_id, falling back to server evaluation`
|
|
327
|
+
)
|
|
328
|
+
)
|
|
316
329
|
throw new InconclusiveMatchError(`Can't compute feature flag: ${flag.key} without $device_id`)
|
|
317
330
|
}
|
|
318
|
-
return await this.matchFeatureFlagProperties(
|
|
319
|
-
flag,
|
|
320
|
-
bucketingValue,
|
|
321
|
-
personProperties,
|
|
322
|
-
evaluationCache,
|
|
323
|
-
distinctId,
|
|
324
|
-
groups,
|
|
325
|
-
personProperties,
|
|
326
|
-
groupProperties
|
|
327
|
-
)
|
|
331
|
+
return await this.matchFeatureFlagProperties(flag, bucketingValue, personProperties, evaluationContext)
|
|
328
332
|
}
|
|
329
333
|
}
|
|
330
334
|
|
|
@@ -384,13 +388,10 @@ class FeatureFlagsPoller {
|
|
|
384
388
|
|
|
385
389
|
private async evaluateFlagDependency(
|
|
386
390
|
property: FlagProperty,
|
|
387
|
-
distinctId: string,
|
|
388
|
-
groups: Record<string, string>,
|
|
389
|
-
personProperties: Record<string, any>,
|
|
390
|
-
groupProperties: Record<string, Record<string, any>>,
|
|
391
391
|
properties: Record<string, any>,
|
|
392
|
-
|
|
392
|
+
evaluationContext: FeatureFlagEvaluationContext
|
|
393
393
|
): Promise<boolean> {
|
|
394
|
+
const { evaluationCache } = evaluationContext
|
|
394
395
|
const targetFlagKey = property.key
|
|
395
396
|
|
|
396
397
|
if (!this.featureFlagsByKey) {
|
|
@@ -434,14 +435,7 @@ class FeatureFlagsPoller {
|
|
|
434
435
|
} else {
|
|
435
436
|
// Reuse full flag evaluation so dependencies respect person vs group bucketing rules.
|
|
436
437
|
try {
|
|
437
|
-
const depResult = await this.computeFlagValueLocally(
|
|
438
|
-
depFlag,
|
|
439
|
-
distinctId,
|
|
440
|
-
groups,
|
|
441
|
-
personProperties,
|
|
442
|
-
groupProperties,
|
|
443
|
-
evaluationCache
|
|
444
|
-
)
|
|
438
|
+
const depResult = await this.computeFlagValueLocally(depFlag, evaluationContext)
|
|
445
439
|
evaluationCache[depFlagKey] = depResult
|
|
446
440
|
} catch (error) {
|
|
447
441
|
throw new InconclusiveMatchError(
|
|
@@ -486,11 +480,7 @@ class FeatureFlagsPoller {
|
|
|
486
480
|
flag: PostHogFeatureFlag,
|
|
487
481
|
bucketingValue: string,
|
|
488
482
|
properties: Record<string, any>,
|
|
489
|
-
|
|
490
|
-
distinctId: string = bucketingValue,
|
|
491
|
-
groups: Record<string, string> = {},
|
|
492
|
-
personProperties: Record<string, any> = {},
|
|
493
|
-
groupProperties: Record<string, Record<string, any>> = {}
|
|
483
|
+
evaluationContext: FeatureFlagEvaluationContext
|
|
494
484
|
): Promise<FeatureFlagValue> {
|
|
495
485
|
const flagFilters = flag.filters || {}
|
|
496
486
|
const flagConditions = flagFilters.groups || []
|
|
@@ -499,19 +489,7 @@ class FeatureFlagsPoller {
|
|
|
499
489
|
|
|
500
490
|
for (const condition of flagConditions) {
|
|
501
491
|
try {
|
|
502
|
-
if (
|
|
503
|
-
await this.isConditionMatch(
|
|
504
|
-
flag,
|
|
505
|
-
bucketingValue,
|
|
506
|
-
condition,
|
|
507
|
-
properties,
|
|
508
|
-
evaluationCache,
|
|
509
|
-
distinctId,
|
|
510
|
-
groups,
|
|
511
|
-
personProperties,
|
|
512
|
-
groupProperties
|
|
513
|
-
)
|
|
514
|
-
) {
|
|
492
|
+
if (await this.isConditionMatch(flag, bucketingValue, condition, properties, evaluationContext)) {
|
|
515
493
|
const variantOverride = condition.variant
|
|
516
494
|
const flagVariants = flagFilters.multivariate?.variants || []
|
|
517
495
|
if (variantOverride && flagVariants.some((variant) => variant.key === variantOverride)) {
|
|
@@ -551,11 +529,7 @@ class FeatureFlagsPoller {
|
|
|
551
529
|
bucketingValue: string,
|
|
552
530
|
condition: FeatureFlagCondition,
|
|
553
531
|
properties: Record<string, any>,
|
|
554
|
-
|
|
555
|
-
distinctId: string = bucketingValue,
|
|
556
|
-
groups: Record<string, string> = {},
|
|
557
|
-
personProperties: Record<string, any> = {},
|
|
558
|
-
groupProperties: Record<string, Record<string, any>> = {}
|
|
532
|
+
evaluationContext: FeatureFlagEvaluationContext
|
|
559
533
|
): Promise<boolean> {
|
|
560
534
|
const rolloutPercentage = condition.rollout_percentage
|
|
561
535
|
const warnFunction = (msg: string): void => {
|
|
@@ -569,15 +543,7 @@ class FeatureFlagsPoller {
|
|
|
569
543
|
if (propertyType === 'cohort') {
|
|
570
544
|
matches = matchCohort(prop, properties, this.cohorts, this.debugMode)
|
|
571
545
|
} else if (propertyType === 'flag') {
|
|
572
|
-
matches = await this.evaluateFlagDependency(
|
|
573
|
-
prop,
|
|
574
|
-
distinctId,
|
|
575
|
-
groups,
|
|
576
|
-
personProperties,
|
|
577
|
-
groupProperties,
|
|
578
|
-
properties,
|
|
579
|
-
evaluationCache
|
|
580
|
-
)
|
|
546
|
+
matches = await this.evaluateFlagDependency(prop, properties, evaluationContext)
|
|
581
547
|
} else {
|
|
582
548
|
matches = matchProperty(prop, properties, warnFunction)
|
|
583
549
|
}
|