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.
@@ -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: string;
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 { ResponseData } from 'undici/types/dispatcher';
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
- constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host }: FeatureFlagsPollerOptions);
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<ResponseData>;
46
+ _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse>;
45
47
  stopPoller(): void;
46
48
  }
47
49
  declare function matchProperty(property: FeatureFlagCondition['properties'][number], propertyValues: Record<string, any>): boolean;
@@ -0,0 +1,2 @@
1
+ import { PostHogFetchOptions, PostHogFetchResponse } from 'posthog-core/src';
2
+ export declare const fetch: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
@@ -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;
@@ -20,6 +20,7 @@ export declare type FeatureFlagCondition = {
20
20
  operator?: string;
21
21
  }[];
22
22
  rollout_percentage?: number;
23
+ variant?: string;
23
24
  };
24
25
  export declare type PostHogFeatureFlag = {
25
26
  id: number;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": "PostHog/posthog-node",
6
6
  "scripts": {
7
- "prepublish": "cd .. && yarn build"
7
+ "prepublishOnly": "cd .. && yarn build"
8
8
  },
9
9
  "engines": {
10
- "node": ">=10"
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
- "undici": "^5.8.0"
22
+ "axios": "^0.27.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^18.0.0",
@@ -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 { ResponseData } from 'undici/types/dispatcher'
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
- constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host }: FeatureFlagsPollerOptions) {
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
- flagConditions.forEach((condition) => {
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
- result = this.getMatchingVariant(flag, distinctId) || true
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.statusCode === 401) {
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.body.json()
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<ResponseData> {
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: Parameters<typeof request>[1] = {
339
+ const options: PostHogFetchOptions = {
309
340
  method: 'GET',
310
- headers: 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
- options.bodyTimeout = this.timeout
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
- res = await request(url, options)
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
+ }
@@ -1,5 +1,5 @@
1
1
  import { version } from '../package.json'
2
- import undici from 'undici'
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 undici.fetch(url, options)
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 = {}
package/src/types.ts CHANGED
@@ -23,6 +23,7 @@ export type FeatureFlagCondition = {
23
23
  operator?: string
24
24
  }[]
25
25
  rollout_percentage?: number
26
+ variant?: string
26
27
  }
27
28
 
28
29
  export type PostHogFeatureFlag = {