posthog-node 5.25.0 → 5.26.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/client.d.ts +24 -25
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +60 -15
- package/dist/client.mjs +60 -15
- 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 +22 -4
- package/dist/extensions/feature-flags/feature-flags.d.ts.map +1 -1
- package/dist/extensions/feature-flags/feature-flags.js +48 -22
- package/dist/extensions/feature-flags/feature-flags.mjs +48 -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 +134 -58
- package/src/extensions/context/context.ts +19 -14
- package/src/extensions/context/types.ts +1 -0
- package/src/extensions/feature-flags/feature-flags.ts +81 -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
|
)
|
|
@@ -808,7 +821,16 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
808
821
|
locally_evaluated: flagWasLocallyEvaluated,
|
|
809
822
|
[`$feature/${key}`]: response,
|
|
810
823
|
$feature_flag_request_id: requestId,
|
|
811
|
-
$feature_flag_evaluated_at: evaluatedAt,
|
|
824
|
+
$feature_flag_evaluated_at: flagWasLocallyEvaluated ? Date.now() : evaluatedAt,
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Add local evaluation definition load timestamp
|
|
828
|
+
if (flagWasLocallyEvaluated && this.featureFlagsPoller) {
|
|
829
|
+
const flagDefinitionsLoadedAt = this.featureFlagsPoller.getFlagDefinitionsLoadedAt()
|
|
830
|
+
|
|
831
|
+
if (flagDefinitionsLoadedAt !== undefined) {
|
|
832
|
+
properties.$feature_flag_definitions_loaded_at = flagDefinitionsLoadedAt
|
|
833
|
+
}
|
|
812
834
|
}
|
|
813
835
|
|
|
814
836
|
if (featureFlagError) {
|
|
@@ -1008,21 +1030,30 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1008
1030
|
* @param options - Optional configuration for flag evaluation
|
|
1009
1031
|
* @returns Promise that resolves to the flag result or undefined
|
|
1010
1032
|
*/
|
|
1033
|
+
async getFeatureFlagResult(key: string, options?: FlagEvaluationOptions): Promise<FeatureFlagResult | undefined>
|
|
1011
1034
|
async getFeatureFlagResult(
|
|
1012
1035
|
key: string,
|
|
1013
1036
|
distinctId: string,
|
|
1014
|
-
options?:
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
disableGeoip?: boolean
|
|
1021
|
-
}
|
|
1037
|
+
options?: FlagEvaluationOptions
|
|
1038
|
+
): Promise<FeatureFlagResult | undefined>
|
|
1039
|
+
async getFeatureFlagResult(
|
|
1040
|
+
key: string,
|
|
1041
|
+
distinctIdOrOptions?: string | FlagEvaluationOptions,
|
|
1042
|
+
options?: FlagEvaluationOptions
|
|
1022
1043
|
): Promise<FeatureFlagResult | undefined> {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1044
|
+
const { distinctId: resolvedDistinctId, options: resolvedOptions } = this._resolveDistinctId(
|
|
1045
|
+
distinctIdOrOptions,
|
|
1046
|
+
options
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
if (!resolvedDistinctId) {
|
|
1050
|
+
this._logger.warn('[PostHog] distinctId is required — pass it explicitly or use withContext()')
|
|
1051
|
+
return undefined
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return this._getFeatureFlagResult(key, resolvedDistinctId, {
|
|
1055
|
+
...resolvedOptions,
|
|
1056
|
+
sendFeatureFlagEvents: resolvedOptions?.sendFeatureFlagEvents ?? this.options.sendFeatureFlagEvent ?? true,
|
|
1026
1057
|
})
|
|
1027
1058
|
}
|
|
1028
1059
|
|
|
@@ -1155,18 +1186,24 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1155
1186
|
* @param options - Optional configuration for flag evaluation
|
|
1156
1187
|
* @returns Promise that resolves to a record of flag keys and their values
|
|
1157
1188
|
*/
|
|
1189
|
+
async getAllFlags(options?: AllFlagsOptions): Promise<Record<string, FeatureFlagValue>>
|
|
1190
|
+
async getAllFlags(distinctId: string, options?: AllFlagsOptions): Promise<Record<string, FeatureFlagValue>>
|
|
1158
1191
|
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
|
-
}
|
|
1192
|
+
distinctIdOrOptions?: string | AllFlagsOptions,
|
|
1193
|
+
options?: AllFlagsOptions
|
|
1168
1194
|
): Promise<Record<string, FeatureFlagValue>> {
|
|
1169
|
-
const
|
|
1195
|
+
const { distinctId: resolvedDistinctId, options: resolvedOptions } = this._resolveDistinctId(
|
|
1196
|
+
distinctIdOrOptions,
|
|
1197
|
+
options
|
|
1198
|
+
)
|
|
1199
|
+
if (!resolvedDistinctId) {
|
|
1200
|
+
this._logger.warn(
|
|
1201
|
+
'[PostHog] distinctId is required to get feature flags — pass it explicitly or use withContext()'
|
|
1202
|
+
)
|
|
1203
|
+
return {}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const response = await this.getAllFlagsAndPayloads(resolvedDistinctId, resolvedOptions)
|
|
1170
1207
|
return response.featureFlags || {}
|
|
1171
1208
|
}
|
|
1172
1209
|
|
|
@@ -1203,22 +1240,28 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1203
1240
|
* @param options - Optional configuration for flag evaluation
|
|
1204
1241
|
* @returns Promise that resolves to flags and payloads
|
|
1205
1242
|
*/
|
|
1243
|
+
async getAllFlagsAndPayloads(options?: AllFlagsOptions): Promise<PostHogFlagsAndPayloadsResponse>
|
|
1244
|
+
async getAllFlagsAndPayloads(distinctId: string, options?: AllFlagsOptions): Promise<PostHogFlagsAndPayloadsResponse>
|
|
1206
1245
|
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
|
-
}
|
|
1246
|
+
distinctIdOrOptions?: string | AllFlagsOptions,
|
|
1247
|
+
options?: AllFlagsOptions
|
|
1216
1248
|
): Promise<PostHogFlagsAndPayloadsResponse> {
|
|
1217
|
-
const {
|
|
1218
|
-
|
|
1249
|
+
const { distinctId: resolvedDistinctId, options: resolvedOptions } = this._resolveDistinctId(
|
|
1250
|
+
distinctIdOrOptions,
|
|
1251
|
+
options
|
|
1252
|
+
)
|
|
1253
|
+
if (!resolvedDistinctId) {
|
|
1254
|
+
this._logger.warn(
|
|
1255
|
+
'[PostHog] distinctId is required to get feature flags and payloads — pass it explicitly or use withContext()'
|
|
1256
|
+
)
|
|
1257
|
+
return { featureFlags: {}, featureFlagPayloads: {} }
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const { groups, disableGeoip, flagKeys } = resolvedOptions || {}
|
|
1261
|
+
let { onlyEvaluateLocally, personProperties, groupProperties } = resolvedOptions || {}
|
|
1219
1262
|
|
|
1220
1263
|
const adjustedProperties = this.addLocalPersonAndGroupProperties(
|
|
1221
|
-
|
|
1264
|
+
resolvedDistinctId,
|
|
1222
1265
|
groups,
|
|
1223
1266
|
personProperties,
|
|
1224
1267
|
groupProperties
|
|
@@ -1226,19 +1269,19 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1226
1269
|
|
|
1227
1270
|
personProperties = adjustedProperties.allPersonProperties
|
|
1228
1271
|
groupProperties = adjustedProperties.allGroupProperties
|
|
1272
|
+
const evaluationContext = this.createFeatureFlagEvaluationContext(
|
|
1273
|
+
resolvedDistinctId,
|
|
1274
|
+
groups,
|
|
1275
|
+
personProperties,
|
|
1276
|
+
groupProperties
|
|
1277
|
+
)
|
|
1229
1278
|
|
|
1230
1279
|
// set defaults
|
|
1231
1280
|
if (onlyEvaluateLocally == undefined) {
|
|
1232
1281
|
onlyEvaluateLocally = this.options.strictLocalEvaluation ?? false
|
|
1233
1282
|
}
|
|
1234
1283
|
|
|
1235
|
-
const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(
|
|
1236
|
-
distinctId,
|
|
1237
|
-
groups,
|
|
1238
|
-
personProperties,
|
|
1239
|
-
groupProperties,
|
|
1240
|
-
flagKeys
|
|
1241
|
-
)
|
|
1284
|
+
const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(evaluationContext, flagKeys)
|
|
1242
1285
|
|
|
1243
1286
|
let featureFlags = {}
|
|
1244
1287
|
let featureFlagPayloads = {}
|
|
@@ -1251,10 +1294,10 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1251
1294
|
|
|
1252
1295
|
if (fallbackToFlags && !onlyEvaluateLocally) {
|
|
1253
1296
|
const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(
|
|
1254
|
-
distinctId,
|
|
1255
|
-
groups,
|
|
1256
|
-
personProperties,
|
|
1257
|
-
groupProperties,
|
|
1297
|
+
evaluationContext.distinctId,
|
|
1298
|
+
evaluationContext.groups,
|
|
1299
|
+
evaluationContext.personProperties,
|
|
1300
|
+
evaluationContext.groupProperties,
|
|
1258
1301
|
disableGeoip,
|
|
1259
1302
|
flagKeys
|
|
1260
1303
|
)
|
|
@@ -1510,6 +1553,24 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1510
1553
|
return this.context?.get()
|
|
1511
1554
|
}
|
|
1512
1555
|
|
|
1556
|
+
/**
|
|
1557
|
+
* Set context without a callback wrapper.
|
|
1558
|
+
*
|
|
1559
|
+
* Uses `AsyncLocalStorage.enterWith()` to attach context to the current
|
|
1560
|
+
* async execution context. The context lives until that async context ends.
|
|
1561
|
+
*
|
|
1562
|
+
* Must be called in the same async scope that makes PostHog calls.
|
|
1563
|
+
* Calling this outside a request-scoped async context will leak context
|
|
1564
|
+
* across unrelated work. Prefer `withContext()` when you can wrap code
|
|
1565
|
+
* in a callback — it creates an isolated scope that cleans up automatically.
|
|
1566
|
+
*
|
|
1567
|
+
* @param data - Context data to apply (distinctId, sessionId, properties)
|
|
1568
|
+
* @param options - Context options (fresh: true to start with clean context instead of inheriting)
|
|
1569
|
+
*/
|
|
1570
|
+
enterContext(data: Partial<ContextData>, options?: ContextOptions): void {
|
|
1571
|
+
this.context?.enter(data as ContextData, options)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1513
1574
|
/**
|
|
1514
1575
|
* Shutdown the PostHog client gracefully.
|
|
1515
1576
|
*
|
|
@@ -1691,6 +1752,21 @@ export abstract class PostHogBackendClient extends PostHogCoreStateless implemen
|
|
|
1691
1752
|
return { allPersonProperties, allGroupProperties }
|
|
1692
1753
|
}
|
|
1693
1754
|
|
|
1755
|
+
private createFeatureFlagEvaluationContext(
|
|
1756
|
+
distinctId: string,
|
|
1757
|
+
groups?: Record<string, string>,
|
|
1758
|
+
personProperties?: Record<string, any>,
|
|
1759
|
+
groupProperties?: Record<string, Record<string, any>>
|
|
1760
|
+
): FeatureFlagEvaluationContext {
|
|
1761
|
+
return {
|
|
1762
|
+
distinctId,
|
|
1763
|
+
groups: groups || {},
|
|
1764
|
+
personProperties: personProperties || {},
|
|
1765
|
+
groupProperties: groupProperties || {},
|
|
1766
|
+
evaluationCache: {},
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1694
1770
|
/**
|
|
1695
1771
|
* Capture an error exception as an event.
|
|
1696
1772
|
*
|
|
@@ -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
|
|
@@ -82,6 +95,7 @@ class FeatureFlagsPoller {
|
|
|
82
95
|
private flagsEtag?: string
|
|
83
96
|
private nextFetchAllowedAt?: number
|
|
84
97
|
private strictLocalEvaluation: boolean
|
|
98
|
+
private flagDefinitionsLoadedAt?: number
|
|
85
99
|
|
|
86
100
|
constructor({
|
|
87
101
|
pollingInterval,
|
|
@@ -122,6 +136,22 @@ class FeatureFlagsPoller {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
private createEvaluationContext(
|
|
140
|
+
distinctId: string,
|
|
141
|
+
groups: Record<string, string> = {},
|
|
142
|
+
personProperties: Record<string, any> = {},
|
|
143
|
+
groupProperties: Record<string, Record<string, any>> = {},
|
|
144
|
+
evaluationCache: Record<string, FeatureFlagValue> = {}
|
|
145
|
+
): FeatureFlagEvaluationContext {
|
|
146
|
+
return {
|
|
147
|
+
distinctId,
|
|
148
|
+
groups,
|
|
149
|
+
personProperties,
|
|
150
|
+
groupProperties,
|
|
151
|
+
evaluationCache,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
125
155
|
async getFeatureFlag(
|
|
126
156
|
key: string,
|
|
127
157
|
distinctId: string,
|
|
@@ -141,14 +171,9 @@ class FeatureFlagsPoller {
|
|
|
141
171
|
featureFlag = this.featureFlagsByKey[key]
|
|
142
172
|
|
|
143
173
|
if (featureFlag !== undefined) {
|
|
174
|
+
const evaluationContext = this.createEvaluationContext(distinctId, groups, personProperties, groupProperties)
|
|
144
175
|
try {
|
|
145
|
-
const result = await this.computeFlagAndPayloadLocally(
|
|
146
|
-
featureFlag,
|
|
147
|
-
distinctId,
|
|
148
|
-
groups,
|
|
149
|
-
personProperties,
|
|
150
|
-
groupProperties
|
|
151
|
-
)
|
|
176
|
+
const result = await this.computeFlagAndPayloadLocally(featureFlag, evaluationContext)
|
|
152
177
|
response = result.value
|
|
153
178
|
this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`))
|
|
154
179
|
} catch (e) {
|
|
@@ -164,10 +189,7 @@ class FeatureFlagsPoller {
|
|
|
164
189
|
}
|
|
165
190
|
|
|
166
191
|
async getAllFlagsAndPayloads(
|
|
167
|
-
|
|
168
|
-
groups: Record<string, string> = {},
|
|
169
|
-
personProperties: Record<string, any> = {},
|
|
170
|
-
groupProperties: Record<string, Record<string, any>> = {},
|
|
192
|
+
evaluationContext: FeatureFlagEvaluationContext,
|
|
171
193
|
flagKeysToExplicitlyEvaluate?: string[]
|
|
172
194
|
): Promise<{
|
|
173
195
|
response: Record<string, FeatureFlagValue>
|
|
@@ -184,20 +206,17 @@ class FeatureFlagsPoller {
|
|
|
184
206
|
? flagKeysToExplicitlyEvaluate.map((key) => this.featureFlagsByKey[key]).filter(Boolean)
|
|
185
207
|
: this.featureFlags
|
|
186
208
|
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
const sharedEvaluationContext = {
|
|
210
|
+
...evaluationContext,
|
|
211
|
+
evaluationCache: evaluationContext.evaluationCache ?? {},
|
|
212
|
+
}
|
|
189
213
|
|
|
190
214
|
await Promise.all(
|
|
191
215
|
flagsToEvaluate.map(async (flag) => {
|
|
192
216
|
try {
|
|
193
217
|
const { value: matchValue, payload: matchPayload } = await this.computeFlagAndPayloadLocally(
|
|
194
218
|
flag,
|
|
195
|
-
|
|
196
|
-
groups,
|
|
197
|
-
personProperties,
|
|
198
|
-
groupProperties,
|
|
199
|
-
undefined /* matchValue */,
|
|
200
|
-
sharedEvaluationCache
|
|
219
|
+
sharedEvaluationContext
|
|
201
220
|
)
|
|
202
221
|
response[flag.key] = matchValue
|
|
203
222
|
if (matchPayload) {
|
|
@@ -219,17 +238,14 @@ class FeatureFlagsPoller {
|
|
|
219
238
|
|
|
220
239
|
async computeFlagAndPayloadLocally(
|
|
221
240
|
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
|
|
241
|
+
evaluationContext: FeatureFlagEvaluationContext,
|
|
242
|
+
options: ComputeFlagAndPayloadOptions = {}
|
|
229
243
|
): Promise<{
|
|
230
244
|
value: FeatureFlagValue
|
|
231
245
|
payload: JsonType | null
|
|
232
246
|
}> {
|
|
247
|
+
const { matchValue, skipLoadCheck = false } = options
|
|
248
|
+
|
|
233
249
|
// Only load flags if not already loaded and not skipping the check
|
|
234
250
|
if (!skipLoadCheck) {
|
|
235
251
|
await this.loadFeatureFlags()
|
|
@@ -245,14 +261,7 @@ class FeatureFlagsPoller {
|
|
|
245
261
|
if (matchValue !== undefined) {
|
|
246
262
|
flagValue = matchValue
|
|
247
263
|
} else {
|
|
248
|
-
flagValue = await this.computeFlagValueLocally(
|
|
249
|
-
flag,
|
|
250
|
-
distinctId,
|
|
251
|
-
groups,
|
|
252
|
-
personProperties,
|
|
253
|
-
groupProperties,
|
|
254
|
-
evaluationCache
|
|
255
|
-
)
|
|
264
|
+
flagValue = await this.computeFlagValueLocally(flag, evaluationContext)
|
|
256
265
|
}
|
|
257
266
|
|
|
258
267
|
// Always compute payload based on the final flagValue (whether provided or computed)
|
|
@@ -263,12 +272,10 @@ class FeatureFlagsPoller {
|
|
|
263
272
|
|
|
264
273
|
private async computeFlagValueLocally(
|
|
265
274
|
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> = {}
|
|
275
|
+
evaluationContext: FeatureFlagEvaluationContext
|
|
271
276
|
): Promise<FeatureFlagValue> {
|
|
277
|
+
const { distinctId, groups, personProperties, groupProperties } = evaluationContext
|
|
278
|
+
|
|
272
279
|
if (flag.ensure_experience_continuity) {
|
|
273
280
|
throw new InconclusiveMatchError('Flag has experience continuity enabled')
|
|
274
281
|
}
|
|
@@ -299,32 +306,30 @@ class FeatureFlagsPoller {
|
|
|
299
306
|
return false
|
|
300
307
|
}
|
|
301
308
|
|
|
309
|
+
if (
|
|
310
|
+
flag.bucketing_identifier === 'device_id' &&
|
|
311
|
+
(personProperties?.$device_id === undefined ||
|
|
312
|
+
personProperties?.$device_id === null ||
|
|
313
|
+
personProperties?.$device_id === '')
|
|
314
|
+
) {
|
|
315
|
+
this.logMsgIfDebug(() =>
|
|
316
|
+
console.warn(`[FEATURE FLAGS] Ignoring bucketing_identifier for group flag: ${flag.key}`)
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
302
320
|
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
|
-
)
|
|
321
|
+
return await this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties, evaluationContext)
|
|
313
322
|
} else {
|
|
314
323
|
const bucketingValue = this.getBucketingValueForFlag(flag, distinctId, personProperties)
|
|
315
324
|
if (bucketingValue === undefined) {
|
|
325
|
+
this.logMsgIfDebug(() =>
|
|
326
|
+
console.warn(
|
|
327
|
+
`[FEATURE FLAGS] Can't compute feature flag: ${flag.key} without $device_id, falling back to server evaluation`
|
|
328
|
+
)
|
|
329
|
+
)
|
|
316
330
|
throw new InconclusiveMatchError(`Can't compute feature flag: ${flag.key} without $device_id`)
|
|
317
331
|
}
|
|
318
|
-
return await this.matchFeatureFlagProperties(
|
|
319
|
-
flag,
|
|
320
|
-
bucketingValue,
|
|
321
|
-
personProperties,
|
|
322
|
-
evaluationCache,
|
|
323
|
-
distinctId,
|
|
324
|
-
groups,
|
|
325
|
-
personProperties,
|
|
326
|
-
groupProperties
|
|
327
|
-
)
|
|
332
|
+
return await this.matchFeatureFlagProperties(flag, bucketingValue, personProperties, evaluationContext)
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
335
|
|
|
@@ -384,13 +389,10 @@ class FeatureFlagsPoller {
|
|
|
384
389
|
|
|
385
390
|
private async evaluateFlagDependency(
|
|
386
391
|
property: FlagProperty,
|
|
387
|
-
distinctId: string,
|
|
388
|
-
groups: Record<string, string>,
|
|
389
|
-
personProperties: Record<string, any>,
|
|
390
|
-
groupProperties: Record<string, Record<string, any>>,
|
|
391
392
|
properties: Record<string, any>,
|
|
392
|
-
|
|
393
|
+
evaluationContext: FeatureFlagEvaluationContext
|
|
393
394
|
): Promise<boolean> {
|
|
395
|
+
const { evaluationCache } = evaluationContext
|
|
394
396
|
const targetFlagKey = property.key
|
|
395
397
|
|
|
396
398
|
if (!this.featureFlagsByKey) {
|
|
@@ -434,14 +436,7 @@ class FeatureFlagsPoller {
|
|
|
434
436
|
} else {
|
|
435
437
|
// Reuse full flag evaluation so dependencies respect person vs group bucketing rules.
|
|
436
438
|
try {
|
|
437
|
-
const depResult = await this.computeFlagValueLocally(
|
|
438
|
-
depFlag,
|
|
439
|
-
distinctId,
|
|
440
|
-
groups,
|
|
441
|
-
personProperties,
|
|
442
|
-
groupProperties,
|
|
443
|
-
evaluationCache
|
|
444
|
-
)
|
|
439
|
+
const depResult = await this.computeFlagValueLocally(depFlag, evaluationContext)
|
|
445
440
|
evaluationCache[depFlagKey] = depResult
|
|
446
441
|
} catch (error) {
|
|
447
442
|
throw new InconclusiveMatchError(
|
|
@@ -486,11 +481,7 @@ class FeatureFlagsPoller {
|
|
|
486
481
|
flag: PostHogFeatureFlag,
|
|
487
482
|
bucketingValue: string,
|
|
488
483
|
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>> = {}
|
|
484
|
+
evaluationContext: FeatureFlagEvaluationContext
|
|
494
485
|
): Promise<FeatureFlagValue> {
|
|
495
486
|
const flagFilters = flag.filters || {}
|
|
496
487
|
const flagConditions = flagFilters.groups || []
|
|
@@ -499,19 +490,7 @@ class FeatureFlagsPoller {
|
|
|
499
490
|
|
|
500
491
|
for (const condition of flagConditions) {
|
|
501
492
|
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
|
-
) {
|
|
493
|
+
if (await this.isConditionMatch(flag, bucketingValue, condition, properties, evaluationContext)) {
|
|
515
494
|
const variantOverride = condition.variant
|
|
516
495
|
const flagVariants = flagFilters.multivariate?.variants || []
|
|
517
496
|
if (variantOverride && flagVariants.some((variant) => variant.key === variantOverride)) {
|
|
@@ -551,11 +530,7 @@ class FeatureFlagsPoller {
|
|
|
551
530
|
bucketingValue: string,
|
|
552
531
|
condition: FeatureFlagCondition,
|
|
553
532
|
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>> = {}
|
|
533
|
+
evaluationContext: FeatureFlagEvaluationContext
|
|
559
534
|
): Promise<boolean> {
|
|
560
535
|
const rolloutPercentage = condition.rollout_percentage
|
|
561
536
|
const warnFunction = (msg: string): void => {
|
|
@@ -569,15 +544,7 @@ class FeatureFlagsPoller {
|
|
|
569
544
|
if (propertyType === 'cohort') {
|
|
570
545
|
matches = matchCohort(prop, properties, this.cohorts, this.debugMode)
|
|
571
546
|
} else if (propertyType === 'flag') {
|
|
572
|
-
matches = await this.evaluateFlagDependency(
|
|
573
|
-
prop,
|
|
574
|
-
distinctId,
|
|
575
|
-
groups,
|
|
576
|
-
personProperties,
|
|
577
|
-
groupProperties,
|
|
578
|
-
properties,
|
|
579
|
-
evaluationCache
|
|
580
|
-
)
|
|
547
|
+
matches = await this.evaluateFlagDependency(prop, properties, evaluationContext)
|
|
581
548
|
} else {
|
|
582
549
|
matches = matchProperty(prop, properties, warnFunction)
|
|
583
550
|
}
|
|
@@ -724,6 +691,14 @@ class FeatureFlagsPoller {
|
|
|
724
691
|
return (this.loadedSuccessfullyOnce ?? false) && (this.featureFlags?.length ?? 0) > 0
|
|
725
692
|
}
|
|
726
693
|
|
|
694
|
+
/**
|
|
695
|
+
* Returns the timestamp (in milliseconds) when flag definitions were last loaded.
|
|
696
|
+
* Returns undefined if flags have not been loaded yet.
|
|
697
|
+
*/
|
|
698
|
+
getFlagDefinitionsLoadedAt(): number | undefined {
|
|
699
|
+
return this.flagDefinitionsLoadedAt
|
|
700
|
+
}
|
|
701
|
+
|
|
727
702
|
/**
|
|
728
703
|
* If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
|
|
729
704
|
* until a successful request is made, up to a maximum of 60 seconds.
|
|
@@ -885,6 +860,8 @@ class FeatureFlagsPoller {
|
|
|
885
860
|
}
|
|
886
861
|
|
|
887
862
|
this.updateFlagState(flagData)
|
|
863
|
+
// Set timestamp to when definitions were actually fetched from server
|
|
864
|
+
this.flagDefinitionsLoadedAt = Date.now()
|
|
888
865
|
this.clearBackoff()
|
|
889
866
|
|
|
890
867
|
if (this.cacheProvider && shouldFetch) {
|