posthog-node 2.0.0-alpha8 → 2.0.2

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.
@@ -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 }
@@ -9,17 +9,29 @@ import {
9
9
  } from '../../posthog-core/src'
10
10
  import { PostHogMemoryStorage } from '../../posthog-core/src/storage-memory'
11
11
  import { EventMessageV1, GroupIdentifyMessage, IdentifyMessageV1, PostHogNodeV1 } from './types'
12
+ import { FeatureFlagsPoller } from './feature-flags'
12
13
 
13
14
  export type PostHogOptions = PosthogCoreOptions & {
14
15
  persistence?: 'memory'
16
+ personalApiKey?: string
17
+ // The interval in milliseconds between polls for refreshing feature flag definitions
18
+ featureFlagsPollingInterval?: number
19
+ // Timeout in milliseconds for feature flag definitions calls. Defaults to 30 seconds.
20
+ requestTimeout?: number
21
+ // Maximum size of cache that deduplicates $feature_flag_called calls per user.
22
+ maxCacheSize?: number
15
23
  }
16
24
 
17
- class PostHog extends PostHogCore {
25
+ const THIRTY_SECONDS = 30 * 1000
26
+ const MAX_CACHE_SIZE = 50 * 1000
27
+
28
+ class PostHogClient extends PostHogCore {
18
29
  private _memoryStorage = new PostHogMemoryStorage()
19
30
 
20
31
  constructor(apiKey: string, options: PostHogOptions = {}) {
21
32
  options.captureMode = options?.captureMode || 'json'
22
33
  options.preloadFeatureFlags = false // Don't preload as this makes no sense without a distinctId
34
+ options.sendFeatureFlagEvent = false // Let `posthog-node` handle this on its own, since we're dealing with multiple distinctIDs
23
35
 
24
36
  super(apiKey, options)
25
37
  }
@@ -53,23 +65,34 @@ class PostHog extends PostHogCore {
53
65
  }
54
66
 
55
67
  // The actual exported Nodejs API.
56
- export class PostHogGlobal implements PostHogNodeV1 {
57
- private _sharedClient: PostHog
68
+ export class PostHog implements PostHogNodeV1 {
69
+ private _sharedClient: PostHogClient
70
+ private featureFlagsPoller?: FeatureFlagsPoller
71
+ private maxCacheSize: number
72
+
73
+ distinctIdHasSentFlagCalls: Record<string, string[]>
58
74
 
59
75
  constructor(apiKey: string, options: PostHogOptions = {}) {
60
- options.decidePollInterval = 0 // Forcefully set to 0 so we don't auto-reload
61
- this._sharedClient = new PostHog(apiKey, options)
76
+ this._sharedClient = new PostHogClient(apiKey, options)
77
+ if (options.personalApiKey) {
78
+ this.featureFlagsPoller = new FeatureFlagsPoller({
79
+ pollingInterval:
80
+ typeof options.featureFlagsPollingInterval === 'number'
81
+ ? options.featureFlagsPollingInterval
82
+ : THIRTY_SECONDS,
83
+ personalApiKey: options.personalApiKey,
84
+ projectApiKey: apiKey,
85
+ timeout: options.requestTimeout,
86
+ host: this._sharedClient.host,
87
+ })
88
+ }
89
+ this.distinctIdHasSentFlagCalls = {}
90
+ this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE
62
91
  }
63
92
 
64
93
  private reInit(distinctId: string): void {
65
- // Certain properties we want to persist
66
- const propertiesToKeep = [PostHogPersistedProperty.Queue, PostHogPersistedProperty.OptedOut]
67
-
68
- for (const key in PostHogPersistedProperty) {
69
- if (!propertiesToKeep.includes(key as any)) {
70
- this._sharedClient.setPersistedProperty((PostHogPersistedProperty as any)[key], null)
71
- }
72
- }
94
+ // Certain properties we want to persist. Queue is persisted always by default.
95
+ this._sharedClient.reset([PostHogPersistedProperty.OptedOut])
73
96
  this._sharedClient.setPersistedProperty(PostHogPersistedProperty.DistinctId, distinctId)
74
97
  }
75
98
 
@@ -81,12 +104,12 @@ export class PostHogGlobal implements PostHogNodeV1 {
81
104
  return this._sharedClient.optOut()
82
105
  }
83
106
 
84
- capture({ distinctId, event, properties, groups }: EventMessageV1): void {
107
+ capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void {
85
108
  this.reInit(distinctId)
86
109
  if (groups) {
87
110
  this._sharedClient.groups(groups)
88
111
  }
89
- this._sharedClient.capture(event, properties)
112
+ this._sharedClient.capture(event, properties, sendFeatureFlags || false)
90
113
  }
91
114
 
92
115
  identify({ distinctId, properties }: IdentifyMessageV1): void {
@@ -102,39 +125,170 @@ export class PostHogGlobal implements PostHogNodeV1 {
102
125
  async getFeatureFlag(
103
126
  key: string,
104
127
  distinctId: string,
105
- groups?: Record<string, string> | undefined
128
+ options?: {
129
+ groups?: Record<string, string>
130
+ personProperties?: Record<string, string>
131
+ groupProperties?: Record<string, Record<string, string>>
132
+ onlyEvaluateLocally?: boolean
133
+ sendFeatureFlagEvents?: boolean
134
+ }
106
135
  ): Promise<string | boolean | undefined> {
107
- this.reInit(distinctId)
108
- if (groups) {
109
- this._sharedClient.groups(groups)
136
+ const { groups, personProperties, groupProperties } = options || {}
137
+ let { onlyEvaluateLocally, sendFeatureFlagEvents } = options || {}
138
+
139
+ // set defaults
140
+ if (onlyEvaluateLocally == undefined) {
141
+ onlyEvaluateLocally = false
142
+ }
143
+ if (sendFeatureFlagEvents == undefined) {
144
+ sendFeatureFlagEvents = true
145
+ }
146
+
147
+ let response = await this.featureFlagsPoller?.getFeatureFlag(
148
+ key,
149
+ distinctId,
150
+ groups,
151
+ personProperties,
152
+ groupProperties
153
+ )
154
+
155
+ const flagWasLocallyEvaluated = response !== undefined
156
+
157
+ if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) {
158
+ this.reInit(distinctId)
159
+ if (groups != undefined) {
160
+ this._sharedClient.groups(groups)
161
+ }
162
+
163
+ if (personProperties) {
164
+ this._sharedClient.personProperties(personProperties)
165
+ }
166
+
167
+ if (groupProperties) {
168
+ this._sharedClient.groupProperties(groupProperties)
169
+ }
170
+ await this._sharedClient.reloadFeatureFlagsAsync(false)
171
+ response = this._sharedClient.getFeatureFlag(key)
172
+ }
173
+
174
+ const featureFlagReportedKey = `${key}_${response}`
175
+
176
+ if (
177
+ sendFeatureFlagEvents &&
178
+ (!(distinctId in this.distinctIdHasSentFlagCalls) ||
179
+ !this.distinctIdHasSentFlagCalls[distinctId].includes(featureFlagReportedKey))
180
+ ) {
181
+ if (Object.keys(this.distinctIdHasSentFlagCalls).length >= this.maxCacheSize) {
182
+ this.distinctIdHasSentFlagCalls = {}
183
+ }
184
+ if (Array.isArray(this.distinctIdHasSentFlagCalls[distinctId])) {
185
+ this.distinctIdHasSentFlagCalls[distinctId].push(featureFlagReportedKey)
186
+ } else {
187
+ this.distinctIdHasSentFlagCalls[distinctId] = [featureFlagReportedKey]
188
+ }
189
+ this.capture({
190
+ distinctId,
191
+ event: '$feature_flag_called',
192
+ properties: {
193
+ $feature_flag: key,
194
+ $feature_flag_response: response,
195
+ locally_evaluated: flagWasLocallyEvaluated,
196
+ },
197
+ groups,
198
+ })
110
199
  }
111
- await this._sharedClient.reloadFeatureFlagsAsync()
112
- return this._sharedClient.getFeatureFlag(key)
200
+ return response
113
201
  }
114
202
 
115
203
  async isFeatureEnabled(
116
204
  key: string,
117
205
  distinctId: string,
118
- defaultResult?: boolean | undefined,
119
- groups?: Record<string, string> | undefined
120
- ): Promise<boolean> {
121
- const feat = await this.getFeatureFlag(key, distinctId, groups)
122
- return !!feat || defaultResult || false
206
+ options?: {
207
+ groups?: Record<string, string>
208
+ personProperties?: Record<string, string>
209
+ groupProperties?: Record<string, Record<string, string>>
210
+ onlyEvaluateLocally?: boolean
211
+ sendFeatureFlagEvents?: boolean
212
+ }
213
+ ): Promise<boolean | undefined> {
214
+ const feat = await this.getFeatureFlag(key, distinctId, options)
215
+ if (feat === undefined) {
216
+ return undefined
217
+ }
218
+ return !!feat || false
219
+ }
220
+
221
+ async getAllFlags(
222
+ distinctId: string,
223
+ options?: {
224
+ groups?: Record<string, string>
225
+ personProperties?: Record<string, string>
226
+ groupProperties?: Record<string, Record<string, string>>
227
+ onlyEvaluateLocally?: boolean
228
+ }
229
+ ): Promise<Record<string, string | boolean>> {
230
+ const { groups, personProperties, groupProperties } = options || {}
231
+ let { onlyEvaluateLocally } = options || {}
232
+
233
+ // set defaults
234
+ if (onlyEvaluateLocally == undefined) {
235
+ onlyEvaluateLocally = false
236
+ }
237
+
238
+ const localEvaluationResult = await this.featureFlagsPoller?.getAllFlags(
239
+ distinctId,
240
+ groups,
241
+ personProperties,
242
+ groupProperties
243
+ )
244
+
245
+ let response = {}
246
+ let fallbackToDecide = true
247
+ if (localEvaluationResult) {
248
+ response = localEvaluationResult.response
249
+ fallbackToDecide = localEvaluationResult.fallbackToDecide
250
+ }
251
+
252
+ if (fallbackToDecide && !onlyEvaluateLocally) {
253
+ this.reInit(distinctId)
254
+ if (groups) {
255
+ this._sharedClient.groups(groups)
256
+ }
257
+
258
+ if (personProperties) {
259
+ this._sharedClient.personProperties(personProperties)
260
+ }
261
+
262
+ if (groupProperties) {
263
+ this._sharedClient.groupProperties(groupProperties)
264
+ }
265
+ await this._sharedClient.reloadFeatureFlagsAsync(false)
266
+ const remoteEvaluationResult = this._sharedClient.getFeatureFlags()
267
+
268
+ return { ...response, ...remoteEvaluationResult }
269
+ }
270
+
271
+ return response
123
272
  }
124
273
 
125
274
  groupIdentify({ groupType, groupKey, properties }: GroupIdentifyMessage): void {
126
275
  this._sharedClient.groupIdentify(groupType, groupKey, properties)
127
276
  }
128
277
 
129
- reloadFeatureFlags(): Promise<void> {
130
- throw new Error('Method not implemented.')
278
+ async reloadFeatureFlags(): Promise<void> {
279
+ await this.featureFlagsPoller?.loadFeatureFlags(true)
280
+ }
281
+
282
+ flush(): void {
283
+ this._sharedClient.flush()
131
284
  }
132
285
 
133
286
  shutdown(): void {
134
- void this._sharedClient.shutdownAsync()
287
+ void this.shutdownAsync()
135
288
  }
136
289
 
137
- shutdownAsync(): Promise<void> {
290
+ async shutdownAsync(): Promise<void> {
291
+ this.featureFlagsPoller?.stopPoller()
138
292
  return this._sharedClient.shutdownAsync()
139
293
  }
140
294