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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/benchmarks/rusha-vs-native.mjs +70 -0
- package/lib/index.cjs.js +34 -58
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +20 -5
- package/lib/index.esm.js +34 -58
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/types.d.ts +4 -1
- package/lib/posthog-node/src/types.d.ts +16 -4
- package/package.json +4 -4
- package/src/feature-flags.ts +2 -3
- package/src/posthog-node.ts +30 -51
- package/src/types.ts +16 -4
- package/test/posthog-node.spec.ts +2 -35
- package/tsconfig.json +2 -2
- package/src/types/rusha.d.ts +0 -23
|
@@ -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
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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
|
|
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
|
+
"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",
|
package/src/feature-flags.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from '
|
|
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
|
-
|
|
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
|
}
|
package/src/posthog-node.ts
CHANGED
|
@@ -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
|
|
297
|
+
const { groups, disableGeoip } = options || {}
|
|
298
|
+
let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {}
|
|
298
299
|
|
|
299
|
-
const
|
|
300
|
+
const adjustedProperties = this.addLocalPersonAndGroupProperties(
|
|
300
301
|
distinctId,
|
|
301
302
|
groups,
|
|
302
303
|
personProperties,
|
|
303
304
|
groupProperties
|
|
304
305
|
)
|
|
305
306
|
|
|
306
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
//
|
|
326
|
-
|
|
332
|
+
// set defaults
|
|
333
|
+
if (onlyEvaluateLocally == undefined) {
|
|
334
|
+
onlyEvaluateLocally = false
|
|
335
|
+
}
|
|
327
336
|
|
|
328
|
-
|
|
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
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
|
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(
|
|
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"
|
|
5
|
-
"typeRoots": ["./node_modules/@types", "../node_modules/@types"
|
|
4
|
+
"types": ["node"],
|
|
5
|
+
"typeRoots": ["./node_modules/@types", "../node_modules/@types"]
|
|
6
6
|
}
|
|
7
7
|
}
|
package/src/types/rusha.d.ts
DELETED
|
@@ -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
|
-
}
|