posthog-node 2.2.2 → 2.3.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.
@@ -1,4 +1,4 @@
1
- import { PostHogFetchOptions, PostHogFetchResponse, PostHogAutocaptureElement, PostHogDecideResponse, PosthogCoreOptions, PostHogEventProperties, PostHogPersistedProperty } from './types';
1
+ import { PostHogFetchOptions, PostHogFetchResponse, PostHogAutocaptureElement, PostHogDecideResponse, PosthogCoreOptions, PostHogEventProperties, PostHogPersistedProperty, JsonType } from './types';
2
2
  import { RetriableOptions } from './utils';
3
3
  export * as utils from './utils';
4
4
  import { LZString } from './lz-string';
@@ -79,8 +79,16 @@ export declare abstract class PostHogCore {
79
79
  private decideAsync;
80
80
  private _decideAsync;
81
81
  private setKnownFeatureFlags;
82
+ private setKnownFeatureFlagPayloads;
82
83
  getFeatureFlag(key: string): boolean | string | undefined;
84
+ getFeatureFlagPayload(key: string): JsonType | undefined;
85
+ getFeatureFlagPayloads(): PostHogDecideResponse['featureFlagPayloads'] | undefined;
86
+ _parsePayload(response: any): any;
83
87
  getFeatureFlags(): PostHogDecideResponse['featureFlags'] | undefined;
88
+ getFeatureFlagsAndPayloads(): {
89
+ flags: PostHogDecideResponse['featureFlags'] | undefined;
90
+ payloads: PostHogDecideResponse['featureFlagPayloads'] | undefined;
91
+ };
84
92
  isFeatureEnabled(key: string): boolean | undefined;
85
93
  reloadFeatureFlagsAsync(sendAnonDistinctId?: boolean): Promise<PostHogDecideResponse['featureFlags']>;
86
94
  onFeatureFlags(cb: (flags: PostHogDecideResponse['featureFlags']) => void): () => void;
@@ -10,6 +10,7 @@ export declare type PosthogCoreOptions = {
10
10
  distinctId?: string;
11
11
  isIdentifiedId?: boolean;
12
12
  featureFlags?: Record<string, boolean | string>;
13
+ featureFlagPayloads?: Record<string, JsonType>;
13
14
  };
14
15
  fetchRetryCount?: number;
15
16
  fetchRetryDelay?: number;
@@ -22,6 +23,7 @@ export declare enum PostHogPersistedProperty {
22
23
  DistinctId = "distinct_id",
23
24
  Props = "props",
24
25
  FeatureFlags = "feature_flags",
26
+ FeatureFlagPayloads = "feature_flag_payloads",
25
27
  OverrideFeatureFlags = "override_feature_flags",
26
28
  Queue = "queue",
27
29
  OptedOut = "opted_out",
@@ -75,5 +77,16 @@ export declare type PostHogDecideResponse = {
75
77
  featureFlags: {
76
78
  [key: string]: string | boolean;
77
79
  };
80
+ featureFlagPayloads: {
81
+ [key: string]: JsonType;
82
+ };
83
+ errorsWhileComputingFlags: boolean;
78
84
  sessionRecording: boolean;
79
85
  };
86
+ export declare type PosthogFlagsAndPayloadsResponse = {
87
+ featureFlags: PostHogDecideResponse['featureFlags'];
88
+ featureFlagPayloads: PostHogDecideResponse['featureFlagPayloads'];
89
+ };
90
+ export declare type JsonType = string | number | boolean | null | {
91
+ [key: string]: JsonType;
92
+ } | Array<JsonType>;
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import { FeatureFlagCondition, PostHogFeatureFlag } from './types';
3
- import { PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src';
3
+ import { JsonType, PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src';
4
4
  declare class ClientError extends Error {
5
5
  constructor(message: string);
6
6
  }
@@ -20,6 +20,7 @@ declare class FeatureFlagsPoller {
20
20
  personalApiKey: string;
21
21
  projectApiKey: string;
22
22
  featureFlags: Array<PostHogFeatureFlag>;
23
+ featureFlagsByKey: Record<string, PostHogFeatureFlag>;
23
24
  groupTypeMapping: Record<string, string>;
24
25
  loadedSuccessfullyOnce: boolean;
25
26
  timeout?: number;
@@ -28,8 +29,10 @@ declare class FeatureFlagsPoller {
28
29
  fetch: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
29
30
  constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host, ...options }: FeatureFlagsPollerOptions);
30
31
  getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<string | boolean | undefined>;
31
- getAllFlags(distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<{
32
+ computeFeatureFlagPayloadLocally(key: string, matchValue: string | boolean): Promise<JsonType | undefined>;
33
+ getAllFlagsAndPayloads(distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<{
32
34
  response: Record<string, string | boolean>;
35
+ payloads: Record<string, JsonType>;
33
36
  fallbackToDecide: boolean;
34
37
  }>;
35
38
  computeFlagLocally(flag: PostHogFeatureFlag, distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): string | boolean;
@@ -1,4 +1,4 @@
1
- import { PosthogCoreOptions, PostHogFetchOptions, PostHogFetchResponse } from '../../posthog-core/src';
1
+ import { JsonType, PosthogCoreOptions, PostHogFetchOptions, PostHogFetchResponse, PosthogFlagsAndPayloadsResponse } from '../../posthog-core/src';
2
2
  import { EventMessageV1, GroupIdentifyMessage, IdentifyMessageV1, PostHogNodeV1 } from './types';
3
3
  export declare type PostHogOptions = PosthogCoreOptions & {
4
4
  persistence?: 'memory';
@@ -30,6 +30,13 @@ export declare class PostHog implements PostHogNodeV1 {
30
30
  onlyEvaluateLocally?: boolean;
31
31
  sendFeatureFlagEvents?: boolean;
32
32
  }): Promise<string | boolean | undefined>;
33
+ getFeatureFlagPayload(key: string, distinctId: string, matchValue?: string | boolean, options?: {
34
+ groups?: Record<string, string>;
35
+ personProperties?: Record<string, string>;
36
+ groupProperties?: Record<string, Record<string, string>>;
37
+ onlyEvaluateLocally?: boolean;
38
+ sendFeatureFlagEvents?: boolean;
39
+ }): Promise<JsonType | undefined>;
33
40
  isFeatureEnabled(key: string, distinctId: string, options?: {
34
41
  groups?: Record<string, string>;
35
42
  personProperties?: Record<string, string>;
@@ -43,6 +50,12 @@ export declare class PostHog implements PostHogNodeV1 {
43
50
  groupProperties?: Record<string, Record<string, string>>;
44
51
  onlyEvaluateLocally?: boolean;
45
52
  }): Promise<Record<string, string | boolean>>;
53
+ getAllFlagsAndPayloads(distinctId: string, options?: {
54
+ groups?: Record<string, string>;
55
+ personProperties?: Record<string, string>;
56
+ groupProperties?: Record<string, Record<string, string>>;
57
+ onlyEvaluateLocally?: boolean;
58
+ }): Promise<PosthogFlagsAndPayloadsResponse>;
46
59
  groupIdentify({ groupType, groupKey, properties }: GroupIdentifyMessage): void;
47
60
  reloadFeatureFlags(): Promise<void>;
48
61
  flush(): void;
@@ -1,3 +1,4 @@
1
+ import { JsonType } from 'posthog-core/src';
1
2
  export interface IdentifyMessageV1 {
2
3
  distinctId: string;
3
4
  properties?: Record<string | number, any>;
@@ -35,6 +36,7 @@ export declare type PostHogFeatureFlag = {
35
36
  rollout_percentage: number;
36
37
  }[];
37
38
  };
39
+ payloads?: Record<string, JsonType>;
38
40
  };
39
41
  deleted: boolean;
40
42
  active: boolean;
@@ -125,6 +127,23 @@ export declare type PostHogNodeV1 = {
125
127
  onlyEvaluateLocally?: boolean;
126
128
  sendFeatureFlagEvents?: boolean;
127
129
  }): Promise<string | boolean | undefined>;
130
+ /**
131
+ * @description Retrieves payload associated with the specified flag and matched value that is passed in.
132
+ * (Expected to be used in conjuction with getFeatureFlag but allows for manual lookup).
133
+ * If matchValue isn't passed, getFeatureFlag is called implicitly.
134
+ * Will try to evaluate for payload locally first otherwise default to network call if allowed
135
+ *
136
+ * @param key the unique key of your feature flag
137
+ * @param distinctId the current unique id
138
+ * @param matchValue optional- the matched flag string or boolean
139
+ * @param options: dict with optional parameters below
140
+ * @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false.
141
+ *
142
+ * @returns payload of a json type object
143
+ */
144
+ getFeatureFlagPayload(key: string, distinctId: string, matchValue?: string | boolean, options?: {
145
+ onlyEvaluateLocally?: boolean;
146
+ }): Promise<JsonType | undefined>;
128
147
  /**
129
148
  * @description Sets a groups properties, which allows asking questions like "Who are the most active companies"
130
149
  * using my product in PostHog.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": "PostHog/posthog-node",
6
6
  "scripts": {
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'crypto'
2
2
  import { FeatureFlagCondition, PostHogFeatureFlag } from './types'
3
3
  import { version } from '../package.json'
4
- import { PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
4
+ import { JsonType, PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
5
5
  import { safeSetTimeout } from 'posthog-core/src/utils'
6
6
  import { fetch } from './fetch'
7
7
 
@@ -44,6 +44,7 @@ class FeatureFlagsPoller {
44
44
  personalApiKey: string
45
45
  projectApiKey: string
46
46
  featureFlags: Array<PostHogFeatureFlag>
47
+ featureFlagsByKey: Record<string, PostHogFeatureFlag>
47
48
  groupTypeMapping: Record<string, string>
48
49
  loadedSuccessfullyOnce: boolean
49
50
  timeout?: number
@@ -62,6 +63,7 @@ class FeatureFlagsPoller {
62
63
  this.pollingInterval = pollingInterval
63
64
  this.personalApiKey = personalApiKey
64
65
  this.featureFlags = []
66
+ this.featureFlagsByKey = {}
65
67
  this.groupTypeMapping = {}
66
68
  this.loadedSuccessfullyOnce = false
67
69
  this.timeout = timeout
@@ -100,10 +102,9 @@ class FeatureFlagsPoller {
100
102
  if (featureFlag !== undefined) {
101
103
  try {
102
104
  response = this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties)
103
- console.debug(`Successfully computed flag locally: ${key} -> ${response}`)
104
105
  } catch (e) {
105
106
  if (e instanceof InconclusiveMatchError) {
106
- console.debug(`Can't compute flag locally: ${key}: ${e}`)
107
+ console.error(`InconclusiveMatchError when computing flag locally: ${key}: ${e}`)
107
108
  } else if (e instanceof Error) {
108
109
  console.error(`Error computing flag locally: ${key}: ${e}`)
109
110
  }
@@ -113,20 +114,53 @@ class FeatureFlagsPoller {
113
114
  return response
114
115
  }
115
116
 
116
- async getAllFlags(
117
+ async computeFeatureFlagPayloadLocally(key: string, matchValue: string | boolean): Promise<JsonType | undefined> {
118
+ await this.loadFeatureFlags()
119
+
120
+ let response = undefined
121
+
122
+ if (!this.loadedSuccessfullyOnce) {
123
+ return undefined
124
+ }
125
+
126
+ if (typeof matchValue == 'boolean') {
127
+ response = this.featureFlagsByKey?.[key]?.filters?.payloads?.[matchValue.toString()]
128
+ } else if (typeof matchValue == 'string') {
129
+ response = this.featureFlagsByKey?.[key]?.filters?.payloads?.[matchValue]
130
+ }
131
+
132
+ // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
133
+ if (response === undefined) {
134
+ return null
135
+ }
136
+
137
+ return response
138
+ }
139
+
140
+ async getAllFlagsAndPayloads(
117
141
  distinctId: string,
118
142
  groups: Record<string, string> = {},
119
143
  personProperties: Record<string, string> = {},
120
144
  groupProperties: Record<string, Record<string, string>> = {}
121
- ): Promise<{ response: Record<string, string | boolean>; fallbackToDecide: boolean }> {
145
+ ): Promise<{
146
+ response: Record<string, string | boolean>
147
+ payloads: Record<string, JsonType>
148
+ fallbackToDecide: boolean
149
+ }> {
122
150
  await this.loadFeatureFlags()
123
151
 
124
152
  const response: Record<string, string | boolean> = {}
153
+ const payloads: Record<string, JsonType> = {}
125
154
  let fallbackToDecide = this.featureFlags.length == 0
126
155
 
127
- this.featureFlags.map((flag) => {
156
+ this.featureFlags.map(async (flag) => {
128
157
  try {
129
- response[flag.key] = this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties)
158
+ const matchValue = this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties)
159
+ response[flag.key] = matchValue
160
+ const matchPayload = await this.computeFeatureFlagPayloadLocally(flag.key, matchValue)
161
+ if (matchPayload) {
162
+ payloads[flag.key] = matchPayload
163
+ }
130
164
  } catch (e) {
131
165
  if (e instanceof InconclusiveMatchError) {
132
166
  // do nothing
@@ -137,7 +171,7 @@ class FeatureFlagsPoller {
137
171
  }
138
172
  })
139
173
 
140
- return { response, fallbackToDecide }
174
+ return { response, payloads, fallbackToDecide }
141
175
  }
142
176
 
143
177
  computeFlagLocally(
@@ -316,12 +350,23 @@ class FeatureFlagsPoller {
316
350
  `Your personalApiKey is invalid. Are you sure you're not using your Project API key? More information: https://posthog.com/docs/api/overview`
317
351
  )
318
352
  }
353
+
354
+ if (res && res.status !== 200) {
355
+ // something else went wrong, or the server is down.
356
+ // In this case, don't override existing flags
357
+ return
358
+ }
359
+
319
360
  const responseJson = await res.json()
320
361
  if (!('flags' in responseJson)) {
321
362
  console.error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`)
322
363
  }
323
364
 
324
365
  this.featureFlags = responseJson.flags || []
366
+ this.featureFlagsByKey = this.featureFlags.reduce(
367
+ (acc, curr) => ((acc[curr.key] = curr), acc),
368
+ <Record<string, PostHogFeatureFlag>>{}
369
+ )
325
370
  this.groupTypeMapping = responseJson.group_type_mapping || {}
326
371
  this.loadedSuccessfullyOnce = true
327
372
  } catch (err) {
@@ -1,10 +1,12 @@
1
1
  import { version } from '../package.json'
2
2
 
3
3
  import {
4
+ JsonType,
4
5
  PostHogCore,
5
6
  PosthogCoreOptions,
6
7
  PostHogFetchOptions,
7
8
  PostHogFetchResponse,
9
+ PosthogFlagsAndPayloadsResponse,
8
10
  PostHogPersistedProperty,
9
11
  } from '../../posthog-core/src'
10
12
  import { PostHogMemoryStorage } from '../../posthog-core/src/storage-memory'
@@ -84,7 +86,7 @@ export class PostHog implements PostHogNodeV1 {
84
86
  : THIRTY_SECONDS,
85
87
  personalApiKey: options.personalApiKey,
86
88
  projectApiKey: apiKey,
87
- timeout: options.requestTimeout ?? 10,
89
+ timeout: options.requestTimeout ?? 10000, // 10 seconds
88
90
  host: this._sharedClient.host,
89
91
  fetch: options.fetch,
90
92
  })
@@ -203,6 +205,73 @@ export class PostHog implements PostHogNodeV1 {
203
205
  return response
204
206
  }
205
207
 
208
+ async getFeatureFlagPayload(
209
+ key: string,
210
+ distinctId: string,
211
+ matchValue?: string | boolean,
212
+ options?: {
213
+ groups?: Record<string, string>
214
+ personProperties?: Record<string, string>
215
+ groupProperties?: Record<string, Record<string, string>>
216
+ onlyEvaluateLocally?: boolean
217
+ sendFeatureFlagEvents?: boolean
218
+ }
219
+ ): Promise<JsonType | undefined> {
220
+ const { groups, personProperties, groupProperties } = options || {}
221
+ let { onlyEvaluateLocally, sendFeatureFlagEvents } = options || {}
222
+ let response = undefined
223
+
224
+ // Try to get match value locally if not provided
225
+ if (!matchValue) {
226
+ matchValue = await this.getFeatureFlag(key, distinctId, {
227
+ ...options,
228
+ onlyEvaluateLocally: true,
229
+ })
230
+ }
231
+
232
+ if (matchValue) {
233
+ response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue)
234
+ }
235
+
236
+ // set defaults
237
+ if (onlyEvaluateLocally == undefined) {
238
+ onlyEvaluateLocally = false
239
+ }
240
+ if (sendFeatureFlagEvents == undefined) {
241
+ sendFeatureFlagEvents = true
242
+ }
243
+
244
+ // set defaults
245
+ if (onlyEvaluateLocally == undefined) {
246
+ onlyEvaluateLocally = false
247
+ }
248
+
249
+ const payloadWasLocallyEvaluated = response !== undefined
250
+
251
+ if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
252
+ this.reInit(distinctId)
253
+ if (groups != undefined) {
254
+ this._sharedClient.groups(groups)
255
+ }
256
+
257
+ if (personProperties) {
258
+ this._sharedClient.personProperties(personProperties)
259
+ }
260
+
261
+ if (groupProperties) {
262
+ this._sharedClient.groupProperties(groupProperties)
263
+ }
264
+ await this._sharedClient.reloadFeatureFlagsAsync(false)
265
+ response = this._sharedClient.getFeatureFlagPayload(key)
266
+ }
267
+
268
+ try {
269
+ return JSON.parse(response as any)
270
+ } catch {
271
+ return response
272
+ }
273
+ }
274
+
206
275
  async isFeatureEnabled(
207
276
  key: string,
208
277
  distinctId: string,
@@ -230,6 +299,19 @@ export class PostHog implements PostHogNodeV1 {
230
299
  onlyEvaluateLocally?: boolean
231
300
  }
232
301
  ): Promise<Record<string, string | boolean>> {
302
+ const response = await this.getAllFlagsAndPayloads(distinctId, options)
303
+ return response.featureFlags
304
+ }
305
+
306
+ async getAllFlagsAndPayloads(
307
+ distinctId: string,
308
+ options?: {
309
+ groups?: Record<string, string>
310
+ personProperties?: Record<string, string>
311
+ groupProperties?: Record<string, Record<string, string>>
312
+ onlyEvaluateLocally?: boolean
313
+ }
314
+ ): Promise<PosthogFlagsAndPayloadsResponse> {
233
315
  const { groups, personProperties, groupProperties } = options || {}
234
316
  let { onlyEvaluateLocally } = options || {}
235
317
 
@@ -238,17 +320,19 @@ export class PostHog implements PostHogNodeV1 {
238
320
  onlyEvaluateLocally = false
239
321
  }
240
322
 
241
- const localEvaluationResult = await this.featureFlagsPoller?.getAllFlags(
323
+ const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(
242
324
  distinctId,
243
325
  groups,
244
326
  personProperties,
245
327
  groupProperties
246
328
  )
247
329
 
248
- let response = {}
330
+ let featureFlags = {}
331
+ let featureFlagPayloads = {}
249
332
  let fallbackToDecide = true
250
333
  if (localEvaluationResult) {
251
- response = localEvaluationResult.response
334
+ featureFlags = localEvaluationResult.response
335
+ featureFlagPayloads = localEvaluationResult.payloads
252
336
  fallbackToDecide = localEvaluationResult.fallbackToDecide
253
337
  }
254
338
 
@@ -266,15 +350,22 @@ export class PostHog implements PostHogNodeV1 {
266
350
  this._sharedClient.groupProperties(groupProperties)
267
351
  }
268
352
  await this._sharedClient.reloadFeatureFlagsAsync(false)
269
- const remoteEvaluationResult = this._sharedClient.getFeatureFlags()
270
-
271
- return { ...response, ...remoteEvaluationResult }
353
+ const remoteEvaluationResult = this._sharedClient.getFeatureFlagsAndPayloads()
354
+ featureFlags = {
355
+ ...featureFlags,
356
+ ...(remoteEvaluationResult.flags || {}),
357
+ }
358
+ featureFlagPayloads = {
359
+ ...featureFlagPayloads,
360
+ ...(remoteEvaluationResult.payloads || {}),
361
+ }
272
362
  }
273
363
 
274
- return response
364
+ return { featureFlags, featureFlagPayloads }
275
365
  }
276
366
 
277
367
  groupIdentify({ groupType, groupKey, properties }: GroupIdentifyMessage): void {
368
+ this.reInit(`$${groupType}_${groupKey}`)
278
369
  this._sharedClient.groupIdentify(groupType, groupKey, properties)
279
370
  }
280
371
 
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { JsonType } from 'posthog-core/src'
2
+
1
3
  export interface IdentifyMessageV1 {
2
4
  distinctId: string
3
5
  properties?: Record<string | number, any>
@@ -39,6 +41,7 @@ export type PostHogFeatureFlag = {
39
41
  rollout_percentage: number
40
42
  }[]
41
43
  }
44
+ payloads?: Record<string, JsonType>
42
45
  }
43
46
  deleted: boolean
44
47
  active: boolean
@@ -140,6 +143,29 @@ export type PostHogNodeV1 = {
140
143
  }
141
144
  ): Promise<string | boolean | undefined>
142
145
 
146
+ /**
147
+ * @description Retrieves payload associated with the specified flag and matched value that is passed in.
148
+ * (Expected to be used in conjuction with getFeatureFlag but allows for manual lookup).
149
+ * If matchValue isn't passed, getFeatureFlag is called implicitly.
150
+ * Will try to evaluate for payload locally first otherwise default to network call if allowed
151
+ *
152
+ * @param key the unique key of your feature flag
153
+ * @param distinctId the current unique id
154
+ * @param matchValue optional- the matched flag string or boolean
155
+ * @param options: dict with optional parameters below
156
+ * @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false.
157
+ *
158
+ * @returns payload of a json type object
159
+ */
160
+ getFeatureFlagPayload(
161
+ key: string,
162
+ distinctId: string,
163
+ matchValue?: string | boolean,
164
+ options?: {
165
+ onlyEvaluateLocally?: boolean
166
+ }
167
+ ): Promise<JsonType | undefined>
168
+
143
169
  /**
144
170
  * @description Sets a groups properties, which allows asking questions like "Who are the most active companies"
145
171
  * using my product in PostHog.