posthog-node
Advanced tools
Comparing version 2.0.0-alpha9 to 2.0.1
@@ -38,3 +38,3 @@ declare type PosthogCoreOptions = { | ||
* @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. | ||
* @param sendFeatureFlags OPTIONAL | Used with experiments | ||
* @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event. | ||
*/ | ||
@@ -70,11 +70,44 @@ capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void; | ||
* features on and off for different user groups or individual users. | ||
* IMPORTANT: To use this method, you need to specify `personalApiKey` in your config! More info: https://posthog.com/docs/api/overview | ||
* @param key the unique key of your feature flag | ||
* @param distinctId the current unique id | ||
* @param defaultResult optional - default value to be returned if the feature flag is not on for the user | ||
* @param groups optional - what groups are currently active (group analytics) | ||
* @param options: dict with optional parameters below | ||
* @param groups optional - what groups are currently active (group analytics). Required if the flag depends on groups. | ||
* @param personProperties optional - what person properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param groupProperties optional - what group properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false. | ||
* @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true. | ||
* | ||
* @returns true if the flag is on, false if the flag is off, undefined if there was an error. | ||
*/ | ||
isFeatureEnabled(key: string, distinctId: string, defaultResult?: boolean, groups?: Record<string, string>): Promise<boolean>; | ||
getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string>): Promise<string | boolean | undefined>; | ||
isFeatureEnabled(key: string, distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
sendFeatureFlagEvents?: boolean; | ||
}): Promise<boolean | undefined>; | ||
/** | ||
* @description PostHog feature flags (https://posthog.com/docs/features/feature-flags) | ||
* allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog, | ||
* you can use this method to check if the flag is on for a given user, allowing you to create logic to turn | ||
* features on and off for different user groups or individual users. | ||
* @param key the unique key of your feature flag | ||
* @param distinctId the current unique id | ||
* @param options: dict with optional parameters below | ||
* @param groups optional - what groups are currently active (group analytics). Required if the flag depends on groups. | ||
* @param personProperties optional - what person properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param groupProperties optional - what group properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false. | ||
* @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true. | ||
* | ||
* @returns true or string(for multivariates) if the flag is on, false if the flag is off, undefined if there was an error. | ||
*/ | ||
getFeatureFlag(key: string, distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
sendFeatureFlagEvents?: boolean; | ||
}): Promise<string | boolean | undefined>; | ||
/** | ||
* @description Sets a groups properties, which allows asking questions like "Who are the most active companies" | ||
@@ -102,5 +135,12 @@ * using my product in PostHog. | ||
persistence?: 'memory'; | ||
personalApiKey?: string; | ||
featureFlagsPollingInterval?: number; | ||
requestTimeout?: number; | ||
maxCacheSize?: number; | ||
}; | ||
declare class PostHogGlobal implements PostHogNodeV1 { | ||
private _sharedClient; | ||
private featureFlagsPoller?; | ||
private maxCacheSize; | ||
distinctIdHasSentFlagCalls: Record<string, string[]>; | ||
constructor(apiKey: string, options?: PostHogOptions); | ||
@@ -110,3 +150,3 @@ private reInit; | ||
disable(): void; | ||
capture({ distinctId, event, properties, groups }: EventMessageV1): void; | ||
capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void; | ||
identify({ distinctId, properties }: IdentifyMessageV1): void; | ||
@@ -117,6 +157,25 @@ alias(data: { | ||
}): void; | ||
getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string> | undefined): Promise<string | boolean | undefined>; | ||
isFeatureEnabled(key: string, distinctId: string, defaultResult?: boolean | undefined, groups?: Record<string, string> | undefined): Promise<boolean>; | ||
getFeatureFlag(key: string, distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
sendFeatureFlagEvents?: boolean; | ||
}): Promise<string | boolean | undefined>; | ||
isFeatureEnabled(key: string, distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
sendFeatureFlagEvents?: boolean; | ||
}): Promise<boolean | undefined>; | ||
getAllFlags(distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
}): Promise<Record<string, string | boolean>>; | ||
groupIdentify({ groupType, groupKey, properties }: GroupIdentifyMessage): void; | ||
reloadFeatureFlags(): Promise<void>; | ||
flush(): void; | ||
shutdown(): void; | ||
@@ -123,0 +182,0 @@ shutdownAsync(): Promise<void>; |
@@ -8,3 +8,3 @@ import { PostHogFetchOptions, PostHogFetchResponse, PostHogAutocaptureElement, PostHogDecideResponse, PosthogCoreOptions, PostHogEventProperties, PostHogPersistedProperty } from './types'; | ||
private apiKey; | ||
private host; | ||
host: string; | ||
private flushAt; | ||
@@ -32,2 +32,3 @@ private flushInterval; | ||
private set props(value); | ||
private clearProps; | ||
private _props; | ||
@@ -38,3 +39,3 @@ get optedOut(): boolean; | ||
on(event: string, cb: (...args: any[]) => void): () => void; | ||
reset(): void; | ||
reset(propertiesToKeep?: PostHogPersistedProperty[]): void; | ||
debug(enabled?: boolean): void; | ||
@@ -56,3 +57,3 @@ private buildPayload; | ||
[key: string]: any; | ||
}): this; | ||
}, forceSendFeatureFlags?: boolean): this; | ||
alias(alias: string): this; | ||
@@ -69,2 +70,11 @@ autocapture(eventType: string, elements: PostHogAutocaptureElement[], properties?: PostHogEventProperties): this; | ||
/*** | ||
* PROPERTIES | ||
***/ | ||
personProperties(properties: { | ||
[type: string]: string; | ||
}): this; | ||
groupProperties(properties: { | ||
[type: string]: Record<string, string>; | ||
}): this; | ||
/*** | ||
*** FEATURE FLAGS | ||
@@ -74,9 +84,12 @@ ***/ | ||
private _decideAsync; | ||
getFeatureFlag(key: string, defaultResult?: string | boolean): boolean | string | undefined; | ||
getFeatureFlag(key: string): boolean | string | undefined; | ||
getFeatureFlags(): PostHogDecideResponse['featureFlags'] | undefined; | ||
isFeatureEnabled(key: string, defaultResult?: boolean): boolean; | ||
reloadFeatureFlagsAsync(): Promise<PostHogDecideResponse['featureFlags']>; | ||
isFeatureEnabled(key: string): boolean | undefined; | ||
reloadFeatureFlagsAsync(sendAnonDistinctId?: boolean): Promise<PostHogDecideResponse['featureFlags']>; | ||
onFeatureFlags(cb: (flags: PostHogDecideResponse['featureFlags']) => void): () => void; | ||
onFeatureFlag(key: string, cb: (value: string | boolean) => void): () => void; | ||
overrideFeatureFlag(flags: PostHogDecideResponse['featureFlags'] | null): void; | ||
_sendFeatureFlags(event: string, properties?: { | ||
[key: string]: any; | ||
}): void; | ||
/*** | ||
@@ -83,0 +96,0 @@ *** QUEUEING AND FLUSHING |
@@ -22,3 +22,5 @@ export declare type PosthogCoreOptions = { | ||
SessionId = "session_id", | ||
SessionLastTimestamp = "session_timestamp" | ||
SessionLastTimestamp = "session_timestamp", | ||
PersonProperties = "person_properties", | ||
GroupProperties = "group_properties" | ||
} | ||
@@ -25,0 +27,0 @@ export declare type PostHogFetchOptions = { |
@@ -5,5 +5,12 @@ import { PosthogCoreOptions } from '../../posthog-core/src'; | ||
persistence?: 'memory'; | ||
personalApiKey?: string; | ||
featureFlagsPollingInterval?: number; | ||
requestTimeout?: number; | ||
maxCacheSize?: number; | ||
}; | ||
export declare class PostHogGlobal implements PostHogNodeV1 { | ||
private _sharedClient; | ||
private featureFlagsPoller?; | ||
private maxCacheSize; | ||
distinctIdHasSentFlagCalls: Record<string, string[]>; | ||
constructor(apiKey: string, options?: PostHogOptions); | ||
@@ -13,3 +20,3 @@ private reInit; | ||
disable(): void; | ||
capture({ distinctId, event, properties, groups }: EventMessageV1): void; | ||
capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void; | ||
identify({ distinctId, properties }: IdentifyMessageV1): void; | ||
@@ -20,6 +27,25 @@ alias(data: { | ||
}): void; | ||
getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string> | undefined): Promise<string | boolean | undefined>; | ||
isFeatureEnabled(key: string, distinctId: string, defaultResult?: boolean | undefined, groups?: Record<string, string> | undefined): Promise<boolean>; | ||
getFeatureFlag(key: string, distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
sendFeatureFlagEvents?: boolean; | ||
}): Promise<string | boolean | undefined>; | ||
isFeatureEnabled(key: string, distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
sendFeatureFlagEvents?: boolean; | ||
}): Promise<boolean | undefined>; | ||
getAllFlags(distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
}): Promise<Record<string, string | boolean>>; | ||
groupIdentify({ groupType, groupKey, properties }: GroupIdentifyMessage): void; | ||
reloadFeatureFlags(): Promise<void>; | ||
flush(): void; | ||
shutdown(): void; | ||
@@ -26,0 +52,0 @@ shutdownAsync(): Promise<void>; |
@@ -15,2 +15,32 @@ export interface IdentifyMessageV1 { | ||
} | ||
export declare type FeatureFlagCondition = { | ||
properties: { | ||
key: string; | ||
type?: string; | ||
value: string | number | (string | number)[]; | ||
operator?: string; | ||
}[]; | ||
rollout_percentage?: number; | ||
}; | ||
export declare type PostHogFeatureFlag = { | ||
id: number; | ||
name: string; | ||
key: string; | ||
filters?: { | ||
aggregation_group_type_index?: number; | ||
groups?: FeatureFlagCondition[]; | ||
multivariate?: { | ||
variants: { | ||
key: string; | ||
rollout_percentage: number; | ||
}[]; | ||
}; | ||
}; | ||
deleted: boolean; | ||
active: boolean; | ||
is_simple_flag: boolean; | ||
rollout_percentage: null | number; | ||
ensure_experience_continuity: boolean; | ||
experiment_set: number[]; | ||
}; | ||
export declare type PostHogNodeV1 = { | ||
@@ -26,3 +56,3 @@ /** | ||
* @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. | ||
* @param sendFeatureFlags OPTIONAL | Used with experiments | ||
* @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event. | ||
*/ | ||
@@ -58,11 +88,44 @@ capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void; | ||
* features on and off for different user groups or individual users. | ||
* IMPORTANT: To use this method, you need to specify `personalApiKey` in your config! More info: https://posthog.com/docs/api/overview | ||
* @param key the unique key of your feature flag | ||
* @param distinctId the current unique id | ||
* @param defaultResult optional - default value to be returned if the feature flag is not on for the user | ||
* @param groups optional - what groups are currently active (group analytics) | ||
* @param options: dict with optional parameters below | ||
* @param groups optional - what groups are currently active (group analytics). Required if the flag depends on groups. | ||
* @param personProperties optional - what person properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param groupProperties optional - what group properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false. | ||
* @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true. | ||
* | ||
* @returns true if the flag is on, false if the flag is off, undefined if there was an error. | ||
*/ | ||
isFeatureEnabled(key: string, distinctId: string, defaultResult?: boolean, groups?: Record<string, string>): Promise<boolean>; | ||
getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string>): Promise<string | boolean | undefined>; | ||
isFeatureEnabled(key: string, distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
sendFeatureFlagEvents?: boolean; | ||
}): Promise<boolean | undefined>; | ||
/** | ||
* @description PostHog feature flags (https://posthog.com/docs/features/feature-flags) | ||
* allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog, | ||
* you can use this method to check if the flag is on for a given user, allowing you to create logic to turn | ||
* features on and off for different user groups or individual users. | ||
* @param key the unique key of your feature flag | ||
* @param distinctId the current unique id | ||
* @param options: dict with optional parameters below | ||
* @param groups optional - what groups are currently active (group analytics). Required if the flag depends on groups. | ||
* @param personProperties optional - what person properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param groupProperties optional - what group properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false. | ||
* @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true. | ||
* | ||
* @returns true or string(for multivariates) if the flag is on, false if the flag is off, undefined if there was an error. | ||
*/ | ||
getFeatureFlag(key: string, distinctId: string, options?: { | ||
groups?: Record<string, string>; | ||
personProperties?: Record<string, string>; | ||
groupProperties?: Record<string, Record<string, string>>; | ||
onlyEvaluateLocally?: boolean; | ||
sendFeatureFlagEvents?: boolean; | ||
}): Promise<string | boolean | undefined>; | ||
/** | ||
* @description Sets a groups properties, which allows asking questions like "Who are the most active companies" | ||
@@ -69,0 +132,0 @@ * using my product in PostHog. |
{ | ||
"name": "posthog-node", | ||
"version": "2.0.0-alpha9", | ||
"version": "2.0.1", | ||
"description": "PostHog Node.js integration", | ||
@@ -5,0 +5,0 @@ "repository": "PostHog/posthog-node", |
# PostHog Node.js | ||
> 🚧 This is a WIP. Currently the only officially supported way of using PostHog on the web is [posthog-node](https://github.com/PostHog/posthog-node) | ||
Please see the main [PostHog docs](https://www.posthog.com/docs). | ||
@@ -6,0 +4,0 @@ |
@@ -12,7 +12,18 @@ import { version } from '../package.json' | ||
import { EventMessageV1, GroupIdentifyMessage, IdentifyMessageV1, PostHogNodeV1 } from './types' | ||
import { FeatureFlagsPoller } from './feature-flags' | ||
export type PostHogOptions = PosthogCoreOptions & { | ||
persistence?: 'memory' | ||
personalApiKey?: string | ||
// The interval in milliseconds between polls for refreshing feature flag definitions | ||
featureFlagsPollingInterval?: number | ||
// Timeout in milliseconds for feature flag definitions calls. Defaults to 30 seconds. | ||
requestTimeout?: number | ||
// Maximum size of cache that deduplicates $feature_flag_called calls per user. | ||
maxCacheSize?: number | ||
} | ||
const THIRTY_SECONDS = 30 * 1000 | ||
const MAX_CACHE_SIZE = 50 * 1000 | ||
class PostHog extends PostHogCore { | ||
@@ -24,2 +35,3 @@ private _memoryStorage = new PostHogMemoryStorage() | ||
options.preloadFeatureFlags = false // Don't preload as this makes no sense without a distinctId | ||
options.sendFeatureFlagEvent = false // Let `posthog-node` handle this on its own, since we're dealing with multiple distinctIDs | ||
@@ -60,16 +72,28 @@ super(apiKey, options) | ||
private _sharedClient: PostHog | ||
private featureFlagsPoller?: FeatureFlagsPoller | ||
private maxCacheSize: number | ||
distinctIdHasSentFlagCalls: Record<string, string[]> | ||
constructor(apiKey: string, options: PostHogOptions = {}) { | ||
this._sharedClient = new PostHog(apiKey, options) | ||
if (options.personalApiKey) { | ||
this.featureFlagsPoller = new FeatureFlagsPoller({ | ||
pollingInterval: | ||
typeof options.featureFlagsPollingInterval === 'number' | ||
? options.featureFlagsPollingInterval | ||
: THIRTY_SECONDS, | ||
personalApiKey: options.personalApiKey, | ||
projectApiKey: apiKey, | ||
timeout: options.requestTimeout, | ||
host: this._sharedClient.host, | ||
}) | ||
} | ||
this.distinctIdHasSentFlagCalls = {} | ||
this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE | ||
} | ||
private reInit(distinctId: string): void { | ||
// Certain properties we want to persist | ||
const propertiesToKeep = [PostHogPersistedProperty.Queue, PostHogPersistedProperty.OptedOut] | ||
for (const key in PostHogPersistedProperty) { | ||
if (!propertiesToKeep.includes(key as any)) { | ||
this._sharedClient.setPersistedProperty((PostHogPersistedProperty as any)[key], null) | ||
} | ||
} | ||
// Certain properties we want to persist. Queue is persisted always by default. | ||
this._sharedClient.reset([PostHogPersistedProperty.OptedOut]) | ||
this._sharedClient.setPersistedProperty(PostHogPersistedProperty.DistinctId, distinctId) | ||
@@ -86,3 +110,3 @@ } | ||
capture({ distinctId, event, properties, groups }: EventMessageV1): void { | ||
capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void { | ||
this.reInit(distinctId) | ||
@@ -92,3 +116,3 @@ if (groups) { | ||
} | ||
this._sharedClient.capture(event, properties) | ||
this._sharedClient.capture(event, properties, sendFeatureFlags || false) | ||
} | ||
@@ -109,10 +133,75 @@ | ||
distinctId: string, | ||
groups?: Record<string, string> | undefined | ||
options?: { | ||
groups?: Record<string, string> | ||
personProperties?: Record<string, string> | ||
groupProperties?: Record<string, Record<string, string>> | ||
onlyEvaluateLocally?: boolean | ||
sendFeatureFlagEvents?: boolean | ||
} | ||
): Promise<string | boolean | undefined> { | ||
this.reInit(distinctId) | ||
if (groups) { | ||
this._sharedClient.groups(groups) | ||
const { groups, personProperties, groupProperties } = options || {} | ||
let { onlyEvaluateLocally, sendFeatureFlagEvents } = options || {} | ||
// set defaults | ||
if (onlyEvaluateLocally == undefined) { | ||
onlyEvaluateLocally = false | ||
} | ||
await this._sharedClient.reloadFeatureFlagsAsync() | ||
return this._sharedClient.getFeatureFlag(key) | ||
if (sendFeatureFlagEvents == undefined) { | ||
sendFeatureFlagEvents = true | ||
} | ||
let response = await this.featureFlagsPoller?.getFeatureFlag( | ||
key, | ||
distinctId, | ||
groups, | ||
personProperties, | ||
groupProperties | ||
) | ||
const flagWasLocallyEvaluated = response !== undefined | ||
if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) { | ||
this.reInit(distinctId) | ||
if (groups != undefined) { | ||
this._sharedClient.groups(groups) | ||
} | ||
if (personProperties) { | ||
this._sharedClient.personProperties(personProperties) | ||
} | ||
if (groupProperties) { | ||
this._sharedClient.groupProperties(groupProperties) | ||
} | ||
await this._sharedClient.reloadFeatureFlagsAsync(false) | ||
response = this._sharedClient.getFeatureFlag(key) | ||
} | ||
const featureFlagReportedKey = `${key}_${response}` | ||
if ( | ||
sendFeatureFlagEvents && | ||
(!(distinctId in this.distinctIdHasSentFlagCalls) || | ||
!this.distinctIdHasSentFlagCalls[distinctId].includes(featureFlagReportedKey)) | ||
) { | ||
if (Object.keys(this.distinctIdHasSentFlagCalls).length >= this.maxCacheSize) { | ||
this.distinctIdHasSentFlagCalls = {} | ||
} | ||
if (Array.isArray(this.distinctIdHasSentFlagCalls[distinctId])) { | ||
this.distinctIdHasSentFlagCalls[distinctId].push(featureFlagReportedKey) | ||
} else { | ||
this.distinctIdHasSentFlagCalls[distinctId] = [featureFlagReportedKey] | ||
} | ||
this.capture({ | ||
distinctId, | ||
event: '$feature_flag_called', | ||
properties: { | ||
$feature_flag: key, | ||
$feature_flag_response: response, | ||
locally_evaluated: flagWasLocallyEvaluated, | ||
}, | ||
groups, | ||
}) | ||
} | ||
return response | ||
} | ||
@@ -123,9 +212,70 @@ | ||
distinctId: string, | ||
defaultResult?: boolean | undefined, | ||
groups?: Record<string, string> | undefined | ||
): Promise<boolean> { | ||
const feat = await this.getFeatureFlag(key, distinctId, groups) | ||
return !!feat || defaultResult || false | ||
options?: { | ||
groups?: Record<string, string> | ||
personProperties?: Record<string, string> | ||
groupProperties?: Record<string, Record<string, string>> | ||
onlyEvaluateLocally?: boolean | ||
sendFeatureFlagEvents?: boolean | ||
} | ||
): Promise<boolean | undefined> { | ||
const feat = await this.getFeatureFlag(key, distinctId, options) | ||
if (feat === undefined) { | ||
return undefined | ||
} | ||
return !!feat || false | ||
} | ||
async getAllFlags( | ||
distinctId: string, | ||
options?: { | ||
groups?: Record<string, string> | ||
personProperties?: Record<string, string> | ||
groupProperties?: Record<string, Record<string, string>> | ||
onlyEvaluateLocally?: boolean | ||
} | ||
): Promise<Record<string, string | boolean>> { | ||
const { groups, personProperties, groupProperties } = options || {} | ||
let { onlyEvaluateLocally } = options || {} | ||
// set defaults | ||
if (onlyEvaluateLocally == undefined) { | ||
onlyEvaluateLocally = false | ||
} | ||
const localEvaluationResult = await this.featureFlagsPoller?.getAllFlags( | ||
distinctId, | ||
groups, | ||
personProperties, | ||
groupProperties | ||
) | ||
let response = {} | ||
let fallbackToDecide = true | ||
if (localEvaluationResult) { | ||
response = localEvaluationResult.response | ||
fallbackToDecide = localEvaluationResult.fallbackToDecide | ||
} | ||
if (fallbackToDecide && !onlyEvaluateLocally) { | ||
this.reInit(distinctId) | ||
if (groups) { | ||
this._sharedClient.groups(groups) | ||
} | ||
if (personProperties) { | ||
this._sharedClient.personProperties(personProperties) | ||
} | ||
if (groupProperties) { | ||
this._sharedClient.groupProperties(groupProperties) | ||
} | ||
await this._sharedClient.reloadFeatureFlagsAsync(false) | ||
const remoteEvaluationResult = this._sharedClient.getFeatureFlags() | ||
return { ...response, ...remoteEvaluationResult } | ||
} | ||
return response | ||
} | ||
groupIdentify({ groupType, groupKey, properties }: GroupIdentifyMessage): void { | ||
@@ -135,11 +285,16 @@ this._sharedClient.groupIdentify(groupType, groupKey, properties) | ||
reloadFeatureFlags(): Promise<void> { | ||
throw new Error('Method not implemented.') | ||
async reloadFeatureFlags(): Promise<void> { | ||
await this.featureFlagsPoller?.loadFeatureFlags(true) | ||
} | ||
flush(): void { | ||
this._sharedClient.flush() | ||
} | ||
shutdown(): void { | ||
void this._sharedClient.shutdownAsync() | ||
void this.shutdownAsync() | ||
} | ||
shutdownAsync(): Promise<void> { | ||
async shutdownAsync(): Promise<void> { | ||
this.featureFlagsPoller?.stopPoller() | ||
return this._sharedClient.shutdownAsync() | ||
@@ -146,0 +301,0 @@ } |
@@ -18,2 +18,34 @@ export interface IdentifyMessageV1 { | ||
export type FeatureFlagCondition = { | ||
properties: { | ||
key: string | ||
type?: string | ||
value: string | number | (string | number)[] | ||
operator?: string | ||
}[] | ||
rollout_percentage?: number | ||
} | ||
export type PostHogFeatureFlag = { | ||
id: number | ||
name: string | ||
key: string | ||
filters?: { | ||
aggregation_group_type_index?: number | ||
groups?: FeatureFlagCondition[] | ||
multivariate?: { | ||
variants: { | ||
key: string | ||
rollout_percentage: number | ||
}[] | ||
} | ||
} | ||
deleted: boolean | ||
active: boolean | ||
is_simple_flag: boolean | ||
rollout_percentage: null | number | ||
ensure_experience_continuity: boolean | ||
experiment_set: number[] | ||
} | ||
export type PostHogNodeV1 = { | ||
@@ -29,3 +61,3 @@ /** | ||
* @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. | ||
* @param sendFeatureFlags OPTIONAL | Used with experiments | ||
* @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event. | ||
*/ | ||
@@ -61,7 +93,12 @@ capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessageV1): void | ||
* features on and off for different user groups or individual users. | ||
* IMPORTANT: To use this method, you need to specify `personalApiKey` in your config! More info: https://posthog.com/docs/api/overview | ||
* @param key the unique key of your feature flag | ||
* @param distinctId the current unique id | ||
* @param defaultResult optional - default value to be returned if the feature flag is not on for the user | ||
* @param groups optional - what groups are currently active (group analytics) | ||
* @param options: dict with optional parameters below | ||
* @param groups optional - what groups are currently active (group analytics). Required if the flag depends on groups. | ||
* @param personProperties optional - what person properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param groupProperties optional - what group properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false. | ||
* @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true. | ||
* | ||
* @returns true if the flag is on, false if the flag is off, undefined if there was an error. | ||
*/ | ||
@@ -71,10 +108,37 @@ isFeatureEnabled( | ||
distinctId: string, | ||
defaultResult?: boolean, | ||
groups?: Record<string, string> | ||
): Promise<boolean> | ||
options?: { | ||
groups?: Record<string, string> | ||
personProperties?: Record<string, string> | ||
groupProperties?: Record<string, Record<string, string>> | ||
onlyEvaluateLocally?: boolean | ||
sendFeatureFlagEvents?: boolean | ||
} | ||
): Promise<boolean | undefined> | ||
/** | ||
* @description PostHog feature flags (https://posthog.com/docs/features/feature-flags) | ||
* allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog, | ||
* you can use this method to check if the flag is on for a given user, allowing you to create logic to turn | ||
* features on and off for different user groups or individual users. | ||
* @param key the unique key of your feature flag | ||
* @param distinctId the current unique id | ||
* @param options: dict with optional parameters below | ||
* @param groups optional - what groups are currently active (group analytics). Required if the flag depends on groups. | ||
* @param personProperties optional - what person properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param groupProperties optional - what group properties are known. Used to compute flags locally, if personalApiKey is present. | ||
* @param onlyEvaluateLocally optional - whether to only evaluate the flag locally. Defaults to false. | ||
* @param sendFeatureFlagEvents optional - whether to send feature flag events. Used for Experiments. Defaults to true. | ||
* | ||
* @returns true or string(for multivariates) if the flag is on, false if the flag is off, undefined if there was an error. | ||
*/ | ||
getFeatureFlag( | ||
key: string, | ||
distinctId: string, | ||
groups?: Record<string, string> | ||
options?: { | ||
groups?: Record<string, string> | ||
personProperties?: Record<string, string> | ||
groupProperties?: Record<string, Record<string, string>> | ||
onlyEvaluateLocally?: boolean | ||
sendFeatureFlagEvents?: boolean | ||
} | ||
): Promise<string | boolean | undefined> | ||
@@ -81,0 +145,0 @@ |
@@ -1,5 +0,10 @@ | ||
import PostHog from '../' | ||
// import PostHog from '../' | ||
import { PostHogGlobal as PostHog } from '../src/posthog-node' | ||
jest.mock('undici') | ||
import undici from 'undici' | ||
import { decideImplementation, localEvaluationImplementation } from './feature-flags.spec' | ||
import { waitForPromises } from '../../posthog-core/test/test-utils/test-utils' | ||
jest.mock('../package.json', () => ({ version: '1.2.3' })) | ||
const mockedUndici = jest.mocked(undici, true) | ||
@@ -40,2 +45,7 @@ | ||
afterEach(async () => { | ||
// ensure clean shutdown & no test interdependencies | ||
await posthog.shutdownAsync() | ||
}) | ||
describe('core methods', () => { | ||
@@ -60,2 +70,43 @@ it('should capture an event to shared queue', async () => { | ||
it('shouldnt muddy subsequent capture calls', async () => { | ||
expect(mockedUndici.fetch).toHaveBeenCalledTimes(0) | ||
posthog.capture({ distinctId: '123', event: 'test-event', properties: { foo: 'bar' }, groups: { org: 123 } }) | ||
jest.runOnlyPendingTimers() | ||
expect(getLastBatchEvents()?.[0]).toEqual( | ||
expect.objectContaining({ | ||
distinct_id: '123', | ||
event: 'test-event', | ||
properties: expect.objectContaining({ | ||
$groups: { org: 123 }, | ||
foo: 'bar', | ||
}), | ||
library: 'posthog-node', | ||
library_version: '1.2.3', | ||
}) | ||
) | ||
mockedUndici.fetch.mockClear() | ||
posthog.capture({ | ||
distinctId: '123', | ||
event: 'test-event', | ||
properties: { foo: 'bar' }, | ||
groups: { other_group: 'x' }, | ||
}) | ||
jest.runOnlyPendingTimers() | ||
expect(getLastBatchEvents()?.[0]).toEqual( | ||
expect.objectContaining({ | ||
distinct_id: '123', | ||
event: 'test-event', | ||
properties: expect.objectContaining({ | ||
$groups: { other_group: 'x' }, | ||
foo: 'bar', | ||
}), | ||
library: 'posthog-node', | ||
library_version: '1.2.3', | ||
}) | ||
) | ||
}) | ||
it('should capture identify events on shared queue', async () => { | ||
@@ -103,22 +154,7 @@ expect(mockedUndici.fetch).toHaveBeenCalledTimes(0) | ||
mockedUndici.fetch.mockImplementation((url) => { | ||
if ((url as any).includes('/decide/')) { | ||
return Promise.resolve({ | ||
status: 200, | ||
text: () => Promise.resolve('ok'), | ||
json: () => | ||
Promise.resolve({ | ||
featureFlags: mockFeatureFlags, | ||
}), | ||
}) as any | ||
} | ||
mockedUndici.fetch.mockImplementation(decideImplementation(mockFeatureFlags)) | ||
return Promise.resolve({ | ||
status: 200, | ||
text: () => Promise.resolve('ok'), | ||
json: () => | ||
Promise.resolve({ | ||
status: 'ok', | ||
}), | ||
}) as any | ||
posthog = new PostHog('TEST_API_KEY', { | ||
host: 'http://example.com', | ||
// flushAt: 1, | ||
}) | ||
@@ -129,3 +165,5 @@ }) | ||
expect(mockedUndici.fetch).toHaveBeenCalledTimes(0) | ||
await expect(posthog.getFeatureFlag('feature-variant', '123', { org: '123' })).resolves.toEqual('variant') | ||
await expect(posthog.getFeatureFlag('feature-variant', '123', { groups: { org: '123' } })).resolves.toEqual( | ||
'variant' | ||
) | ||
expect(mockedUndici.fetch).toHaveBeenCalledTimes(1) | ||
@@ -136,7 +174,245 @@ }) | ||
expect(mockedUndici.fetch).toHaveBeenCalledTimes(0) | ||
await expect(posthog.isFeatureEnabled('feature-1', '123', false, { org: '123' })).resolves.toEqual(true) | ||
await expect(posthog.isFeatureEnabled('feature-4', '123', false, { org: '123' })).resolves.toEqual(false) | ||
await expect(posthog.isFeatureEnabled('feature-1', '123', { groups: { org: '123' } })).resolves.toEqual(true) | ||
await expect(posthog.isFeatureEnabled('feature-4', '123', { groups: { org: '123' } })).resolves.toEqual(false) | ||
expect(mockedUndici.fetch).toHaveBeenCalledTimes(2) | ||
}) | ||
it('captures feature flags when no personal API key is present', async () => { | ||
mockedUndici.fetch.mockClear() | ||
mockedUndici.request.mockClear() | ||
expect(mockedUndici.fetch).toHaveBeenCalledTimes(0) | ||
posthog = new PostHog('TEST_API_KEY', { | ||
host: 'http://example.com', | ||
flushAt: 1, | ||
}) | ||
posthog.capture({ | ||
distinctId: 'distinct_id', | ||
event: 'node test event', | ||
sendFeatureFlags: true, | ||
}) | ||
expect(mockedUndici.fetch).toHaveBeenCalledWith( | ||
'http://example.com/decide/?v=2', | ||
expect.objectContaining({ method: 'POST' }) | ||
) | ||
jest.runOnlyPendingTimers() | ||
await waitForPromises() | ||
expect(getLastBatchEvents()?.[0]).toEqual( | ||
expect.objectContaining({ | ||
distinct_id: 'distinct_id', | ||
event: 'node test event', | ||
properties: expect.objectContaining({ | ||
$active_feature_flags: ['feature-1', 'feature-2', 'feature-variant'], | ||
'$feature/feature-1': true, | ||
'$feature/feature-2': true, | ||
'$feature/feature-variant': 'variant', | ||
$lib: 'posthog-node', | ||
$lib_version: '1.2.3', | ||
}), | ||
}) | ||
) | ||
// no calls to `/local_evaluation` | ||
expect(mockedUndici.request).not.toHaveBeenCalled() | ||
}) | ||
it('manages memory well when sending feature flags', async () => { | ||
const flags = { | ||
flags: [ | ||
{ | ||
id: 1, | ||
name: 'Beta Feature', | ||
key: 'beta-feature', | ||
active: true, | ||
filters: { | ||
groups: [ | ||
{ | ||
properties: [], | ||
rollout_percentage: 100, | ||
}, | ||
], | ||
}, | ||
}, | ||
], | ||
} | ||
mockedUndici.request.mockImplementation(localEvaluationImplementation(flags)) | ||
mockedUndici.fetch.mockImplementation(decideImplementation({ 'beta-feature': 'decide-fallback-value' })) | ||
posthog = new PostHog('TEST_API_KEY', { | ||
host: 'http://example.com', | ||
personalApiKey: 'TEST_PERSONAL_API_KEY', | ||
maxCacheSize: 10, | ||
}) | ||
expect(Object.keys(posthog.distinctIdHasSentFlagCalls).length).toEqual(0) | ||
for (let i = 0; i < 1000; i++) { | ||
const distinctId = `some-distinct-id${i}` | ||
await posthog.getFeatureFlag('beta-feature', distinctId) | ||
jest.runOnlyPendingTimers() | ||
const batchEvents = getLastBatchEvents() | ||
expect(batchEvents).toMatchObject([ | ||
{ | ||
distinct_id: distinctId, | ||
event: '$feature_flag_called', | ||
properties: expect.objectContaining({ | ||
$feature_flag: 'beta-feature', | ||
$feature_flag_response: true, | ||
$lib: 'posthog-node', | ||
$lib_version: '1.2.3', | ||
locally_evaluated: true, | ||
}), | ||
}, | ||
]) | ||
mockedUndici.fetch.mockClear() | ||
expect(Object.keys(posthog.distinctIdHasSentFlagCalls).length <= 10).toEqual(true) | ||
} | ||
}) | ||
it('$feature_flag_called is called appropriately when querying flags', async () => { | ||
const flags = { | ||
flags: [ | ||
{ | ||
id: 1, | ||
name: 'Beta Feature', | ||
key: 'beta-feature', | ||
active: true, | ||
filters: { | ||
groups: [ | ||
{ | ||
properties: [{ key: 'region', value: 'USA' }], | ||
rollout_percentage: 100, | ||
}, | ||
], | ||
}, | ||
}, | ||
], | ||
} | ||
mockedUndici.request.mockImplementation(localEvaluationImplementation(flags)) | ||
mockedUndici.fetch.mockImplementation(decideImplementation({ 'decide-flag': 'decide-value' })) | ||
posthog = new PostHog('TEST_API_KEY', { | ||
host: 'http://example.com', | ||
personalApiKey: 'TEST_PERSONAL_API_KEY', | ||
maxCacheSize: 10, | ||
}) | ||
expect( | ||
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', { | ||
personProperties: { region: 'USA', name: 'Aloha' }, | ||
}) | ||
).toEqual(true) | ||
jest.runOnlyPendingTimers() | ||
expect(mockedUndici.fetch.mock.calls.length).toEqual(1) | ||
expect(getLastBatchEvents()?.[0]).toEqual( | ||
expect.objectContaining({ | ||
distinct_id: 'some-distinct-id', | ||
event: '$feature_flag_called', | ||
properties: expect.objectContaining({ | ||
$feature_flag: 'beta-feature', | ||
$feature_flag_response: true, | ||
$lib: 'posthog-node', | ||
$lib_version: '1.2.3', | ||
locally_evaluated: true, | ||
}), | ||
}) | ||
) | ||
mockedUndici.fetch.mockClear() | ||
// # called again for same user, shouldn't call capture again | ||
expect( | ||
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', { | ||
personProperties: { region: 'USA', name: 'Aloha' }, | ||
}) | ||
).toEqual(true) | ||
jest.runOnlyPendingTimers() | ||
expect(mockedUndici.fetch).not.toBeCalled() | ||
// # called for different user, should call capture again | ||
expect( | ||
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id2', { | ||
groups: { x: 'y' }, | ||
personProperties: { region: 'USA', name: 'Aloha' }, | ||
}) | ||
).toEqual(true) | ||
jest.runOnlyPendingTimers() | ||
expect(mockedUndici.fetch.mock.calls.length).toEqual(1) | ||
expect(getLastBatchEvents()?.[0]).toEqual( | ||
expect.objectContaining({ | ||
distinct_id: 'some-distinct-id2', | ||
event: '$feature_flag_called', | ||
properties: expect.objectContaining({ | ||
$feature_flag: 'beta-feature', | ||
$feature_flag_response: true, | ||
$lib: 'posthog-node', | ||
$lib_version: '1.2.3', | ||
locally_evaluated: true, | ||
$groups: { x: 'y' }, | ||
}), | ||
}) | ||
) | ||
mockedUndici.fetch.mockClear() | ||
// # called for different user, but send configuration is false, so should NOT call capture again | ||
expect( | ||
await posthog.getFeatureFlag('beta-feature', 'some-distinct-id23', { | ||
personProperties: { region: 'USA', name: 'Aloha' }, | ||
sendFeatureFlagEvents: false, | ||
}) | ||
).toEqual(true) | ||
jest.runOnlyPendingTimers() | ||
expect(mockedUndici.fetch).not.toBeCalled() | ||
// # called for different flag, falls back to decide, should call capture again | ||
expect( | ||
await posthog.getFeatureFlag('decide-flag', 'some-distinct-id2345', { | ||
groups: { organization: 'org1' }, | ||
personProperties: { region: 'USA', name: 'Aloha' }, | ||
}) | ||
).toEqual('decide-value') | ||
jest.runOnlyPendingTimers() | ||
// one to decide, one to batch | ||
expect(mockedUndici.fetch.mock.calls.length).toEqual(2) | ||
expect(getLastBatchEvents()?.[0]).toEqual( | ||
expect.objectContaining({ | ||
distinct_id: 'some-distinct-id2345', | ||
event: '$feature_flag_called', | ||
properties: expect.objectContaining({ | ||
$feature_flag: 'decide-flag', | ||
$feature_flag_response: 'decide-value', | ||
$lib: 'posthog-node', | ||
$lib_version: '1.2.3', | ||
locally_evaluated: false, | ||
$groups: { organization: 'org1' }, | ||
}), | ||
}) | ||
) | ||
mockedUndici.fetch.mockClear() | ||
expect( | ||
await posthog.isFeatureEnabled('decide-flag', 'some-distinct-id2345', { | ||
groups: { organization: 'org1' }, | ||
personProperties: { region: 'USA', name: 'Aloha' }, | ||
}) | ||
).toEqual(true) | ||
jest.runOnlyPendingTimers() | ||
// call decide, but not batch | ||
expect(mockedUndici.fetch).toBeCalledTimes(1) | ||
expect(mockedUndici.fetch.mock.calls.find((x) => (x[0] as string).includes('/batch/'))).toEqual(undefined) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
604914
26
8719
1
10