posthog-node 4.3.1 → 4.4.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.
@@ -49,6 +49,8 @@ export declare enum PostHogPersistedProperty {
49
49
  Props = "props",
50
50
  FeatureFlags = "feature_flags",
51
51
  FeatureFlagPayloads = "feature_flag_payloads",
52
+ BootstrapFeatureFlags = "bootstrap_feature_flags",
53
+ BootstrapFeatureFlagPayloads = "bootstrap_feature_flag_payloads",
52
54
  OverrideFeatureFlags = "override_feature_flags",
53
55
  Queue = "queue",
54
56
  OptedOut = "opted_out",
@@ -58,7 +60,8 @@ export declare enum PostHogPersistedProperty {
58
60
  GroupProperties = "group_properties",
59
61
  InstalledAppBuild = "installed_app_build",
60
62
  InstalledAppVersion = "installed_app_version",
61
- SessionReplay = "session_replay"
63
+ SessionReplay = "session_replay",
64
+ DecideEndpointWasHit = "decide_endpoint_was_hit"
62
65
  }
63
66
  export type PostHogFetchOptions = {
64
67
  method: 'GET' | 'POST' | 'PUT' | 'PATCH';
@@ -140,13 +140,25 @@ export type PostHogNodeV1 = {
140
140
  }): Promise<string | boolean | undefined>;
141
141
  /**
142
142
  * @description Retrieves payload associated with the specified flag and matched value that is passed in.
143
- * (Expected to be used in conjuction with getFeatureFlag but allows for manual lookup).
144
- * If matchValue isn't passed, getFeatureFlag is called implicitly.
145
- * Will try to evaluate for payload locally first otherwise default to network call if allowed
143
+ *
144
+ * IMPORTANT: The `matchValue` parameter should be the value you previously obtained from `getFeatureFlag()`.
145
+ * If matchValue isn't passed (or is undefined), this method will automatically call `getFeatureFlag()`
146
+ * internally to fetch the flag value, which could result in a network call to the PostHog server if this flag can
147
+ * not be evaluated locally. This means that omitting `matchValue` will potentially:
148
+ * - Bypass local evaluation
149
+ * - Count as an additional flag evaluation against your quota
150
+ * - Impact performance due to the extra network request
151
+ *
152
+ * Example usage:
153
+ * ```js
154
+ * const flagValue = await client.getFeatureFlag('my-flag', distinctId);
155
+ * const payload = await client.getFeatureFlagPayload('my-flag', distinctId, flagValue);
156
+ * ```
146
157
  *
147
158
  * @param key the unique key of your feature flag
148
159
  * @param distinctId the current unique id
149
- * @param matchValue optional- the matched flag string or boolean
160
+ * @param matchValue The flag value previously obtained from calling `getFeatureFlag()`. Can be a string or boolean.
161
+ * To avoid extra network calls, pass this parameter when you can.
150
162
  * @param options: dict with optional parameters below
151
163
  * @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false.
152
164
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "4.3.1",
3
+ "version": "4.4.0",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,12 +23,12 @@
23
23
  "module": "lib/index.esm.js",
24
24
  "types": "lib/index.d.ts",
25
25
  "dependencies": {
26
- "axios": "^1.7.4",
27
- "rusha": "^0.8.14"
26
+ "axios": "^1.7.4"
28
27
  },
29
28
  "devDependencies": {
30
29
  "@types/node": "^18.0.0",
31
- "commander": "^9.3.0"
30
+ "commander": "^9.3.0",
31
+ "mitata": "^1.0.21"
32
32
  },
33
33
  "keywords": [
34
34
  "posthog",
@@ -1,4 +1,4 @@
1
- import { createHash } from 'rusha'
1
+ import { createHash } from 'node:crypto'
2
2
  import { FeatureFlagCondition, FlagProperty, PostHogFeatureFlag, PropertyGroup } from './types'
3
3
  import { JsonType, PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
4
4
  import { safeSetTimeout } from 'posthog-core/src/utils'
@@ -458,8 +458,7 @@ class FeatureFlagsPoller {
458
458
  // # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
459
459
  // # we can do _hash(key, distinct_id) < 0.2
460
460
  function _hash(key: string, distinctId: string, salt: string = ''): number {
461
- // rusha is a fast sha1 implementation in pure javascript
462
- const sha1Hash = createHash()
461
+ const sha1Hash = createHash('sha1')
463
462
  sha1Hash.update(`${key}.${distinctId}${salt}`)
464
463
  return parseInt(sha1Hash.digest('hex').slice(0, 15), 16) / LONG_SCALE
465
464
  }
@@ -294,80 +294,59 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
294
294
  disableGeoip?: boolean
295
295
  }
296
296
  ): Promise<JsonType | undefined> {
297
- const { groups, disableGeoip, onlyEvaluateLocally = false, personProperties, groupProperties } = options || {}
297
+ const { groups, disableGeoip } = options || {}
298
+ let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {}
298
299
 
299
- const { allPersonProperties, allGroupProperties } = this.addLocalPersonAndGroupProperties(
300
+ const adjustedProperties = this.addLocalPersonAndGroupProperties(
300
301
  distinctId,
301
302
  groups,
302
303
  personProperties,
303
304
  groupProperties
304
305
  )
305
306
 
306
- if (matchValue === undefined) {
307
+ personProperties = adjustedProperties.allPersonProperties
308
+ groupProperties = adjustedProperties.allGroupProperties
309
+
310
+ let response = undefined
311
+
312
+ // Try to get match value locally if not provided
313
+ if (!matchValue) {
307
314
  matchValue = await this.getFeatureFlag(key, distinctId, {
308
315
  ...options,
309
316
  onlyEvaluateLocally: true,
310
- sendFeatureFlagEvents: false,
311
317
  })
312
318
  }
313
319
 
314
- let response: string | boolean | undefined
315
- let payload: JsonType | undefined
316
-
317
320
  if (matchValue) {
318
- response = matchValue
319
- payload = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue)
320
- } else {
321
- response = undefined
322
- payload = undefined
321
+ response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue)
322
+ }
323
+
324
+ // set defaults
325
+ if (onlyEvaluateLocally == undefined) {
326
+ onlyEvaluateLocally = false
327
+ }
328
+ if (sendFeatureFlagEvents == undefined) {
329
+ sendFeatureFlagEvents = true
323
330
  }
324
331
 
325
- // Determine if the payload was evaluated locally
326
- const payloadWasLocallyEvaluated = payload !== undefined
332
+ // set defaults
333
+ if (onlyEvaluateLocally == undefined) {
334
+ onlyEvaluateLocally = false
335
+ }
327
336
 
328
- // Fetch final flags and payloads either locally or from the remote server
329
- let fetchedOrLocalFlags: Record<string, string | boolean> | undefined
330
- let fetchedOrLocalPayloads: Record<string, JsonType | undefined> | undefined
337
+ const payloadWasLocallyEvaluated = response !== undefined
331
338
 
332
- if (payloadWasLocallyEvaluated || onlyEvaluateLocally) {
333
- if (response !== undefined) {
334
- fetchedOrLocalFlags = { [key]: response }
335
- fetchedOrLocalPayloads = { [key]: payload }
336
- } else {
337
- fetchedOrLocalFlags = {}
338
- fetchedOrLocalPayloads = {}
339
- }
340
- } else {
341
- const fetchedData = await super.getFeatureFlagsAndPayloadsStateless(
339
+ if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
340
+ response = await super.getFeatureFlagPayloadStateless(
341
+ key,
342
342
  distinctId,
343
343
  groups,
344
- allPersonProperties,
345
- allGroupProperties,
344
+ personProperties,
345
+ groupProperties,
346
346
  disableGeoip
347
347
  )
348
- fetchedOrLocalFlags = fetchedData.flags || {}
349
- fetchedOrLocalPayloads = fetchedData.payloads || {}
350
348
  }
351
-
352
- const finalResponse = fetchedOrLocalFlags[key]
353
- const finalPayload = fetchedOrLocalPayloads[key]
354
- const finalLocallyEvaluated = payloadWasLocallyEvaluated
355
-
356
- this.capture({
357
- distinctId,
358
- event: '$feature_flag_called',
359
- properties: {
360
- $feature_flag: key,
361
- $feature_flag_response: finalResponse,
362
- $feature_flag_payload: finalPayload,
363
- locally_evaluated: finalLocallyEvaluated,
364
- [`$feature/${key}`]: finalResponse,
365
- },
366
- groups,
367
- disableGeoip,
368
- })
369
-
370
- return finalPayload
349
+ return response
371
350
  }
372
351
 
373
352
  async isFeatureEnabled(
package/src/types.ts CHANGED
@@ -158,13 +158,25 @@ export type PostHogNodeV1 = {
158
158
 
159
159
  /**
160
160
  * @description Retrieves payload associated with the specified flag and matched value that is passed in.
161
- * (Expected to be used in conjuction with getFeatureFlag but allows for manual lookup).
162
- * If matchValue isn't passed, getFeatureFlag is called implicitly.
163
- * Will try to evaluate for payload locally first otherwise default to network call if allowed
161
+ *
162
+ * IMPORTANT: The `matchValue` parameter should be the value you previously obtained from `getFeatureFlag()`.
163
+ * If matchValue isn't passed (or is undefined), this method will automatically call `getFeatureFlag()`
164
+ * internally to fetch the flag value, which could result in a network call to the PostHog server if this flag can
165
+ * not be evaluated locally. This means that omitting `matchValue` will potentially:
166
+ * - Bypass local evaluation
167
+ * - Count as an additional flag evaluation against your quota
168
+ * - Impact performance due to the extra network request
169
+ *
170
+ * Example usage:
171
+ * ```js
172
+ * const flagValue = await client.getFeatureFlag('my-flag', distinctId);
173
+ * const payload = await client.getFeatureFlagPayload('my-flag', distinctId, flagValue);
174
+ * ```
164
175
  *
165
176
  * @param key the unique key of your feature flag
166
177
  * @param distinctId the current unique id
167
- * @param matchValue optional- the matched flag string or boolean
178
+ * @param matchValue The flag value previously obtained from calling `getFeatureFlag()`. Can be a string or boolean.
179
+ * To avoid extra network calls, pass this parameter when you can.
168
180
  * @param options: dict with optional parameters below
169
181
  * @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false.
170
182
  *
@@ -897,18 +897,13 @@ describe('PostHog Node.js', () => {
897
897
  rollout_percentage: 100,
898
898
  },
899
899
  ],
900
- payloads: { true: { variant: 'A' } },
901
900
  },
902
901
  },
903
902
  ],
904
903
  }
905
904
 
906
905
  mockedFetch.mockImplementation(
907
- apiImplementation({
908
- localFlags: flags,
909
- decideFlags: { 'decide-flag': 'decide-value' },
910
- decideFlagPayloads: { 'beta-feature': { variant: 'A' } },
911
- })
906
+ apiImplementation({ localFlags: flags, decideFlags: { 'decide-flag': 'decide-value' } })
912
907
  )
913
908
 
914
909
  posthog = new PostHog('TEST_API_KEY', {
@@ -950,34 +945,6 @@ describe('PostHog Node.js', () => {
950
945
  )
951
946
  mockedFetch.mockClear()
952
947
 
953
- expect(
954
- await posthog.getFeatureFlagPayload('beta-feature', 'some-distinct-id', undefined, {
955
- personProperties: { region: 'USA', name: 'Aloha' },
956
- })
957
- ).toEqual({ variant: 'A' })
958
-
959
- // TRICKY: There's now an extra step before events are queued, so need to wait for that to resolve
960
- jest.runOnlyPendingTimers()
961
- await waitForPromises()
962
- await posthog.flush()
963
-
964
- expect(mockedFetch).toHaveBeenCalledWith('http://example.com/batch/', expect.any(Object))
965
-
966
- expect(getLastBatchEvents()?.[0]).toEqual(
967
- expect.objectContaining({
968
- distinct_id: 'some-distinct-id',
969
- event: '$feature_flag_called',
970
- properties: expect.objectContaining({
971
- $feature_flag: 'beta-feature',
972
- $feature_flag_response: true,
973
- $feature_flag_payload: { variant: 'A' },
974
- locally_evaluated: true,
975
- [`$feature/${'beta-feature'}`]: true,
976
- }),
977
- })
978
- )
979
- mockedFetch.mockClear()
980
-
981
948
  // # called again for same user, shouldn't call capture again
982
949
  expect(
983
950
  await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
@@ -1114,7 +1081,7 @@ describe('PostHog Node.js', () => {
1114
1081
  expect(mockedFetch).toHaveBeenCalledTimes(0)
1115
1082
 
1116
1083
  await expect(posthog.getFeatureFlagPayload('false-flag', '123', true)).resolves.toEqual(300)
1117
- expect(mockedFetch).toHaveBeenCalledTimes(1) // this now calls the server, because in this case the flag is not locally evaluated but we have a payload that we need to calculate
1084
+ expect(mockedFetch).toHaveBeenCalledTimes(0)
1118
1085
  })
1119
1086
 
1120
1087
  it('should not double parse json with getFeatureFlagPayloads and server eval', async () => {
package/tsconfig.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extends": "../tsconfig.json",
3
3
  "compilerOptions": {
4
- "types": ["node", "rusha"],
5
- "typeRoots": ["./node_modules/@types", "../node_modules/@types", "./src/types"]
4
+ "types": ["node"],
5
+ "typeRoots": ["./node_modules/@types", "../node_modules/@types"]
6
6
  }
7
7
  }
@@ -1,23 +0,0 @@
1
- // Adjusted from type definitions for rusha 0.8
2
- // Project: https://github.com/srijs/rusha#readme
3
- // Definitions by: Jacopo Scazzosi <https://github.com/jacoscaz>
4
- // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5
- // Minimum TypeScript Version: 4.0
6
-
7
- /// <reference types="node" />
8
-
9
- interface Hash {
10
- update(value: string | number[] | ArrayBuffer | Buffer): Hash
11
- digest(encoding?: undefined): ArrayBuffer
12
- digest(encoding: 'hex'): string
13
- }
14
-
15
- interface Rusha {
16
- createHash(): Hash
17
- }
18
-
19
- declare const Rusha: Rusha
20
-
21
- declare module 'rusha' {
22
- export = Rusha
23
- }