posthog-node 4.1.0 → 4.2.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 +8 -0
- package/lib/index.cjs.js +25 -3
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.esm.js +25 -3
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +1 -0
- package/lib/posthog-core/src/types.d.ts +2 -0
- package/lib/posthog-node/src/feature-flags.d.ts +1 -1
- package/package.json +1 -1
- package/src/feature-flags.ts +18 -3
- package/test/feature-flags.spec.ts +141 -12
|
@@ -40,6 +40,8 @@ export declare type PostHogCoreOptions = {
|
|
|
40
40
|
/** Whether to post events to PostHog in JSON or compressed format. Defaults to 'form' */
|
|
41
41
|
captureMode?: 'json' | 'form';
|
|
42
42
|
disableGeoip?: boolean;
|
|
43
|
+
/** Special flag to indicate ingested data is for a historical migration. */
|
|
44
|
+
historicalMigration?: boolean;
|
|
43
45
|
};
|
|
44
46
|
export declare enum PostHogPersistedProperty {
|
|
45
47
|
AnonymousId = "anonymous_id",
|
|
@@ -60,6 +60,6 @@ declare class FeatureFlagsPoller {
|
|
|
60
60
|
_requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse>;
|
|
61
61
|
stopPoller(): void;
|
|
62
62
|
}
|
|
63
|
-
declare function matchProperty(property: FeatureFlagCondition['properties'][number], propertyValues: Record<string, any
|
|
63
|
+
declare function matchProperty(property: FeatureFlagCondition['properties'][number], propertyValues: Record<string, any>, warnFunction?: (msg: string) => void): boolean;
|
|
64
64
|
declare function relativeDateParseForFeatureFlagMatching(value: string): Date | null;
|
|
65
65
|
export { FeatureFlagsPoller, matchProperty, relativeDateParseForFeatureFlagMatching, InconclusiveMatchError, ClientError, };
|
package/package.json
CHANGED
package/src/feature-flags.ts
CHANGED
|
@@ -7,6 +7,7 @@ import fetch from './fetch'
|
|
|
7
7
|
// eslint-disable-next-line
|
|
8
8
|
const LONG_SCALE = 0xfffffffffffffff
|
|
9
9
|
|
|
10
|
+
const NULL_VALUES_ALLOWED_OPERATORS = ['is_not']
|
|
10
11
|
class ClientError extends Error {
|
|
11
12
|
constructor(message: string) {
|
|
12
13
|
super()
|
|
@@ -305,7 +306,11 @@ class FeatureFlagsPoller {
|
|
|
305
306
|
properties: Record<string, string>
|
|
306
307
|
): boolean {
|
|
307
308
|
const rolloutPercentage = condition.rollout_percentage
|
|
308
|
-
|
|
309
|
+
const warnFunction = (msg: string): void => {
|
|
310
|
+
if (this.debugMode) {
|
|
311
|
+
console.warn(msg)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
309
314
|
if ((condition.properties || []).length > 0) {
|
|
310
315
|
for (const prop of condition.properties) {
|
|
311
316
|
const propertyType = prop.type
|
|
@@ -314,7 +319,7 @@ class FeatureFlagsPoller {
|
|
|
314
319
|
if (propertyType === 'cohort') {
|
|
315
320
|
matches = matchCohort(prop, properties, this.cohorts, this.debugMode)
|
|
316
321
|
} else {
|
|
317
|
-
matches = matchProperty(prop, properties)
|
|
322
|
+
matches = matchProperty(prop, properties, warnFunction)
|
|
318
323
|
}
|
|
319
324
|
|
|
320
325
|
if (!matches) {
|
|
@@ -461,7 +466,8 @@ function _hash(key: string, distinctId: string, salt: string = ''): number {
|
|
|
461
466
|
|
|
462
467
|
function matchProperty(
|
|
463
468
|
property: FeatureFlagCondition['properties'][number],
|
|
464
|
-
propertyValues: Record<string, any
|
|
469
|
+
propertyValues: Record<string, any>,
|
|
470
|
+
warnFunction?: (msg: string) => void
|
|
465
471
|
): boolean {
|
|
466
472
|
const key = property.key
|
|
467
473
|
const value = property.value
|
|
@@ -474,6 +480,15 @@ function matchProperty(
|
|
|
474
480
|
}
|
|
475
481
|
|
|
476
482
|
const overrideValue = propertyValues[key]
|
|
483
|
+
if (overrideValue == null && !NULL_VALUES_ALLOWED_OPERATORS.includes(operator)) {
|
|
484
|
+
// if the value is null, just fail the feature flag comparison
|
|
485
|
+
// this isn't an InconclusiveMatchError because the property value was provided.
|
|
486
|
+
if (warnFunction) {
|
|
487
|
+
warnFunction(`Property ${key} cannot have a value of null/undefined with the ${operator} operator`)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
477
492
|
|
|
478
493
|
function computeExactMatch(value: any, overrideValue: any): boolean {
|
|
479
494
|
if (Array.isArray(value)) {
|
|
@@ -24,6 +24,74 @@ describe('local evaluation', () => {
|
|
|
24
24
|
await posthog.shutdown()
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
it('evaluates person properties with undefined property values', async () => {
|
|
28
|
+
const flags = {
|
|
29
|
+
flags: [
|
|
30
|
+
{
|
|
31
|
+
id: 1,
|
|
32
|
+
name: 'Beta Feature',
|
|
33
|
+
key: 'person-flag',
|
|
34
|
+
is_simple_flag: true,
|
|
35
|
+
active: true,
|
|
36
|
+
filters: {
|
|
37
|
+
groups: [
|
|
38
|
+
{
|
|
39
|
+
variant: null,
|
|
40
|
+
properties: [
|
|
41
|
+
{
|
|
42
|
+
key: 'latestBuildVersion',
|
|
43
|
+
type: 'person',
|
|
44
|
+
value: '.+',
|
|
45
|
+
operator: 'regex',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'latestBuildVersionMajor',
|
|
49
|
+
type: 'person',
|
|
50
|
+
value: '23',
|
|
51
|
+
operator: 'gt',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'latestBuildVersionMinor',
|
|
55
|
+
type: 'person',
|
|
56
|
+
value: '31',
|
|
57
|
+
operator: 'gt',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'latestBuildVersionPatch',
|
|
61
|
+
type: 'person',
|
|
62
|
+
value: '0',
|
|
63
|
+
operator: 'gt',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
rollout_percentage: 100,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
|
|
74
|
+
|
|
75
|
+
posthog = new PostHog('TEST_API_KEY', {
|
|
76
|
+
host: 'http://example.com',
|
|
77
|
+
personalApiKey: 'TEST_PERSONAL_API_KEY',
|
|
78
|
+
...posthogImmediateResolveOptions,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
await posthog.getFeatureFlag('person-flag', 'some-distinct-id', {
|
|
83
|
+
personProperties: {
|
|
84
|
+
latestBuildVersion: undefined,
|
|
85
|
+
latestBuildVersionMajor: undefined,
|
|
86
|
+
latestBuildVersionMinor: undefined,
|
|
87
|
+
latestBuildVersionPatch: undefined,
|
|
88
|
+
} as unknown as Record<string, string>,
|
|
89
|
+
})
|
|
90
|
+
).toEqual(false)
|
|
91
|
+
|
|
92
|
+
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
|
|
93
|
+
})
|
|
94
|
+
|
|
27
95
|
it('evaluates person properties', async () => {
|
|
28
96
|
const flags = {
|
|
29
97
|
flags: [
|
|
@@ -1824,7 +1892,7 @@ describe('match properties', () => {
|
|
|
1824
1892
|
expect(matchProperty(property_a, { key: 'value' })).toBe(true)
|
|
1825
1893
|
expect(matchProperty(property_a, { key: 'value2' })).toBe(true)
|
|
1826
1894
|
expect(matchProperty(property_a, { key: '' })).toBe(true)
|
|
1827
|
-
expect(matchProperty(property_a, { key: undefined })).toBe(
|
|
1895
|
+
expect(matchProperty(property_a, { key: undefined })).toBe(false)
|
|
1828
1896
|
|
|
1829
1897
|
expect(() => matchProperty(property_a, { key2: 'value' })).toThrow(InconclusiveMatchError)
|
|
1830
1898
|
expect(() => matchProperty(property_a, {})).toThrow(InconclusiveMatchError)
|
|
@@ -2065,48 +2133,109 @@ describe('match properties', () => {
|
|
|
2065
2133
|
expect(matchProperty(property_a, { key: 'nul' })).toBe(true)
|
|
2066
2134
|
|
|
2067
2135
|
const property_b = { key: 'key', value: 'null', operator: 'is_set' }
|
|
2068
|
-
expect(matchProperty(property_b, { key: null })).toBe(
|
|
2069
|
-
expect(matchProperty(property_b, { key: undefined })).toBe(
|
|
2136
|
+
expect(matchProperty(property_b, { key: null })).toBe(false)
|
|
2137
|
+
expect(matchProperty(property_b, { key: undefined })).toBe(false)
|
|
2070
2138
|
expect(matchProperty(property_b, { key: 'null' })).toBe(true)
|
|
2071
2139
|
|
|
2072
2140
|
const property_c = { key: 'key', value: 'undefined', operator: 'icontains' }
|
|
2073
2141
|
expect(matchProperty(property_c, { key: null })).toBe(false)
|
|
2074
|
-
expect(matchProperty(property_c, { key: undefined })).toBe(
|
|
2142
|
+
expect(matchProperty(property_c, { key: undefined })).toBe(false)
|
|
2075
2143
|
expect(matchProperty(property_c, { key: 'lol' })).toBe(false)
|
|
2076
2144
|
|
|
2077
2145
|
const property_d = { key: 'key', value: 'undefined', operator: 'regex' }
|
|
2078
2146
|
expect(matchProperty(property_d, { key: null })).toBe(false)
|
|
2079
|
-
expect(matchProperty(property_d, { key: undefined })).toBe(
|
|
2147
|
+
expect(matchProperty(property_d, { key: undefined })).toBe(false)
|
|
2148
|
+
|
|
2149
|
+
const property_e = { key: 'key', value: 1, operator: 'gt' }
|
|
2150
|
+
expect(matchProperty(property_e, { key: null })).toBe(false)
|
|
2151
|
+
expect(matchProperty(property_e, { key: undefined })).toBe(false)
|
|
2152
|
+
|
|
2153
|
+
const property_f = { key: 'key', value: 1, operator: 'lt' }
|
|
2154
|
+
expect(matchProperty(property_f, { key: null })).toBe(false)
|
|
2155
|
+
expect(matchProperty(property_f, { key: undefined })).toBe(false)
|
|
2156
|
+
|
|
2157
|
+
const property_g = { key: 'key', value: 'xyz', operator: 'gte' }
|
|
2158
|
+
expect(matchProperty(property_g, { key: null })).toBe(false)
|
|
2159
|
+
expect(matchProperty(property_g, { key: undefined })).toBe(false)
|
|
2160
|
+
|
|
2161
|
+
const property_h = { key: 'key', value: 'Oo', operator: 'lte' }
|
|
2162
|
+
expect(matchProperty(property_h, { key: null })).toBe(false)
|
|
2163
|
+
expect(matchProperty(property_h, { key: undefined })).toBe(false)
|
|
2164
|
+
|
|
2165
|
+
const property_h_lower = { key: 'key', value: 'oo', operator: 'lte' }
|
|
2166
|
+
expect(matchProperty(property_h_lower, { key: null })).toBe(false)
|
|
2167
|
+
expect(matchProperty(property_h_lower, { key: undefined })).toBe(false)
|
|
2168
|
+
|
|
2169
|
+
const property_i = { key: 'key', value: '2022-05-01', operator: 'is_date_before' }
|
|
2170
|
+
|
|
2171
|
+
expect(matchProperty(property_i, { key: null })).toBe(false)
|
|
2172
|
+
expect(matchProperty(property_i, { key: undefined })).toBe(false)
|
|
2173
|
+
|
|
2174
|
+
const property_j = { key: 'key', value: '2022-05-01', operator: 'is_date_after' }
|
|
2175
|
+
expect(matchProperty(property_j, { key: null })).toBe(false)
|
|
2176
|
+
|
|
2177
|
+
const property_k = { key: 'key', value: '2022-05-01', operator: 'is_date_before' }
|
|
2178
|
+
expect(matchProperty(property_k, { key: null })).toBe(false)
|
|
2179
|
+
})
|
|
2180
|
+
|
|
2181
|
+
it('null or undefined override value', () => {
|
|
2182
|
+
const property_a = { key: 'key', value: 'ab', operator: 'is_not' }
|
|
2183
|
+
expect(matchProperty(property_a, { key: null })).toBe(true)
|
|
2184
|
+
expect(matchProperty(property_a, { key: undefined })).toBe(true)
|
|
2185
|
+
expect(matchProperty(property_a, { key: 'null' })).toBe(true)
|
|
2186
|
+
expect(matchProperty(property_a, { key: 'nul' })).toBe(true)
|
|
2187
|
+
|
|
2188
|
+
const property_b = { key: 'key', value: 'null', operator: 'is_set' }
|
|
2189
|
+
expect(matchProperty(property_b, { key: null })).toBe(false)
|
|
2190
|
+
expect(matchProperty(property_b, { key: undefined })).toBe(false)
|
|
2191
|
+
expect(matchProperty(property_b, { key: 'null' })).toBe(true)
|
|
2192
|
+
|
|
2193
|
+
const property_c = { key: 'key', value: 'app.posthog.com', operator: 'icontains' }
|
|
2194
|
+
expect(matchProperty(property_c, { key: null })).toBe(false)
|
|
2195
|
+
expect(matchProperty(property_c, { key: undefined })).toBe(false)
|
|
2196
|
+
expect(matchProperty(property_c, { key: 'lol' })).toBe(false)
|
|
2197
|
+
expect(matchProperty(property_c, { key: 'https://app.posthog.com' })).toBe(true)
|
|
2198
|
+
|
|
2199
|
+
const property_d = { key: 'key', value: '.+', operator: 'regex' }
|
|
2200
|
+
expect(matchProperty(property_d, { key: null })).toBe(false)
|
|
2201
|
+
expect(matchProperty(property_d, { key: undefined })).toBe(false)
|
|
2202
|
+
expect(matchProperty(property_d, { key: 'i_am_a_value' })).toBe(true)
|
|
2080
2203
|
|
|
2081
2204
|
const property_e = { key: 'key', value: 1, operator: 'gt' }
|
|
2082
|
-
expect(matchProperty(property_e, { key: null })).toBe(
|
|
2083
|
-
expect(matchProperty(property_e, { key: undefined })).toBe(
|
|
2205
|
+
expect(matchProperty(property_e, { key: null })).toBe(false)
|
|
2206
|
+
expect(matchProperty(property_e, { key: undefined })).toBe(false)
|
|
2207
|
+
expect(matchProperty(property_e, { key: 1 })).toBe(false)
|
|
2208
|
+
expect(matchProperty(property_e, { key: 2 })).toBe(true)
|
|
2084
2209
|
|
|
2085
2210
|
const property_f = { key: 'key', value: 1, operator: 'lt' }
|
|
2086
2211
|
expect(matchProperty(property_f, { key: null })).toBe(false)
|
|
2087
2212
|
expect(matchProperty(property_f, { key: undefined })).toBe(false)
|
|
2213
|
+
expect(matchProperty(property_f, { key: 0 })).toBe(true)
|
|
2088
2214
|
|
|
2089
2215
|
const property_g = { key: 'key', value: 'xyz', operator: 'gte' }
|
|
2090
2216
|
expect(matchProperty(property_g, { key: null })).toBe(false)
|
|
2091
2217
|
expect(matchProperty(property_g, { key: undefined })).toBe(false)
|
|
2218
|
+
expect(matchProperty(property_g, { key: 'xyz' })).toBe(true)
|
|
2092
2219
|
|
|
2093
2220
|
const property_h = { key: 'key', value: 'Oo', operator: 'lte' }
|
|
2094
2221
|
expect(matchProperty(property_h, { key: null })).toBe(false)
|
|
2095
2222
|
expect(matchProperty(property_h, { key: undefined })).toBe(false)
|
|
2223
|
+
expect(matchProperty(property_h, { key: 'Oo' })).toBe(true)
|
|
2096
2224
|
|
|
2097
2225
|
const property_h_lower = { key: 'key', value: 'oo', operator: 'lte' }
|
|
2098
|
-
expect(matchProperty(property_h_lower, { key: null })).toBe(
|
|
2226
|
+
expect(matchProperty(property_h_lower, { key: null })).toBe(false)
|
|
2099
2227
|
expect(matchProperty(property_h_lower, { key: undefined })).toBe(false)
|
|
2228
|
+
expect(matchProperty(property_h_lower, { key: 'oo' })).toBe(true)
|
|
2100
2229
|
|
|
2101
2230
|
const property_i = { key: 'key', value: '2022-05-01', operator: 'is_date_before' }
|
|
2102
|
-
expect(
|
|
2103
|
-
expect(
|
|
2231
|
+
expect(matchProperty(property_i, { key: null })).toBe(false)
|
|
2232
|
+
expect(matchProperty(property_i, { key: undefined })).toBe(false)
|
|
2104
2233
|
|
|
2105
2234
|
const property_j = { key: 'key', value: '2022-05-01', operator: 'is_date_after' }
|
|
2106
|
-
expect(
|
|
2235
|
+
expect(matchProperty(property_j, { key: null })).toBe(false)
|
|
2107
2236
|
|
|
2108
2237
|
const property_k = { key: 'key', value: '2022-05-01', operator: 'is_date_before' }
|
|
2109
|
-
expect(
|
|
2238
|
+
expect(matchProperty(property_k, { key: null })).toBe(false)
|
|
2110
2239
|
})
|
|
2111
2240
|
|
|
2112
2241
|
it('with invalid operator', () => {
|