Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

posthog-node

Package Overview
Dependencies
Maintainers
6
Versions
67
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

posthog-node - npm Package Compare versions

Comparing version 2.0.0-alpha9 to 2.0.1

CHANGELOG.md

77

lib/index.d.ts

@@ -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>;

25

lib/posthog-core/src/index.d.ts

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc