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.
@@ -16,6 +16,7 @@ export declare abstract class PostHogCoreStateless {
16
16
  private captureMode;
17
17
  private removeDebugCallback?;
18
18
  private disableGeoip;
19
+ private historicalMigration;
19
20
  disabled: boolean;
20
21
  private defaultOptIn;
21
22
  private pendingPromises;
@@ -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>): boolean;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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(true)
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(true)
2069
- expect(matchProperty(property_b, { key: undefined })).toBe(true)
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(true)
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(true)
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(true)
2083
- expect(matchProperty(property_e, { key: undefined })).toBe(true)
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(true)
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(() => matchProperty(property_i, { key: null })).toThrow(InconclusiveMatchError)
2103
- expect(() => matchProperty(property_i, { key: undefined })).toThrow(InconclusiveMatchError)
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(() => matchProperty(property_j, { key: null })).toThrow(InconclusiveMatchError)
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(() => matchProperty(property_k, { key: null })).toThrow(InconclusiveMatchError)
2238
+ expect(matchProperty(property_k, { key: null })).toBe(false)
2110
2239
  })
2111
2240
 
2112
2241
  it('with invalid operator', () => {