posthog-node 2.0.2 → 2.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.
- package/CHANGELOG.md +11 -1
- package/lib/index.cjs.js +183 -32
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +22 -0
- package/lib/index.esm.js +182 -31
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +2 -0
- package/lib/posthog-core/src/types.d.ts +8 -1
- package/lib/posthog-node/src/feature-flags.d.ts +5 -3
- package/lib/posthog-node/src/fetch.d.ts +2 -0
- package/lib/posthog-node/src/posthog-node.d.ts +2 -1
- package/lib/posthog-node/src/types.d.ts +1 -0
- package/package.json +4 -4
- package/src/feature-flags.ts +84 -22
- package/src/fetch.ts +20 -0
- package/src/posthog-node.ts +6 -3
- package/src/types.ts +1 -0
- package/test/feature-flags.spec.ts +442 -115
- package/test/posthog-node.spec.ts +41 -40
|
@@ -26,6 +26,7 @@ export declare abstract class PostHogCore {
|
|
|
26
26
|
private _optoutOverride;
|
|
27
27
|
constructor(apiKey: string, options?: PosthogCoreOptions);
|
|
28
28
|
protected getCommonEventProperties(): any;
|
|
29
|
+
protected setupBootstrap(options?: Partial<PosthogCoreOptions>): void;
|
|
29
30
|
private get props();
|
|
30
31
|
private set props(value);
|
|
31
32
|
private clearProps;
|
|
@@ -76,6 +77,7 @@ export declare abstract class PostHogCore {
|
|
|
76
77
|
***/
|
|
77
78
|
private decideAsync;
|
|
78
79
|
private _decideAsync;
|
|
80
|
+
private setKnownFeatureFlags;
|
|
79
81
|
getFeatureFlag(key: string): boolean | string | undefined;
|
|
80
82
|
getFeatureFlags(): PostHogDecideResponse['featureFlags'] | undefined;
|
|
81
83
|
isFeatureEnabled(key: string): boolean | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
export declare type PosthogCoreOptions = {
|
|
2
3
|
host?: string;
|
|
3
4
|
flushAt?: number;
|
|
@@ -5,6 +6,11 @@ export declare type PosthogCoreOptions = {
|
|
|
5
6
|
enable?: boolean;
|
|
6
7
|
sendFeatureFlagEvent?: boolean;
|
|
7
8
|
preloadFeatureFlags?: boolean;
|
|
9
|
+
bootstrap?: {
|
|
10
|
+
distinctId?: string;
|
|
11
|
+
isIdentifiedId?: boolean;
|
|
12
|
+
featureFlags?: Record<string, boolean | string>;
|
|
13
|
+
};
|
|
8
14
|
fetchRetryCount?: number;
|
|
9
15
|
fetchRetryDelay?: number;
|
|
10
16
|
sessionExpirationTimeSeconds?: number;
|
|
@@ -30,7 +36,8 @@ export declare type PostHogFetchOptions = {
|
|
|
30
36
|
headers: {
|
|
31
37
|
[key: string]: string;
|
|
32
38
|
};
|
|
33
|
-
body
|
|
39
|
+
body?: string;
|
|
40
|
+
signal?: AbortSignal;
|
|
34
41
|
};
|
|
35
42
|
export declare type PostHogFetchResponse = {
|
|
36
43
|
status: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { FeatureFlagCondition, PostHogFeatureFlag } from './types';
|
|
3
|
-
import {
|
|
3
|
+
import { PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src';
|
|
4
4
|
declare class ClientError extends Error {
|
|
5
5
|
constructor(message: string);
|
|
6
6
|
}
|
|
@@ -13,6 +13,7 @@ declare type FeatureFlagsPollerOptions = {
|
|
|
13
13
|
host: string;
|
|
14
14
|
pollingInterval: number;
|
|
15
15
|
timeout?: number;
|
|
16
|
+
fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
|
|
16
17
|
};
|
|
17
18
|
declare class FeatureFlagsPoller {
|
|
18
19
|
pollingInterval: number;
|
|
@@ -24,7 +25,8 @@ declare class FeatureFlagsPoller {
|
|
|
24
25
|
timeout?: number;
|
|
25
26
|
host: FeatureFlagsPollerOptions['host'];
|
|
26
27
|
poller?: NodeJS.Timeout;
|
|
27
|
-
|
|
28
|
+
fetch: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
|
|
29
|
+
constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host, ...options }: FeatureFlagsPollerOptions);
|
|
28
30
|
getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<string | boolean | undefined>;
|
|
29
31
|
getAllFlags(distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<{
|
|
30
32
|
response: Record<string, string | boolean>;
|
|
@@ -41,7 +43,7 @@ declare class FeatureFlagsPoller {
|
|
|
41
43
|
}[];
|
|
42
44
|
loadFeatureFlags(forceReload?: boolean): Promise<void>;
|
|
43
45
|
_loadFeatureFlags(): Promise<void>;
|
|
44
|
-
_requestFeatureFlagDefinitions(): Promise<
|
|
46
|
+
_requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse>;
|
|
45
47
|
stopPoller(): void;
|
|
46
48
|
}
|
|
47
49
|
declare function matchProperty(property: FeatureFlagCondition['properties'][number], propertyValues: Record<string, any>): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PosthogCoreOptions } from '../../posthog-core/src';
|
|
1
|
+
import { PosthogCoreOptions, PostHogFetchOptions, PostHogFetchResponse } from '../../posthog-core/src';
|
|
2
2
|
import { EventMessageV1, GroupIdentifyMessage, IdentifyMessageV1, PostHogNodeV1 } from './types';
|
|
3
3
|
export declare type PostHogOptions = PosthogCoreOptions & {
|
|
4
4
|
persistence?: 'memory';
|
|
@@ -6,6 +6,7 @@ export declare type PostHogOptions = PosthogCoreOptions & {
|
|
|
6
6
|
featureFlagsPollingInterval?: number;
|
|
7
7
|
requestTimeout?: number;
|
|
8
8
|
maxCacheSize?: number;
|
|
9
|
+
fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
|
|
9
10
|
};
|
|
10
11
|
export declare class PostHog implements PostHogNodeV1 {
|
|
11
12
|
private _sharedClient;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posthog-node",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "PostHog Node.js integration",
|
|
5
5
|
"repository": "PostHog/posthog-node",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
7
|
+
"prepublishOnly": "cd .. && yarn build"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
|
-
"node": ">=
|
|
10
|
+
"node": ">=14.17.0"
|
|
11
11
|
},
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"author": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"module": "lib/index.esm.js",
|
|
20
20
|
"types": "lib/index.d.ts",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"
|
|
22
|
+
"axios": "^0.27.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^18.0.0",
|
package/src/feature-flags.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'crypto'
|
|
2
|
-
import { request } from 'undici'
|
|
3
2
|
import { FeatureFlagCondition, PostHogFeatureFlag } from './types'
|
|
4
3
|
import { version } from '../package.json'
|
|
5
|
-
import {
|
|
4
|
+
import { PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
|
|
5
|
+
import { safeSetTimeout } from 'posthog-core/src/utils'
|
|
6
|
+
import { fetch } from './fetch'
|
|
6
7
|
|
|
7
8
|
// eslint-disable-next-line
|
|
8
9
|
const LONG_SCALE = 0xfffffffffffffff
|
|
@@ -35,6 +36,7 @@ type FeatureFlagsPollerOptions = {
|
|
|
35
36
|
host: string
|
|
36
37
|
pollingInterval: number
|
|
37
38
|
timeout?: number
|
|
39
|
+
fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
class FeatureFlagsPoller {
|
|
@@ -47,8 +49,16 @@ class FeatureFlagsPoller {
|
|
|
47
49
|
timeout?: number
|
|
48
50
|
host: FeatureFlagsPollerOptions['host']
|
|
49
51
|
poller?: NodeJS.Timeout
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
fetch: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
|
|
53
|
+
|
|
54
|
+
constructor({
|
|
55
|
+
pollingInterval,
|
|
56
|
+
personalApiKey,
|
|
57
|
+
projectApiKey,
|
|
58
|
+
timeout,
|
|
59
|
+
host,
|
|
60
|
+
...options
|
|
61
|
+
}: FeatureFlagsPollerOptions) {
|
|
52
62
|
this.pollingInterval = pollingInterval
|
|
53
63
|
this.personalApiKey = personalApiKey
|
|
54
64
|
this.featureFlags = []
|
|
@@ -58,6 +68,8 @@ class FeatureFlagsPoller {
|
|
|
58
68
|
this.projectApiKey = projectApiKey
|
|
59
69
|
this.host = host
|
|
60
70
|
this.poller = undefined
|
|
71
|
+
// NOTE: as any is required here as the AbortSignal typing is slightly misaligned but works just fine
|
|
72
|
+
this.fetch = options.fetch || fetch
|
|
61
73
|
|
|
62
74
|
void this.loadFeatureFlags()
|
|
63
75
|
}
|
|
@@ -178,10 +190,34 @@ class FeatureFlagsPoller {
|
|
|
178
190
|
let isInconclusive = false
|
|
179
191
|
let result = undefined
|
|
180
192
|
|
|
181
|
-
|
|
193
|
+
// # Stable sort conditions with variant overrides to the top. This ensures that if overrides are present, they are
|
|
194
|
+
// # evaluated first, and the variant override is applied to the first matching condition.
|
|
195
|
+
const sortedFlagConditions = [...flagConditions].sort((conditionA, conditionB) => {
|
|
196
|
+
const AHasVariantOverride = !!conditionA.variant
|
|
197
|
+
const BHasVariantOverride = !!conditionB.variant
|
|
198
|
+
|
|
199
|
+
if (AHasVariantOverride && BHasVariantOverride) {
|
|
200
|
+
return 0
|
|
201
|
+
} else if (AHasVariantOverride) {
|
|
202
|
+
return -1
|
|
203
|
+
} else if (BHasVariantOverride) {
|
|
204
|
+
return 1
|
|
205
|
+
} else {
|
|
206
|
+
return 0
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
for (const condition of sortedFlagConditions) {
|
|
182
211
|
try {
|
|
183
212
|
if (this.isConditionMatch(flag, distinctId, condition, properties)) {
|
|
184
|
-
|
|
213
|
+
const variantOverride = condition.variant
|
|
214
|
+
const flagVariants = flagFilters.multivariate?.variants || []
|
|
215
|
+
if (variantOverride && flagVariants.some((variant) => variant.key === variantOverride)) {
|
|
216
|
+
result = variantOverride
|
|
217
|
+
} else {
|
|
218
|
+
result = this.getMatchingVariant(flag, distinctId) || true
|
|
219
|
+
}
|
|
220
|
+
break
|
|
185
221
|
}
|
|
186
222
|
} catch (e) {
|
|
187
223
|
if (e instanceof InconclusiveMatchError) {
|
|
@@ -190,7 +226,7 @@ class FeatureFlagsPoller {
|
|
|
190
226
|
throw e
|
|
191
227
|
}
|
|
192
228
|
}
|
|
193
|
-
}
|
|
229
|
+
}
|
|
194
230
|
|
|
195
231
|
if (result !== undefined) {
|
|
196
232
|
return result
|
|
@@ -275,12 +311,12 @@ class FeatureFlagsPoller {
|
|
|
275
311
|
try {
|
|
276
312
|
const res = await this._requestFeatureFlagDefinitions()
|
|
277
313
|
|
|
278
|
-
if (res && res.
|
|
314
|
+
if (res && res.status === 401) {
|
|
279
315
|
throw new ClientError(
|
|
280
316
|
`Your personalApiKey is invalid. Are you sure you're not using your Project API key? More information: https://posthog.com/docs/api/overview`
|
|
281
317
|
)
|
|
282
318
|
}
|
|
283
|
-
const responseJson = await res.
|
|
319
|
+
const responseJson = await res.json()
|
|
284
320
|
if (!('flags' in responseJson)) {
|
|
285
321
|
console.error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`)
|
|
286
322
|
}
|
|
@@ -297,31 +333,35 @@ class FeatureFlagsPoller {
|
|
|
297
333
|
}
|
|
298
334
|
}
|
|
299
335
|
|
|
300
|
-
async _requestFeatureFlagDefinitions(): Promise<
|
|
336
|
+
async _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse> {
|
|
301
337
|
const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}`
|
|
302
|
-
const headers = {
|
|
303
|
-
'Content-Type': 'application/json',
|
|
304
|
-
Authorization: `Bearer ${this.personalApiKey}`,
|
|
305
|
-
'user-agent': `posthog-node/${version}`,
|
|
306
|
-
}
|
|
307
338
|
|
|
308
|
-
const options:
|
|
339
|
+
const options: PostHogFetchOptions = {
|
|
309
340
|
method: 'GET',
|
|
310
|
-
headers:
|
|
341
|
+
headers: {
|
|
342
|
+
'Content-Type': 'application/json',
|
|
343
|
+
Authorization: `Bearer ${this.personalApiKey}`,
|
|
344
|
+
'user-agent': `posthog-node/${version}`,
|
|
345
|
+
},
|
|
311
346
|
}
|
|
312
347
|
|
|
348
|
+
let abortTimeout = null
|
|
349
|
+
|
|
313
350
|
if (this.timeout && typeof this.timeout === 'number') {
|
|
314
|
-
|
|
351
|
+
const controller = new AbortController()
|
|
352
|
+
abortTimeout = safeSetTimeout(() => {
|
|
353
|
+
controller.abort()
|
|
354
|
+
}, this.timeout)
|
|
355
|
+
options.signal = controller.signal
|
|
315
356
|
}
|
|
316
357
|
|
|
317
|
-
let res
|
|
318
358
|
try {
|
|
319
|
-
|
|
359
|
+
return await this.fetch(url, options)
|
|
320
360
|
} catch (err) {
|
|
321
361
|
throw new Error(`Request failed with error: ${err}`)
|
|
362
|
+
} finally {
|
|
363
|
+
clearTimeout(abortTimeout)
|
|
322
364
|
}
|
|
323
|
-
|
|
324
|
-
return res
|
|
325
365
|
}
|
|
326
366
|
|
|
327
367
|
stopPoller(): void {
|
|
@@ -378,6 +418,14 @@ function matchProperty(
|
|
|
378
418
|
return typeof overrideValue == typeof value && overrideValue < value
|
|
379
419
|
case 'lte':
|
|
380
420
|
return typeof overrideValue == typeof value && overrideValue <= value
|
|
421
|
+
case 'is_date_after':
|
|
422
|
+
case 'is_date_before':
|
|
423
|
+
const parsedDate = convertToDateTime(value)
|
|
424
|
+
const overrideDate = convertToDateTime(overrideValue)
|
|
425
|
+
if (operator === 'is_date_before') {
|
|
426
|
+
return overrideDate < parsedDate
|
|
427
|
+
}
|
|
428
|
+
return overrideDate > parsedDate
|
|
381
429
|
default:
|
|
382
430
|
console.error(`Unknown operator: ${operator}`)
|
|
383
431
|
return false
|
|
@@ -393,4 +441,18 @@ function isValidRegex(regex: string): boolean {
|
|
|
393
441
|
}
|
|
394
442
|
}
|
|
395
443
|
|
|
444
|
+
function convertToDateTime(value: string | number | (string | number)[] | Date): Date {
|
|
445
|
+
if (value instanceof Date) {
|
|
446
|
+
return value
|
|
447
|
+
} else if (typeof value === 'string' || typeof value === 'number') {
|
|
448
|
+
const date = new Date(value)
|
|
449
|
+
if (!isNaN(date.valueOf())) {
|
|
450
|
+
return date
|
|
451
|
+
}
|
|
452
|
+
throw new InconclusiveMatchError(`${value} is in an invalid date format`)
|
|
453
|
+
} else {
|
|
454
|
+
throw new InconclusiveMatchError(`The date provided ${value} must be a string, number, or date object`)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
396
458
|
export { FeatureFlagsPoller, matchProperty, InconclusiveMatchError, ClientError }
|
package/src/fetch.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src'
|
|
3
|
+
|
|
4
|
+
// NOTE: We use axios as a reliable, well supported request library but follow the Fetch API (roughly)
|
|
5
|
+
// So that alternative implementations can be used if desired
|
|
6
|
+
export const fetch = async (url: string, options: PostHogFetchOptions): Promise<PostHogFetchResponse> => {
|
|
7
|
+
const res = await axios.request({
|
|
8
|
+
url,
|
|
9
|
+
headers: options.headers,
|
|
10
|
+
method: options.method.toLowerCase(),
|
|
11
|
+
data: options.body,
|
|
12
|
+
signal: options.signal,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
status: res.status,
|
|
17
|
+
text: () => res.data,
|
|
18
|
+
json: () => res.data,
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/posthog-node.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { version } from '../package.json'
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import {
|
|
4
4
|
PostHogCore,
|
|
5
5
|
PosthogCoreOptions,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { PostHogMemoryStorage } from '../../posthog-core/src/storage-memory'
|
|
11
11
|
import { EventMessageV1, GroupIdentifyMessage, IdentifyMessageV1, PostHogNodeV1 } from './types'
|
|
12
12
|
import { FeatureFlagsPoller } from './feature-flags'
|
|
13
|
+
import { fetch } from './fetch'
|
|
13
14
|
|
|
14
15
|
export type PostHogOptions = PosthogCoreOptions & {
|
|
15
16
|
persistence?: 'memory'
|
|
@@ -20,6 +21,7 @@ export type PostHogOptions = PosthogCoreOptions & {
|
|
|
20
21
|
requestTimeout?: number
|
|
21
22
|
// Maximum size of cache that deduplicates $feature_flag_called calls per user.
|
|
22
23
|
maxCacheSize?: number
|
|
24
|
+
fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const THIRTY_SECONDS = 30 * 1000
|
|
@@ -28,7 +30,7 @@ const MAX_CACHE_SIZE = 50 * 1000
|
|
|
28
30
|
class PostHogClient extends PostHogCore {
|
|
29
31
|
private _memoryStorage = new PostHogMemoryStorage()
|
|
30
32
|
|
|
31
|
-
constructor(apiKey: string, options: PostHogOptions = {}) {
|
|
33
|
+
constructor(apiKey: string, private options: PostHogOptions = {}) {
|
|
32
34
|
options.captureMode = options?.captureMode || 'json'
|
|
33
35
|
options.preloadFeatureFlags = false // Don't preload as this makes no sense without a distinctId
|
|
34
36
|
options.sendFeatureFlagEvent = false // Let `posthog-node` handle this on its own, since we're dealing with multiple distinctIDs
|
|
@@ -50,7 +52,7 @@ class PostHogClient extends PostHogCore {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
fetch(url: string, options: PostHogFetchOptions): Promise<PostHogFetchResponse> {
|
|
53
|
-
return
|
|
55
|
+
return this.options.fetch ? this.options.fetch(url, options) : fetch(url, options)
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
getLibraryId(): string {
|
|
@@ -84,6 +86,7 @@ export class PostHog implements PostHogNodeV1 {
|
|
|
84
86
|
projectApiKey: apiKey,
|
|
85
87
|
timeout: options.requestTimeout,
|
|
86
88
|
host: this._sharedClient.host,
|
|
89
|
+
fetch: options.fetch,
|
|
87
90
|
})
|
|
88
91
|
}
|
|
89
92
|
this.distinctIdHasSentFlagCalls = {}
|