posthog-node 2.5.3 → 2.6.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/CHANGELOG.md +7 -0
- package/lib/index.cjs.js +163 -10
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.esm.js +163 -10
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +1 -0
- package/lib/posthog-node/src/feature-flags.d.ts +4 -1
- package/lib/posthog-node/src/posthog-node.d.ts +1 -0
- package/lib/posthog-node/src/types.d.ts +12 -6
- package/package.json +1 -1
- package/src/feature-flags.ts +143 -10
- package/src/posthog-node.ts +5 -0
- package/src/types.ts +14 -6
- package/test/feature-flags.spec.ts +173 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import { FeatureFlagCondition, PostHogFeatureFlag } from './types';
|
|
2
|
+
import { FeatureFlagCondition, PostHogFeatureFlag, PropertyGroup } from './types';
|
|
3
3
|
import { JsonType, PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src';
|
|
4
4
|
declare class ClientError extends Error {
|
|
5
5
|
constructor(message: string);
|
|
@@ -22,12 +22,15 @@ declare class FeatureFlagsPoller {
|
|
|
22
22
|
featureFlags: Array<PostHogFeatureFlag>;
|
|
23
23
|
featureFlagsByKey: Record<string, PostHogFeatureFlag>;
|
|
24
24
|
groupTypeMapping: Record<string, string>;
|
|
25
|
+
cohorts: Record<string, PropertyGroup>;
|
|
25
26
|
loadedSuccessfullyOnce: boolean;
|
|
26
27
|
timeout?: number;
|
|
27
28
|
host: FeatureFlagsPollerOptions['host'];
|
|
28
29
|
poller?: NodeJS.Timeout;
|
|
29
30
|
fetch: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
|
|
31
|
+
debugMode: boolean;
|
|
30
32
|
constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host, ...options }: FeatureFlagsPollerOptions);
|
|
33
|
+
debug(enabled?: boolean): void;
|
|
31
34
|
getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<string | boolean | undefined>;
|
|
32
35
|
computeFeatureFlagPayloadLocally(key: string, matchValue: string | boolean): Promise<JsonType | undefined>;
|
|
33
36
|
getAllFlagsAndPayloads(distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<{
|
|
@@ -23,6 +23,7 @@ export declare class PostHog extends PostHogCoreStateless implements PostHogNode
|
|
|
23
23
|
getCustomUserAgent(): string;
|
|
24
24
|
enable(): void;
|
|
25
25
|
disable(): void;
|
|
26
|
+
debug(enabled?: boolean): void;
|
|
26
27
|
capture({ distinctId, event, properties, groups, sendFeatureFlags, timestamp }: EventMessageV1): void;
|
|
27
28
|
identify({ distinctId, properties }: IdentifyMessageV1): void;
|
|
28
29
|
alias(data: {
|
|
@@ -15,13 +15,19 @@ export interface GroupIdentifyMessage {
|
|
|
15
15
|
properties?: Record<string | number, any>;
|
|
16
16
|
distinctId?: string;
|
|
17
17
|
}
|
|
18
|
+
export declare type PropertyGroup = {
|
|
19
|
+
type: 'AND' | 'OR';
|
|
20
|
+
values: PropertyGroup[] | FlagProperty[];
|
|
21
|
+
};
|
|
22
|
+
export declare type FlagProperty = {
|
|
23
|
+
key: string;
|
|
24
|
+
type?: string;
|
|
25
|
+
value: string | number | (string | number)[];
|
|
26
|
+
operator?: string;
|
|
27
|
+
negation?: boolean;
|
|
28
|
+
};
|
|
18
29
|
export declare type FeatureFlagCondition = {
|
|
19
|
-
properties:
|
|
20
|
-
key: string;
|
|
21
|
-
type?: string;
|
|
22
|
-
value: string | number | (string | number)[];
|
|
23
|
-
operator?: string;
|
|
24
|
-
}[];
|
|
30
|
+
properties: FlagProperty[];
|
|
25
31
|
rollout_percentage?: number;
|
|
26
32
|
variant?: string;
|
|
27
33
|
};
|
package/package.json
CHANGED
package/src/feature-flags.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'crypto'
|
|
2
|
-
import { FeatureFlagCondition, PostHogFeatureFlag } from './types'
|
|
2
|
+
import { FeatureFlagCondition, FlagProperty, PostHogFeatureFlag, PropertyGroup } from './types'
|
|
3
3
|
import { version } from '../package.json'
|
|
4
4
|
import { JsonType, PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
|
|
5
5
|
import { safeSetTimeout } from 'posthog-core/src/utils'
|
|
@@ -46,11 +46,13 @@ class FeatureFlagsPoller {
|
|
|
46
46
|
featureFlags: Array<PostHogFeatureFlag>
|
|
47
47
|
featureFlagsByKey: Record<string, PostHogFeatureFlag>
|
|
48
48
|
groupTypeMapping: Record<string, string>
|
|
49
|
+
cohorts: Record<string, PropertyGroup>
|
|
49
50
|
loadedSuccessfullyOnce: boolean
|
|
50
51
|
timeout?: number
|
|
51
52
|
host: FeatureFlagsPollerOptions['host']
|
|
52
53
|
poller?: NodeJS.Timeout
|
|
53
54
|
fetch: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
|
|
55
|
+
debugMode: boolean = false
|
|
54
56
|
|
|
55
57
|
constructor({
|
|
56
58
|
pollingInterval,
|
|
@@ -65,6 +67,7 @@ class FeatureFlagsPoller {
|
|
|
65
67
|
this.featureFlags = []
|
|
66
68
|
this.featureFlagsByKey = {}
|
|
67
69
|
this.groupTypeMapping = {}
|
|
70
|
+
this.cohorts = {}
|
|
68
71
|
this.loadedSuccessfullyOnce = false
|
|
69
72
|
this.timeout = timeout
|
|
70
73
|
this.projectApiKey = projectApiKey
|
|
@@ -76,6 +79,10 @@ class FeatureFlagsPoller {
|
|
|
76
79
|
void this.loadFeatureFlags()
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
debug(enabled: boolean = true): void {
|
|
83
|
+
this.debugMode = enabled
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
async getFeatureFlag(
|
|
80
87
|
key: string,
|
|
81
88
|
distinctId: string,
|
|
@@ -102,9 +109,14 @@ class FeatureFlagsPoller {
|
|
|
102
109
|
if (featureFlag !== undefined) {
|
|
103
110
|
try {
|
|
104
111
|
response = this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties)
|
|
112
|
+
if (this.debugMode) {
|
|
113
|
+
console.debug(`Successfully computed flag locally: ${key} -> ${response}`)
|
|
114
|
+
}
|
|
105
115
|
} catch (e) {
|
|
106
116
|
if (e instanceof InconclusiveMatchError) {
|
|
107
|
-
|
|
117
|
+
if (this.debugMode) {
|
|
118
|
+
console.debug(`InconclusiveMatchError when computing flag locally: ${key}: ${e}`)
|
|
119
|
+
}
|
|
108
120
|
} else if (e instanceof Error) {
|
|
109
121
|
console.error(`Error computing flag locally: ${key}: ${e}`)
|
|
110
122
|
}
|
|
@@ -281,13 +293,22 @@ class FeatureFlagsPoller {
|
|
|
281
293
|
const rolloutPercentage = condition.rollout_percentage
|
|
282
294
|
|
|
283
295
|
if ((condition.properties || []).length > 0) {
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
296
|
+
for (const prop of condition.properties) {
|
|
297
|
+
const propertyType = prop.type
|
|
298
|
+
let matches = false
|
|
299
|
+
|
|
300
|
+
if (propertyType === 'cohort') {
|
|
301
|
+
matches = matchCohort(prop, properties, this.cohorts)
|
|
302
|
+
} else {
|
|
303
|
+
matches = matchProperty(prop, properties)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!matches) {
|
|
307
|
+
return false
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (rolloutPercentage == undefined) {
|
|
291
312
|
return true
|
|
292
313
|
}
|
|
293
314
|
}
|
|
@@ -368,6 +389,7 @@ class FeatureFlagsPoller {
|
|
|
368
389
|
<Record<string, PostHogFeatureFlag>>{}
|
|
369
390
|
)
|
|
370
391
|
this.groupTypeMapping = responseJson.group_type_mapping || {}
|
|
392
|
+
this.cohorts = responseJson.cohorts || []
|
|
371
393
|
this.loadedSuccessfullyOnce = true
|
|
372
394
|
} catch (err) {
|
|
373
395
|
// if an error that is not an instance of ClientError is thrown
|
|
@@ -379,7 +401,7 @@ class FeatureFlagsPoller {
|
|
|
379
401
|
}
|
|
380
402
|
|
|
381
403
|
async _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse> {
|
|
382
|
-
const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}`
|
|
404
|
+
const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`
|
|
383
405
|
|
|
384
406
|
const options: PostHogFetchOptions = {
|
|
385
407
|
method: 'GET',
|
|
@@ -477,6 +499,117 @@ function matchProperty(
|
|
|
477
499
|
}
|
|
478
500
|
}
|
|
479
501
|
|
|
502
|
+
function matchCohort(
|
|
503
|
+
property: FeatureFlagCondition['properties'][number],
|
|
504
|
+
propertyValues: Record<string, any>,
|
|
505
|
+
cohortProperties: FeatureFlagsPoller['cohorts']
|
|
506
|
+
): boolean {
|
|
507
|
+
const cohortId = String(property.value)
|
|
508
|
+
if (!(cohortId in cohortProperties)) {
|
|
509
|
+
throw new InconclusiveMatchError("can't match cohort without a given cohort property value")
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const propertyGroup = cohortProperties[cohortId]
|
|
513
|
+
return matchPropertyGroup(propertyGroup, propertyValues, cohortProperties)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function matchPropertyGroup(
|
|
517
|
+
propertyGroup: PropertyGroup,
|
|
518
|
+
propertyValues: Record<string, any>,
|
|
519
|
+
cohortProperties: FeatureFlagsPoller['cohorts']
|
|
520
|
+
): boolean {
|
|
521
|
+
if (!propertyGroup) {
|
|
522
|
+
return true
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const propertyGroupType = propertyGroup.type
|
|
526
|
+
const properties = propertyGroup.values
|
|
527
|
+
|
|
528
|
+
if (!properties || properties.length === 0) {
|
|
529
|
+
// empty groups are no-ops, always match
|
|
530
|
+
return true
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
let errorMatchingLocally = false
|
|
534
|
+
|
|
535
|
+
if ('values' in properties[0]) {
|
|
536
|
+
// a nested property group
|
|
537
|
+
for (const prop of properties as PropertyGroup[]) {
|
|
538
|
+
try {
|
|
539
|
+
const matches = matchPropertyGroup(prop, propertyValues, cohortProperties)
|
|
540
|
+
if (propertyGroupType === 'AND') {
|
|
541
|
+
if (!matches) {
|
|
542
|
+
return false
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
// OR group
|
|
546
|
+
if (matches) {
|
|
547
|
+
return true
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch (err) {
|
|
551
|
+
if (err instanceof InconclusiveMatchError) {
|
|
552
|
+
console.debug(`Failed to compute property ${prop} locally: ${err}`)
|
|
553
|
+
errorMatchingLocally = true
|
|
554
|
+
} else {
|
|
555
|
+
throw err
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (errorMatchingLocally) {
|
|
561
|
+
throw new InconclusiveMatchError("Can't match cohort without a given cohort property value")
|
|
562
|
+
}
|
|
563
|
+
// if we get here, all matched in AND case, or none matched in OR case
|
|
564
|
+
return propertyGroupType === 'AND'
|
|
565
|
+
} else {
|
|
566
|
+
for (const prop of properties as FlagProperty[]) {
|
|
567
|
+
try {
|
|
568
|
+
let matches: boolean
|
|
569
|
+
if (prop.type === 'cohort') {
|
|
570
|
+
matches = matchCohort(prop, propertyValues, cohortProperties)
|
|
571
|
+
} else {
|
|
572
|
+
matches = matchProperty(prop, propertyValues)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const negation = prop.negation || false
|
|
576
|
+
|
|
577
|
+
if (propertyGroupType === 'AND') {
|
|
578
|
+
// if negated property, do the inverse
|
|
579
|
+
if (!matches && !negation) {
|
|
580
|
+
return false
|
|
581
|
+
}
|
|
582
|
+
if (matches && negation) {
|
|
583
|
+
return false
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
// OR group
|
|
587
|
+
if (matches && !negation) {
|
|
588
|
+
return true
|
|
589
|
+
}
|
|
590
|
+
if (!matches && negation) {
|
|
591
|
+
return true
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} catch (err) {
|
|
595
|
+
if (err instanceof InconclusiveMatchError) {
|
|
596
|
+
console.debug(`Failed to compute property ${prop} locally: ${err}`)
|
|
597
|
+
errorMatchingLocally = true
|
|
598
|
+
} else {
|
|
599
|
+
throw err
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (errorMatchingLocally) {
|
|
605
|
+
throw new InconclusiveMatchError("can't match cohort without a given cohort property value")
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// if we get here, all matched in AND case, or none matched in OR case
|
|
609
|
+
return propertyGroupType === 'AND'
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
480
613
|
function isValidRegex(regex: string): boolean {
|
|
481
614
|
try {
|
|
482
615
|
new RegExp(regex)
|
package/src/posthog-node.ts
CHANGED
|
@@ -92,6 +92,11 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
|
|
|
92
92
|
return super.optOut()
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
debug(enabled: boolean = true): void {
|
|
96
|
+
super.debug(enabled)
|
|
97
|
+
this.featureFlagsPoller?.debug(enabled)
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
capture({ distinctId, event, properties, groups, sendFeatureFlags, timestamp }: EventMessageV1): void {
|
|
96
101
|
const _capture = (props: EventMessageV1['properties']): void => {
|
|
97
102
|
super.captureStateless(distinctId, event, props, { timestamp })
|
package/src/types.ts
CHANGED
|
@@ -19,13 +19,21 @@ export interface GroupIdentifyMessage {
|
|
|
19
19
|
distinctId?: string // optional distinctId to associate message with a person
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export type PropertyGroup = {
|
|
23
|
+
type: 'AND' | 'OR'
|
|
24
|
+
values: PropertyGroup[] | FlagProperty[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FlagProperty = {
|
|
28
|
+
key: string
|
|
29
|
+
type?: string
|
|
30
|
+
value: string | number | (string | number)[]
|
|
31
|
+
operator?: string
|
|
32
|
+
negation?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
export type FeatureFlagCondition = {
|
|
23
|
-
properties:
|
|
24
|
-
key: string
|
|
25
|
-
type?: string
|
|
26
|
-
value: string | number | (string | number)[]
|
|
27
|
-
operator?: string
|
|
28
|
-
}[]
|
|
36
|
+
properties: FlagProperty[]
|
|
29
37
|
rollout_percentage?: number
|
|
30
38
|
variant?: string
|
|
31
39
|
}
|
|
@@ -38,7 +38,7 @@ export const apiImplementation = ({
|
|
|
38
38
|
}) as any
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
if ((url as any).includes('api/feature_flag/local_evaluation?token=TEST_API_KEY')) {
|
|
41
|
+
if ((url as any).includes('api/feature_flag/local_evaluation?token=TEST_API_KEY&send_cohorts')) {
|
|
42
42
|
return Promise.resolve({
|
|
43
43
|
status: 200,
|
|
44
44
|
text: () => Promise.resolve('ok'),
|
|
@@ -58,7 +58,7 @@ export const apiImplementation = ({
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export const anyLocalEvalCall = [
|
|
61
|
-
'http://example.com/api/feature_flag/local_evaluation?token=TEST_API_KEY',
|
|
61
|
+
'http://example.com/api/feature_flag/local_evaluation?token=TEST_API_KEY&send_cohorts',
|
|
62
62
|
expect.any(Object),
|
|
63
63
|
]
|
|
64
64
|
export const anyDecideCall = ['http://example.com/decide/?v=3', expect.any(Object)]
|
|
@@ -1179,6 +1179,177 @@ describe('local evaluation', () => {
|
|
|
1179
1179
|
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1180
1180
|
})
|
|
1181
1181
|
|
|
1182
|
+
it('computes complex cohorts locally', async () => {
|
|
1183
|
+
const flags = {
|
|
1184
|
+
flags: [
|
|
1185
|
+
{
|
|
1186
|
+
id: 1,
|
|
1187
|
+
name: 'Beta Feature',
|
|
1188
|
+
key: 'beta-feature',
|
|
1189
|
+
is_simple_flag: false,
|
|
1190
|
+
active: true,
|
|
1191
|
+
rollout_percentage: 100,
|
|
1192
|
+
filters: {
|
|
1193
|
+
groups: [
|
|
1194
|
+
{
|
|
1195
|
+
properties: [
|
|
1196
|
+
{
|
|
1197
|
+
key: 'region',
|
|
1198
|
+
operator: 'exact',
|
|
1199
|
+
value: ['USA'],
|
|
1200
|
+
type: 'person',
|
|
1201
|
+
},
|
|
1202
|
+
{ key: 'id', value: 98, type: 'cohort' },
|
|
1203
|
+
],
|
|
1204
|
+
rollout_percentage: 100,
|
|
1205
|
+
},
|
|
1206
|
+
],
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
],
|
|
1210
|
+
cohorts: {
|
|
1211
|
+
'98': {
|
|
1212
|
+
type: 'OR',
|
|
1213
|
+
values: [
|
|
1214
|
+
{ key: 'id', value: 1, type: 'cohort' },
|
|
1215
|
+
{
|
|
1216
|
+
key: 'nation',
|
|
1217
|
+
operator: 'exact',
|
|
1218
|
+
value: ['UK'],
|
|
1219
|
+
type: 'person',
|
|
1220
|
+
},
|
|
1221
|
+
],
|
|
1222
|
+
},
|
|
1223
|
+
'1': {
|
|
1224
|
+
type: 'AND',
|
|
1225
|
+
values: [{ key: 'other', operator: 'exact', value: ['thing'], type: 'person' }],
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
}
|
|
1229
|
+
mockedFetch.mockImplementation(
|
|
1230
|
+
apiImplementation({
|
|
1231
|
+
localFlags: flags,
|
|
1232
|
+
decideFlags: {},
|
|
1233
|
+
})
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
posthog = new PostHog('TEST_API_KEY', {
|
|
1237
|
+
host: 'http://example.com',
|
|
1238
|
+
personalApiKey: 'TEST_PERSONAL_API_KEY',
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
expect(
|
|
1242
|
+
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', { personProperties: { region: 'UK' } })
|
|
1243
|
+
).toEqual(false)
|
|
1244
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1245
|
+
|
|
1246
|
+
// # even though 'other' property is not present, the cohort should still match since it's an OR condition
|
|
1247
|
+
expect(
|
|
1248
|
+
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
|
|
1249
|
+
personProperties: { region: 'USA', nation: 'UK' },
|
|
1250
|
+
})
|
|
1251
|
+
).toEqual(true)
|
|
1252
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1253
|
+
|
|
1254
|
+
// # even though 'other' property is not present, the cohort should still match since it's an OR condition
|
|
1255
|
+
expect(
|
|
1256
|
+
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
|
|
1257
|
+
personProperties: { region: 'USA', other: 'thing' },
|
|
1258
|
+
})
|
|
1259
|
+
).toEqual(true)
|
|
1260
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
it('computes complex cohorts with negation locally', async () => {
|
|
1264
|
+
const flags = {
|
|
1265
|
+
flags: [
|
|
1266
|
+
{
|
|
1267
|
+
id: 1,
|
|
1268
|
+
name: 'Beta Feature',
|
|
1269
|
+
key: 'beta-feature',
|
|
1270
|
+
is_simple_flag: false,
|
|
1271
|
+
active: true,
|
|
1272
|
+
rollout_percentage: 100,
|
|
1273
|
+
filters: {
|
|
1274
|
+
groups: [
|
|
1275
|
+
{
|
|
1276
|
+
properties: [
|
|
1277
|
+
{
|
|
1278
|
+
key: 'region',
|
|
1279
|
+
operator: 'exact',
|
|
1280
|
+
value: ['USA'],
|
|
1281
|
+
type: 'person',
|
|
1282
|
+
},
|
|
1283
|
+
{ key: 'id', value: 98, type: 'cohort' },
|
|
1284
|
+
],
|
|
1285
|
+
rollout_percentage: 100,
|
|
1286
|
+
},
|
|
1287
|
+
],
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
],
|
|
1291
|
+
cohorts: {
|
|
1292
|
+
'98': {
|
|
1293
|
+
type: 'OR',
|
|
1294
|
+
values: [
|
|
1295
|
+
{ key: 'id', value: 1, type: 'cohort' },
|
|
1296
|
+
{
|
|
1297
|
+
key: 'nation',
|
|
1298
|
+
operator: 'exact',
|
|
1299
|
+
value: ['UK'],
|
|
1300
|
+
type: 'person',
|
|
1301
|
+
},
|
|
1302
|
+
],
|
|
1303
|
+
},
|
|
1304
|
+
'1': {
|
|
1305
|
+
type: 'AND',
|
|
1306
|
+
values: [{ key: 'other', operator: 'exact', value: ['thing'], type: 'person', negation: true }],
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
1309
|
+
}
|
|
1310
|
+
mockedFetch.mockImplementation(
|
|
1311
|
+
apiImplementation({
|
|
1312
|
+
localFlags: flags,
|
|
1313
|
+
decideFlags: {},
|
|
1314
|
+
})
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
posthog = new PostHog('TEST_API_KEY', {
|
|
1318
|
+
host: 'http://example.com',
|
|
1319
|
+
personalApiKey: 'TEST_PERSONAL_API_KEY',
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
expect(
|
|
1323
|
+
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', { personProperties: { region: 'UK' } })
|
|
1324
|
+
).toEqual(false)
|
|
1325
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1326
|
+
|
|
1327
|
+
// # even though 'other' property is not present, the cohort should still match since it's an OR condition
|
|
1328
|
+
expect(
|
|
1329
|
+
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
|
|
1330
|
+
personProperties: { region: 'USA', nation: 'UK' },
|
|
1331
|
+
})
|
|
1332
|
+
).toEqual(true)
|
|
1333
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1334
|
+
|
|
1335
|
+
// # since 'other' is negated, we return False. Since 'nation' is not present, we can't tell whether the flag should be true or false, so go to decide
|
|
1336
|
+
expect(
|
|
1337
|
+
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
|
|
1338
|
+
personProperties: { region: 'USA', other: 'thing' },
|
|
1339
|
+
})
|
|
1340
|
+
).toEqual(false)
|
|
1341
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)
|
|
1342
|
+
|
|
1343
|
+
mockedFetch.mockClear()
|
|
1344
|
+
|
|
1345
|
+
expect(
|
|
1346
|
+
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
|
|
1347
|
+
personProperties: { region: 'USA', other: 'thing2' },
|
|
1348
|
+
})
|
|
1349
|
+
).toEqual(true)
|
|
1350
|
+
expect(mockedFetch).not.toHaveBeenCalledWith(...anyDecideCall)
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1182
1353
|
it('gets feature flag with variant overrides', async () => {
|
|
1183
1354
|
const flags = {
|
|
1184
1355
|
flags: [
|