posthog-node 3.4.0 → 3.5.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.
@@ -12,9 +12,9 @@ export declare abstract class PostHogCoreStateless {
12
12
  private captureMode;
13
13
  private removeDebugCallback?;
14
14
  private debugMode;
15
- private pendingPromises;
16
15
  private disableGeoip;
17
16
  private _optoutOverride;
17
+ private pendingPromises;
18
18
  protected _events: SimpleEventEmitter;
19
19
  protected _flushTimer?: any;
20
20
  protected _retryOptions: RetriableOptions;
@@ -32,6 +32,7 @@ export declare abstract class PostHogCoreStateless {
32
32
  on(event: string, cb: (...args: any[]) => void): () => void;
33
33
  debug(enabled?: boolean): void;
34
34
  private buildPayload;
35
+ protected addPendingPromise(promise: Promise<any>): void;
35
36
  /***
36
37
  *** TRACKING
37
38
  ***/
@@ -73,4 +73,5 @@ export declare class PostHog extends PostHogCoreStateless implements PostHogNode
73
73
  reloadFeatureFlags(): Promise<void>;
74
74
  shutdown(): void;
75
75
  shutdownAsync(): Promise<void>;
76
+ private addLocalPersonAndGroupProperties;
76
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -121,7 +121,7 @@ class FeatureFlagsPoller {
121
121
  console.debug(`InconclusiveMatchError when computing flag locally: ${key}: ${e}`)
122
122
  }
123
123
  } else if (e instanceof Error) {
124
- console.error(`Error computing flag locally: ${key}: ${e}`)
124
+ this.onError?.(new Error(`Error computing flag locally: ${key}: ${e}`))
125
125
  }
126
126
  }
127
127
  }
@@ -211,14 +211,18 @@ class FeatureFlagsPoller {
211
211
  const groupName = this.groupTypeMapping[String(aggregation_group_type_index)]
212
212
 
213
213
  if (!groupName) {
214
- console.warn(
215
- `[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`
216
- )
214
+ if (this.debugMode) {
215
+ console.warn(
216
+ `[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`
217
+ )
218
+ }
217
219
  throw new InconclusiveMatchError('Flag has unknown group type index')
218
220
  }
219
221
 
220
222
  if (!(groupName in groups)) {
221
- console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`)
223
+ if (this.debugMode) {
224
+ console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`)
225
+ }
222
226
  return false
223
227
  }
224
228
 
@@ -383,7 +387,7 @@ class FeatureFlagsPoller {
383
387
 
384
388
  const responseJson = await res.json()
385
389
  if (!('flags' in responseJson)) {
386
- console.error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`)
390
+ this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`))
387
391
  }
388
392
 
389
393
  this.featureFlags = responseJson.flags || []
@@ -105,26 +105,54 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
105
105
  super.captureStateless(distinctId, event, props, { timestamp, disableGeoip })
106
106
  }
107
107
 
108
- if (sendFeatureFlags) {
109
- super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip).then((flags) => {
110
- const featureVariantProperties: Record<string, string | boolean> = {}
108
+ // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
109
+ const capturePromise = Promise.resolve()
110
+ .then(async () => {
111
+ if (sendFeatureFlags) {
112
+ // If we are sending feature flags, we need to make sure we have the latest flags
113
+ return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
114
+ }
115
+
116
+ if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
117
+ // Otherwise we may as well check for the flags locally and include them if there
118
+ const groupsWithStringValues: Record<string, string> = {}
119
+ for (const [key, value] of Object.entries(groups || {})) {
120
+ groupsWithStringValues[key] = String(value)
121
+ }
122
+
123
+ return await this.getAllFlags(distinctId, {
124
+ groups: groupsWithStringValues,
125
+ disableGeoip,
126
+ onlyEvaluateLocally: true,
127
+ })
128
+ }
129
+ return {}
130
+ })
131
+ .then((flags) => {
132
+ // Derive the relevant flag properties to add
133
+ const additionalProperties: Record<string, any> = {}
111
134
  if (flags) {
112
135
  for (const [feature, variant] of Object.entries(flags)) {
113
- if (variant !== false) {
114
- featureVariantProperties[`$feature/${feature}`] = variant
115
- }
136
+ additionalProperties[`$feature/${feature}`] = variant
116
137
  }
117
138
  }
118
139
  const activeFlags = Object.keys(flags || {}).filter((flag) => flags?.[flag] !== false)
119
- const flagProperties = {
120
- $active_feature_flags: activeFlags || undefined,
121
- ...featureVariantProperties,
140
+ if (activeFlags.length > 0) {
141
+ additionalProperties['$active_feature_flags'] = activeFlags
122
142
  }
123
- _capture({ ...properties, $groups: groups, ...flagProperties })
143
+
144
+ return additionalProperties
124
145
  })
125
- } else {
126
- _capture({ ...properties, $groups: groups })
127
- }
146
+ .catch(() => {
147
+ // Something went wrong getting the flag info - we should capture the event anyways
148
+ return {}
149
+ })
150
+ .then((additionalProperties) => {
151
+ // No matter what - capture the event
152
+ _capture({ ...additionalProperties, ...properties, $groups: groups })
153
+ })
154
+
155
+ this.addPendingPromise(capturePromise)
128
156
  }
129
157
 
130
158
  identify({ distinctId, properties, disableGeoip }: IdentifyMessageV1): void {
@@ -156,8 +184,18 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
156
184
  disableGeoip?: boolean
157
185
  }
158
186
  ): Promise<string | boolean | undefined> {
159
- const { groups, personProperties, groupProperties, disableGeoip } = options || {}
160
- let { onlyEvaluateLocally, sendFeatureFlagEvents } = options || {}
187
+ const { groups, disableGeoip } = options || {}
188
+ let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {}
189
+
190
+ const adjustedProperties = this.addLocalPersonAndGroupProperties(
191
+ distinctId,
192
+ groups,
193
+ personProperties,
194
+ groupProperties
195
+ )
196
+
197
+ personProperties = adjustedProperties.allPersonProperties
198
+ groupProperties = adjustedProperties.allGroupProperties
161
199
 
162
200
  // set defaults
163
201
  if (onlyEvaluateLocally == undefined) {
@@ -232,8 +270,19 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
232
270
  disableGeoip?: boolean
233
271
  }
234
272
  ): Promise<JsonType | undefined> {
235
- const { groups, personProperties, groupProperties, disableGeoip } = options || {}
236
- let { onlyEvaluateLocally, sendFeatureFlagEvents } = options || {}
273
+ const { groups, disableGeoip } = options || {}
274
+ let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {}
275
+
276
+ const adjustedProperties = this.addLocalPersonAndGroupProperties(
277
+ distinctId,
278
+ groups,
279
+ personProperties,
280
+ groupProperties
281
+ )
282
+
283
+ personProperties = adjustedProperties.allPersonProperties
284
+ groupProperties = adjustedProperties.allGroupProperties
285
+
237
286
  let response = undefined
238
287
 
239
288
  // Try to get match value locally if not provided
@@ -324,8 +373,18 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
324
373
  disableGeoip?: boolean
325
374
  }
326
375
  ): Promise<PosthogFlagsAndPayloadsResponse> {
327
- const { groups, personProperties, groupProperties, disableGeoip } = options || {}
328
- let { onlyEvaluateLocally } = options || {}
376
+ const { groups, disableGeoip } = options || {}
377
+ let { onlyEvaluateLocally, personProperties, groupProperties } = options || {}
378
+
379
+ const adjustedProperties = this.addLocalPersonAndGroupProperties(
380
+ distinctId,
381
+ groups,
382
+ personProperties,
383
+ groupProperties
384
+ )
385
+
386
+ personProperties = adjustedProperties.allPersonProperties
387
+ groupProperties = adjustedProperties.allGroupProperties
329
388
 
330
389
  // set defaults
331
390
  if (onlyEvaluateLocally == undefined) {
@@ -385,4 +444,25 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
385
444
  this.featureFlagsPoller?.stopPoller()
386
445
  return super.shutdownAsync()
387
446
  }
447
+
448
+ private addLocalPersonAndGroupProperties(
449
+ distinctId: string,
450
+ groups?: Record<string, string>,
451
+ personProperties?: Record<string, string>,
452
+ groupProperties?: Record<string, Record<string, string>>
453
+ ): { allPersonProperties: Record<string, string>; allGroupProperties: Record<string, Record<string, string>> } {
454
+ const allPersonProperties = { $current_distinct_id: distinctId, ...(personProperties || {}) }
455
+
456
+ const allGroupProperties: Record<string, Record<string, string>> = {}
457
+ if (groups) {
458
+ for (const groupName of Object.keys(groups)) {
459
+ allGroupProperties[groupName] = {
460
+ $group_key: groups[groupName],
461
+ ...(groupProperties?.[groupName] || {}),
462
+ }
463
+ }
464
+ }
465
+
466
+ return { allPersonProperties, allGroupProperties }
467
+ }
388
468
  }
@@ -3,6 +3,7 @@ import { PostHog as PostHog } from '../../src/posthog-node'
3
3
  import { PostHogSentryIntegration } from '../../src/extensions/sentry-integration'
4
4
  jest.mock('../../src/fetch')
5
5
  import fetch from '../../src/fetch'
6
+ import { waitForPromises } from 'posthog-core/test/test-utils/test-utils'
6
7
 
7
8
  jest.mock('../../package.json', () => ({ version: '1.2.3' }))
8
9
 
@@ -108,6 +109,7 @@ describe('PostHogSentryIntegration', () => {
108
109
 
109
110
  processorFunction(createMockSentryException())
110
111
 
112
+ await waitForPromises()
111
113
  jest.runOnlyPendingTimers()
112
114
  const batchEvents = getLastBatchEvents()
113
115
 
@@ -50,6 +50,17 @@ export const apiImplementation = ({
50
50
  }) as any
51
51
  }
52
52
 
53
+ if ((url as any).includes('batch/')) {
54
+ return Promise.resolve({
55
+ status: 200,
56
+ text: () => Promise.resolve('ok'),
57
+ json: () =>
58
+ Promise.resolve({
59
+ status: 'ok',
60
+ }),
61
+ }) as any
62
+ }
63
+
53
64
  return Promise.resolve({
54
65
  status: 400,
55
66
  text: () => Promise.resolve('ok'),
@@ -342,7 +353,11 @@ describe('local evaluation', () => {
342
353
  token: 'TEST_API_KEY',
343
354
  distinct_id: 'some-distinct-id_outside_rollout?',
344
355
  groups: {},
345
- person_properties: { region: 'USA', email: 'a@b.com' },
356
+ person_properties: {
357
+ $current_distinct_id: 'some-distinct-id_outside_rollout?',
358
+ region: 'USA',
359
+ email: 'a@b.com',
360
+ },
346
361
  group_properties: {},
347
362
  geoip_disable: true,
348
363
  }),
@@ -361,7 +376,7 @@ describe('local evaluation', () => {
361
376
  token: 'TEST_API_KEY',
362
377
  distinct_id: 'some-distinct-id',
363
378
  groups: {},
364
- person_properties: { doesnt_matter: '1' },
379
+ person_properties: { $current_distinct_id: 'some-distinct-id', doesnt_matter: '1' },
365
380
  group_properties: {},
366
381
  geoip_disable: true,
367
382
  }),