posthog-node 2.0.0-alpha9 → 2.0.1

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.
@@ -5,7 +5,7 @@ import { LZString } from './lz-string';
5
5
  import { SimpleEventEmitter } from './eventemitter';
6
6
  export declare abstract class PostHogCore {
7
7
  private apiKey;
8
- private host;
8
+ host: string;
9
9
  private flushAt;
10
10
  private flushInterval;
11
11
  private captureMode;
@@ -28,12 +28,13 @@ export declare abstract class PostHogCore {
28
28
  protected getCommonEventProperties(): any;
29
29
  private get props();
30
30
  private set props(value);
31
+ private clearProps;
31
32
  private _props;
32
33
  get optedOut(): boolean;
33
34
  optIn(): void;
34
35
  optOut(): void;
35
36
  on(event: string, cb: (...args: any[]) => void): () => void;
36
- reset(): void;
37
+ reset(propertiesToKeep?: PostHogPersistedProperty[]): void;
37
38
  debug(enabled?: boolean): void;
38
39
  private buildPayload;
39
40
  getSessionId(): string | undefined;
@@ -50,7 +51,7 @@ export declare abstract class PostHogCore {
50
51
  identify(distinctId?: string, properties?: PostHogEventProperties): this;
51
52
  capture(event: string, properties?: {
52
53
  [key: string]: any;
53
- }): this;
54
+ }, forceSendFeatureFlags?: boolean): this;
54
55
  alias(alias: string): this;
55
56
  autocapture(eventType: string, elements: PostHogAutocaptureElement[], properties?: PostHogEventProperties): this;
56
57
  /***
@@ -61,18 +62,30 @@ export declare abstract class PostHogCore {
61
62
  }): this;
62
63
  group(groupType: string, groupKey: string | number, groupProperties?: PostHogEventProperties): this;
63
64
  groupIdentify(groupType: string, groupKey: string | number, groupProperties?: PostHogEventProperties): this;
65
+ /***
66
+ * PROPERTIES
67
+ ***/
68
+ personProperties(properties: {
69
+ [type: string]: string;
70
+ }): this;
71
+ groupProperties(properties: {
72
+ [type: string]: Record<string, string>;
73
+ }): this;
64
74
  /***
65
75
  *** FEATURE FLAGS
66
76
  ***/
67
77
  private decideAsync;
68
78
  private _decideAsync;
69
- getFeatureFlag(key: string, defaultResult?: string | boolean): boolean | string | undefined;
79
+ getFeatureFlag(key: string): boolean | string | undefined;
70
80
  getFeatureFlags(): PostHogDecideResponse['featureFlags'] | undefined;
71
- isFeatureEnabled(key: string, defaultResult?: boolean): boolean;
72
- reloadFeatureFlagsAsync(): Promise<PostHogDecideResponse['featureFlags']>;
81
+ isFeatureEnabled(key: string): boolean | undefined;
82
+ reloadFeatureFlagsAsync(sendAnonDistinctId?: boolean): Promise<PostHogDecideResponse['featureFlags']>;
73
83
  onFeatureFlags(cb: (flags: PostHogDecideResponse['featureFlags']) => void): () => void;
74
84
  onFeatureFlag(key: string, cb: (value: string | boolean) => void): () => void;
75
85
  overrideFeatureFlag(flags: PostHogDecideResponse['featureFlags'] | null): void;
86
+ _sendFeatureFlags(event: string, properties?: {
87
+ [key: string]: any;
88
+ }): void;
76
89
  /***
77
90
  *** QUEUEING AND FLUSHING
78
91
  ***/
@@ -19,7 +19,9 @@ export declare enum PostHogPersistedProperty {
19
19
  Queue = "queue",
20
20
  OptedOut = "opted_out",
21
21
  SessionId = "session_id",
22
- SessionLastTimestamp = "session_timestamp"
22
+ SessionLastTimestamp = "session_timestamp",
23
+ PersonProperties = "person_properties",
24
+ GroupProperties = "group_properties"
23
25
  }
24
26
  export declare type PostHogFetchOptions = {
25
27
  method: 'GET' | 'POST' | 'PUT' | 'PATCH';
@@ -0,0 +1,48 @@
1
+ /// <reference types="node" />
2
+ import { FeatureFlagCondition, PostHogFeatureFlag } from './types';
3
+ import { ResponseData } from 'undici/types/dispatcher';
4
+ declare class ClientError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ declare class InconclusiveMatchError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ declare type FeatureFlagsPollerOptions = {
11
+ personalApiKey: string;
12
+ projectApiKey: string;
13
+ host: string;
14
+ pollingInterval: number;
15
+ timeout?: number;
16
+ };
17
+ declare class FeatureFlagsPoller {
18
+ pollingInterval: number;
19
+ personalApiKey: string;
20
+ projectApiKey: string;
21
+ featureFlags: Array<PostHogFeatureFlag>;
22
+ groupTypeMapping: Record<string, string>;
23
+ loadedSuccessfullyOnce: boolean;
24
+ timeout?: number;
25
+ host: FeatureFlagsPollerOptions['host'];
26
+ poller?: NodeJS.Timeout;
27
+ constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host }: FeatureFlagsPollerOptions);
28
+ getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<string | boolean | undefined>;
29
+ getAllFlags(distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<{
30
+ response: Record<string, string | boolean>;
31
+ fallbackToDecide: boolean;
32
+ }>;
33
+ computeFlagLocally(flag: PostHogFeatureFlag, distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): string | boolean;
34
+ matchFeatureFlagProperties(flag: PostHogFeatureFlag, distinctId: string, properties: Record<string, string>): string | boolean;
35
+ isConditionMatch(flag: PostHogFeatureFlag, distinctId: string, condition: FeatureFlagCondition, properties: Record<string, string>): boolean;
36
+ getMatchingVariant(flag: PostHogFeatureFlag, distinctId: string): string | boolean | undefined;
37
+ variantLookupTable(flag: PostHogFeatureFlag): {
38
+ valueMin: number;
39
+ valueMax: number;
40
+ key: string;
41
+ }[];
42
+ loadFeatureFlags(forceReload?: boolean): Promise<void>;
43
+ _loadFeatureFlags(): Promise<void>;
44
+ _requestFeatureFlagDefinitions(): Promise<ResponseData>;
45
+ stopPoller(): void;
46
+ }
47
+ declare function matchProperty(property: FeatureFlagCondition['properties'][number], propertyValues: Record<string, any>): boolean;
48
+ export { FeatureFlagsPoller, matchProperty, InconclusiveMatchError, ClientError };
@@ -2,23 +2,49 @@ import { PosthogCoreOptions } from '../../posthog-core/src';
2
2
  import { EventMessageV1, GroupIdentifyMessage, IdentifyMessageV1, PostHogNodeV1 } from './types';
3
3
  export declare type PostHogOptions = PosthogCoreOptions & {
4
4
  persistence?: 'memory';
5
+ personalApiKey?: string;
6
+ featureFlagsPollingInterval?: number;
7
+ requestTimeout?: number;
8
+ maxCacheSize?: number;
5
9
  };
6
10
  export declare class PostHogGlobal implements PostHogNodeV1 {
7
11
  private _sharedClient;
12
+ private featureFlagsPoller?;
13
+ private maxCacheSize;
14
+ distinctIdHasSentFlagCalls: Record<string, string[]>;
8
15
  constructor(apiKey: string, options?: PostHogOptions);
9
16
  private reInit;
10
17
  enable(): void;
11
18
  disable(): void;
12
- capture({ distinctId, event, properties, groups }: EventMessageV1): void;
19
+ capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void;
13
20
  identify({ distinctId, properties }: IdentifyMessageV1): void;
14
21
  alias(data: {
15
22
  distinctId: string;
16
23
  alias: string;
17
24
  }): void;
18
- getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string> | undefined): Promise<string | boolean | undefined>;
19
- isFeatureEnabled(key: string, distinctId: string, defaultResult?: boolean | undefined, groups?: Record<string, string> | undefined): Promise<boolean>;
25
+ getFeatureFlag(key: string, distinctId: string, options?: {
26
+ groups?: Record<string, string>;
27
+ personProperties?: Record<string, string>;
28
+ groupProperties?: Record<string, Record<string, string>>;
29
+ onlyEvaluateLocally?: boolean;
30
+ sendFeatureFlagEvents?: boolean;
31
+ }): Promise<string | boolean | undefined>;
32
+ isFeatureEnabled(key: string, distinctId: string, options?: {
33
+ groups?: Record<string, string>;
34
+ personProperties?: Record<string, string>;
35
+ groupProperties?: Record<string, Record<string, string>>;
36
+ onlyEvaluateLocally?: boolean;
37
+ sendFeatureFlagEvents?: boolean;
38
+ }): Promise<boolean | undefined>;
39
+ getAllFlags(distinctId: string, options?: {
40
+ groups?: Record<string, string>;
41
+ personProperties?: Record<string, string>;
42
+ groupProperties?: Record<string, Record<string, string>>;
43
+ onlyEvaluateLocally?: boolean;
44
+ }): Promise<Record<string, string | boolean>>;
20
45
  groupIdentify({ groupType, groupKey, properties }: GroupIdentifyMessage): void;
21
46
  reloadFeatureFlags(): Promise<void>;
47
+ flush(): void;
22
48
  shutdown(): void;
23
49
  shutdownAsync(): Promise<void>;
24
50
  debug(enabled?: boolean): void;
@@ -12,6 +12,36 @@ export interface GroupIdentifyMessage {
12
12
  groupKey: string;
13
13
  properties?: Record<string | number, any>;
14
14
  }
15
+ export declare type FeatureFlagCondition = {
16
+ properties: {
17
+ key: string;
18
+ type?: string;
19
+ value: string | number | (string | number)[];
20
+ operator?: string;
21
+ }[];
22
+ rollout_percentage?: number;
23
+ };
24
+ export declare type PostHogFeatureFlag = {
25
+ id: number;
26
+ name: string;
27
+ key: string;
28
+ filters?: {
29
+ aggregation_group_type_index?: number;
30
+ groups?: FeatureFlagCondition[];
31
+ multivariate?: {
32
+ variants: {
33
+ key: string;
34
+ rollout_percentage: number;
35
+ }[];
36
+ };
37
+ };
38
+ deleted: boolean;
39
+ active: boolean;
40
+ is_simple_flag: boolean;
41
+ rollout_percentage: null | number;
42
+ ensure_experience_continuity: boolean;
43
+ experiment_set: number[];
44
+ };
15
45
  export declare type PostHogNodeV1 = {
16
46
  /**
17
47
  * @description Capture allows you to capture anything a user does within your system,
@@ -22,7 +52,7 @@ export declare type PostHogNodeV1 = {
22
52
  * @param event We recommend using [verb] [noun], like movie played or movie updated to easily identify what your events mean later on.
23
53
  * @param properties OPTIONAL | which can be a object with any information you'd like to add
24
54
  * @param groups OPTIONAL | object of what groups are related to this event, example: { company: 'id:5' }. Can be used to analyze companies instead of users.
25
- * @param sendFeatureFlags OPTIONAL | Used with experiments
55
+ * @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event.
26
56
  */
27
57
  capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void;
28
58
  /**
@@ -53,14 +83,47 @@ export declare type PostHogNodeV1 = {
53
83
  * allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog,
54
84
  * you can use this method to check if the flag is on for a given user, allowing you to create logic to turn
55
85
  * features on and off for different user groups or individual users.
56
- * IMPORTANT: To use this method, you need to specify `personalApiKey` in your config! More info: https://posthog.com/docs/api/overview
57
86
  * @param key the unique key of your feature flag
58
87
  * @param distinctId the current unique id
59
- * @param defaultResult optional - default value to be returned if the feature flag is not on for the user
60
- * @param groups optional - what groups are currently active (group analytics)
88
+ * @param options: dict with optional parameters below
89
+ * @param groups optional - what groups are currently active (group analytics). Required if the flag depends on groups.
90
+ * @param personProperties optional - what person properties are known. Used to compute flags locally, if personalApiKey is present.
91
+ * @param groupProperties optional - what group properties are known. Used to compute flags locally, if personalApiKey is present.
92
+ * @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false.
93
+ * @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true.
94
+ *
95
+ * @returns true if the flag is on, false if the flag is off, undefined if there was an error.
96
+ */
97
+ isFeatureEnabled(key: string, distinctId: string, options?: {
98
+ groups?: Record<string, string>;
99
+ personProperties?: Record<string, string>;
100
+ groupProperties?: Record<string, Record<string, string>>;
101
+ onlyEvaluateLocally?: boolean;
102
+ sendFeatureFlagEvents?: boolean;
103
+ }): Promise<boolean | undefined>;
104
+ /**
105
+ * @description PostHog feature flags (https://posthog.com/docs/features/feature-flags)
106
+ * allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog,
107
+ * you can use this method to check if the flag is on for a given user, allowing you to create logic to turn
108
+ * features on and off for different user groups or individual users.
109
+ * @param key the unique key of your feature flag
110
+ * @param distinctId the current unique id
111
+ * @param options: dict with optional parameters below
112
+ * @param groups optional - what groups are currently active (group analytics). Required if the flag depends on groups.
113
+ * @param personProperties optional - what person properties are known. Used to compute flags locally, if personalApiKey is present.
114
+ * @param groupProperties optional - what group properties are known. Used to compute flags locally, if personalApiKey is present.
115
+ * @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false.
116
+ * @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true.
117
+ *
118
+ * @returns true or string(for multivariates) if the flag is on, false if the flag is off, undefined if there was an error.
61
119
  */
62
- isFeatureEnabled(key: string, distinctId: string, defaultResult?: boolean, groups?: Record<string, string>): Promise<boolean>;
63
- getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string>): Promise<string | boolean | undefined>;
120
+ getFeatureFlag(key: string, distinctId: string, options?: {
121
+ groups?: Record<string, string>;
122
+ personProperties?: Record<string, string>;
123
+ groupProperties?: Record<string, Record<string, string>>;
124
+ onlyEvaluateLocally?: boolean;
125
+ sendFeatureFlagEvents?: boolean;
126
+ }): Promise<string | boolean | undefined>;
64
127
  /**
65
128
  * @description Sets a groups properties, which allows asking questions like "Who are the most active companies"
66
129
  * using my product in PostHog.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "2.0.0-alpha9",
3
+ "version": "2.0.1",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": "PostHog/posthog-node",
6
6
  "scripts": {
@@ -0,0 +1,396 @@
1
+ import { createHash } from 'crypto'
2
+ import { request } from 'undici'
3
+ import { FeatureFlagCondition, PostHogFeatureFlag } from './types'
4
+ import { version } from '../package.json'
5
+ import { ResponseData } from 'undici/types/dispatcher'
6
+
7
+ // eslint-disable-next-line
8
+ const LONG_SCALE = 0xfffffffffffffff
9
+
10
+ class ClientError extends Error {
11
+ constructor(message: string) {
12
+ super()
13
+ Error.captureStackTrace(this, this.constructor)
14
+ this.name = 'ClientError'
15
+ this.message = message
16
+ Object.setPrototypeOf(this, ClientError.prototype)
17
+ }
18
+ }
19
+
20
+ class InconclusiveMatchError extends Error {
21
+ constructor(message: string) {
22
+ super(message)
23
+ this.name = this.constructor.name
24
+ Error.captureStackTrace(this, this.constructor)
25
+ // instanceof doesn't work in ES3 or ES5
26
+ // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
27
+ // this is the workaround
28
+ Object.setPrototypeOf(this, InconclusiveMatchError.prototype)
29
+ }
30
+ }
31
+
32
+ type FeatureFlagsPollerOptions = {
33
+ personalApiKey: string
34
+ projectApiKey: string
35
+ host: string
36
+ pollingInterval: number
37
+ timeout?: number
38
+ }
39
+
40
+ class FeatureFlagsPoller {
41
+ pollingInterval: number
42
+ personalApiKey: string
43
+ projectApiKey: string
44
+ featureFlags: Array<PostHogFeatureFlag>
45
+ groupTypeMapping: Record<string, string>
46
+ loadedSuccessfullyOnce: boolean
47
+ timeout?: number
48
+ host: FeatureFlagsPollerOptions['host']
49
+ poller?: NodeJS.Timeout
50
+
51
+ constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host }: FeatureFlagsPollerOptions) {
52
+ this.pollingInterval = pollingInterval
53
+ this.personalApiKey = personalApiKey
54
+ this.featureFlags = []
55
+ this.groupTypeMapping = {}
56
+ this.loadedSuccessfullyOnce = false
57
+ this.timeout = timeout
58
+ this.projectApiKey = projectApiKey
59
+ this.host = host
60
+ this.poller = undefined
61
+
62
+ void this.loadFeatureFlags()
63
+ }
64
+
65
+ async getFeatureFlag(
66
+ key: string,
67
+ distinctId: string,
68
+ groups: Record<string, string> = {},
69
+ personProperties: Record<string, string> = {},
70
+ groupProperties: Record<string, Record<string, string>> = {}
71
+ ): Promise<string | boolean | undefined> {
72
+ await this.loadFeatureFlags()
73
+
74
+ let response = undefined
75
+ let featureFlag = undefined
76
+
77
+ if (!this.loadedSuccessfullyOnce) {
78
+ return response
79
+ }
80
+
81
+ for (const flag of this.featureFlags) {
82
+ if (key === flag.key) {
83
+ featureFlag = flag
84
+ break
85
+ }
86
+ }
87
+
88
+ if (featureFlag !== undefined) {
89
+ try {
90
+ response = this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties)
91
+ console.debug(`Successfully computed flag locally: ${key} -> ${response}`)
92
+ } catch (e) {
93
+ if (e instanceof InconclusiveMatchError) {
94
+ console.debug(`Can't compute flag locally: ${key}: ${e}`)
95
+ } else if (e instanceof Error) {
96
+ console.error(`Error computing flag locally: ${key}: ${e}`)
97
+ }
98
+ }
99
+ }
100
+
101
+ return response
102
+ }
103
+
104
+ async getAllFlags(
105
+ distinctId: string,
106
+ groups: Record<string, string> = {},
107
+ personProperties: Record<string, string> = {},
108
+ groupProperties: Record<string, Record<string, string>> = {}
109
+ ): Promise<{ response: Record<string, string | boolean>; fallbackToDecide: boolean }> {
110
+ await this.loadFeatureFlags()
111
+
112
+ const response: Record<string, string | boolean> = {}
113
+ let fallbackToDecide = this.featureFlags.length == 0
114
+
115
+ this.featureFlags.map((flag) => {
116
+ try {
117
+ response[flag.key] = this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties)
118
+ } catch (e) {
119
+ if (e instanceof InconclusiveMatchError) {
120
+ // do nothing
121
+ } else if (e instanceof Error) {
122
+ console.error(`Error computing flag locally: ${flag.key}: ${e}`)
123
+ }
124
+ fallbackToDecide = true
125
+ }
126
+ })
127
+
128
+ return { response, fallbackToDecide }
129
+ }
130
+
131
+ computeFlagLocally(
132
+ flag: PostHogFeatureFlag,
133
+ distinctId: string,
134
+ groups: Record<string, string> = {},
135
+ personProperties: Record<string, string> = {},
136
+ groupProperties: Record<string, Record<string, string>> = {}
137
+ ): string | boolean {
138
+ if (flag.ensure_experience_continuity) {
139
+ throw new InconclusiveMatchError('Flag has experience continuity enabled')
140
+ }
141
+
142
+ if (!flag.active) {
143
+ return false
144
+ }
145
+
146
+ const flagFilters = flag.filters || {}
147
+ const aggregation_group_type_index = flagFilters.aggregation_group_type_index
148
+
149
+ if (aggregation_group_type_index != undefined) {
150
+ const groupName = this.groupTypeMapping[String(aggregation_group_type_index)]
151
+
152
+ if (!groupName) {
153
+ console.warn(
154
+ `[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`
155
+ )
156
+ throw new InconclusiveMatchError('Flag has unknown group type index')
157
+ }
158
+
159
+ if (!(groupName in groups)) {
160
+ console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`)
161
+ return false
162
+ }
163
+
164
+ const focusedGroupProperties = groupProperties[groupName]
165
+ return this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties)
166
+ } else {
167
+ return this.matchFeatureFlagProperties(flag, distinctId, personProperties)
168
+ }
169
+ }
170
+
171
+ matchFeatureFlagProperties(
172
+ flag: PostHogFeatureFlag,
173
+ distinctId: string,
174
+ properties: Record<string, string>
175
+ ): string | boolean {
176
+ const flagFilters = flag.filters || {}
177
+ const flagConditions = flagFilters.groups || []
178
+ let isInconclusive = false
179
+ let result = undefined
180
+
181
+ flagConditions.forEach((condition) => {
182
+ try {
183
+ if (this.isConditionMatch(flag, distinctId, condition, properties)) {
184
+ result = this.getMatchingVariant(flag, distinctId) || true
185
+ }
186
+ } catch (e) {
187
+ if (e instanceof InconclusiveMatchError) {
188
+ isInconclusive = true
189
+ } else {
190
+ throw e
191
+ }
192
+ }
193
+ })
194
+
195
+ if (result !== undefined) {
196
+ return result
197
+ } else if (isInconclusive) {
198
+ throw new InconclusiveMatchError("Can't determine if feature flag is enabled or not with given properties")
199
+ }
200
+
201
+ // We can only return False when all conditions are False
202
+ return false
203
+ }
204
+
205
+ isConditionMatch(
206
+ flag: PostHogFeatureFlag,
207
+ distinctId: string,
208
+ condition: FeatureFlagCondition,
209
+ properties: Record<string, string>
210
+ ): boolean {
211
+ const rolloutPercentage = condition.rollout_percentage
212
+
213
+ if ((condition.properties || []).length > 0) {
214
+ const matchAll = condition.properties.every((property) => {
215
+ return matchProperty(property, properties)
216
+ })
217
+ if (!matchAll) {
218
+ return false
219
+ } else if (rolloutPercentage == undefined) {
220
+ // == to include `null` as a match, not just `undefined`
221
+ return true
222
+ }
223
+ }
224
+
225
+ if (rolloutPercentage != undefined && _hash(flag.key, distinctId) > rolloutPercentage / 100.0) {
226
+ return false
227
+ }
228
+
229
+ return true
230
+ }
231
+
232
+ getMatchingVariant(flag: PostHogFeatureFlag, distinctId: string): string | boolean | undefined {
233
+ const hashValue = _hash(flag.key, distinctId, 'variant')
234
+ const matchingVariant = this.variantLookupTable(flag).find((variant) => {
235
+ return hashValue >= variant.valueMin && hashValue < variant.valueMax
236
+ })
237
+
238
+ if (matchingVariant) {
239
+ return matchingVariant.key
240
+ }
241
+ return undefined
242
+ }
243
+
244
+ variantLookupTable(flag: PostHogFeatureFlag): { valueMin: number; valueMax: number; key: string }[] {
245
+ const lookupTable: { valueMin: number; valueMax: number; key: string }[] = []
246
+ let valueMin = 0
247
+ let valueMax = 0
248
+ const flagFilters = flag.filters || {}
249
+ const multivariates: {
250
+ key: string
251
+ rollout_percentage: number
252
+ }[] = flagFilters.multivariate?.variants || []
253
+
254
+ multivariates.forEach((variant) => {
255
+ valueMax = valueMin + variant.rollout_percentage / 100.0
256
+ lookupTable.push({ valueMin, valueMax, key: variant.key })
257
+ valueMin = valueMax
258
+ })
259
+ return lookupTable
260
+ }
261
+
262
+ async loadFeatureFlags(forceReload = false): Promise<void> {
263
+ if (!this.loadedSuccessfullyOnce || forceReload) {
264
+ await this._loadFeatureFlags()
265
+ }
266
+ }
267
+
268
+ async _loadFeatureFlags(): Promise<void> {
269
+ if (this.poller) {
270
+ clearTimeout(this.poller)
271
+ this.poller = undefined
272
+ }
273
+ this.poller = setTimeout(() => this._loadFeatureFlags(), this.pollingInterval)
274
+
275
+ try {
276
+ const res = await this._requestFeatureFlagDefinitions()
277
+
278
+ if (res && res.statusCode === 401) {
279
+ throw new ClientError(
280
+ `Your personalApiKey is invalid. Are you sure you're not using your Project API key? More information: https://posthog.com/docs/api/overview`
281
+ )
282
+ }
283
+ const responseJson = await res.body.json()
284
+ if (!('flags' in responseJson)) {
285
+ console.error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`)
286
+ }
287
+
288
+ this.featureFlags = responseJson.flags || []
289
+ this.groupTypeMapping = responseJson.group_type_mapping || {}
290
+ this.loadedSuccessfullyOnce = true
291
+ } catch (err) {
292
+ // if an error that is not an instance of ClientError is thrown
293
+ // we silently ignore the error when reloading feature flags
294
+ if (err instanceof ClientError) {
295
+ throw err
296
+ }
297
+ }
298
+ }
299
+
300
+ async _requestFeatureFlagDefinitions(): Promise<ResponseData> {
301
+ 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
+
308
+ const options: Parameters<typeof request>[1] = {
309
+ method: 'GET',
310
+ headers: headers,
311
+ }
312
+
313
+ if (this.timeout && typeof this.timeout === 'number') {
314
+ options.bodyTimeout = this.timeout
315
+ }
316
+
317
+ let res
318
+ try {
319
+ res = await request(url, options)
320
+ } catch (err) {
321
+ throw new Error(`Request failed with error: ${err}`)
322
+ }
323
+
324
+ return res
325
+ }
326
+
327
+ stopPoller(): void {
328
+ clearTimeout(this.poller)
329
+ }
330
+ }
331
+
332
+ // # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
333
+ // # Given the same distinct_id and key, it'll always return the same float. These floats are
334
+ // # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
335
+ // # we can do _hash(key, distinct_id) < 0.2
336
+ function _hash(key: string, distinctId: string, salt: string = ''): number {
337
+ const sha1Hash = createHash('sha1')
338
+ sha1Hash.update(`${key}.${distinctId}${salt}`)
339
+ return parseInt(sha1Hash.digest('hex').slice(0, 15), 16) / LONG_SCALE
340
+ }
341
+
342
+ function matchProperty(
343
+ property: FeatureFlagCondition['properties'][number],
344
+ propertyValues: Record<string, any>
345
+ ): boolean {
346
+ const key = property.key
347
+ const value = property.value
348
+ const operator = property.operator || 'exact'
349
+
350
+ if (!(key in propertyValues)) {
351
+ throw new InconclusiveMatchError(`Property ${key} not found in propertyValues`)
352
+ } else if (operator === 'is_not_set') {
353
+ throw new InconclusiveMatchError(`Operator is_not_set is not supported`)
354
+ }
355
+
356
+ const overrideValue = propertyValues[key]
357
+
358
+ switch (operator) {
359
+ case 'exact':
360
+ return Array.isArray(value) ? value.indexOf(overrideValue) !== -1 : value === overrideValue
361
+ case 'is_not':
362
+ return Array.isArray(value) ? value.indexOf(overrideValue) === -1 : value !== overrideValue
363
+ case 'is_set':
364
+ return key in propertyValues
365
+ case 'icontains':
366
+ return String(overrideValue).toLowerCase().includes(String(value).toLowerCase())
367
+ case 'not_icontains':
368
+ return !String(overrideValue).toLowerCase().includes(String(value).toLowerCase())
369
+ case 'regex':
370
+ return isValidRegex(String(value)) && String(overrideValue).match(String(value)) !== null
371
+ case 'not_regex':
372
+ return isValidRegex(String(value)) && String(overrideValue).match(String(value)) === null
373
+ case 'gt':
374
+ return typeof overrideValue == typeof value && overrideValue > value
375
+ case 'gte':
376
+ return typeof overrideValue == typeof value && overrideValue >= value
377
+ case 'lt':
378
+ return typeof overrideValue == typeof value && overrideValue < value
379
+ case 'lte':
380
+ return typeof overrideValue == typeof value && overrideValue <= value
381
+ default:
382
+ console.error(`Unknown operator: ${operator}`)
383
+ return false
384
+ }
385
+ }
386
+
387
+ function isValidRegex(regex: string): boolean {
388
+ try {
389
+ new RegExp(regex)
390
+ return true
391
+ } catch (err) {
392
+ return false
393
+ }
394
+ }
395
+
396
+ export { FeatureFlagsPoller, matchProperty, InconclusiveMatchError, ClientError }