Socket
Socket
Sign inDemoInstall

posthog-node

Package Overview
Dependencies
Maintainers
6
Versions
66
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 3.4.0 to 3.5.0

5

CHANGELOG.md

@@ -0,1 +1,6 @@

# 3.5.0 - 2024-01-09
1. When local evaluation is enabled, we automatically add flag information to all events sent to PostHog, whenever possible. This makes it easier to use these events in experiments.
2. Fixes a bug where in some rare cases we may drop events when send_feature_flags is enabled on capture.
# 3.4.0 - 2024-01-09

@@ -2,0 +7,0 @@

4

lib/index.d.ts

@@ -111,5 +111,5 @@ /// <reference types="node" />

private debugMode;
private pendingPromises;
private disableGeoip;
private _optoutOverride;
private pendingPromises;
protected _events: SimpleEventEmitter;

@@ -132,2 +132,3 @@ protected _flushTimer?: any;

private buildPayload;
protected addPendingPromise(promise: Promise<any>): void;
/***

@@ -382,2 +383,3 @@ *** TRACKING

shutdownAsync(): Promise<void>;
private addLocalPersonAndGroupProperties;
}

@@ -384,0 +386,0 @@

@@ -15,5 +15,5 @@ import { PostHogFetchOptions, PostHogFetchResponse, PostHogAutocaptureElement, PostHogDecideResponse, PosthogCoreOptions, PostHogEventProperties, PostHogPersistedProperty, PosthogCaptureOptions, JsonType } from './types';

private debugMode;
private pendingPromises;
private disableGeoip;
private _optoutOverride;
private pendingPromises;
protected _events: SimpleEventEmitter;

@@ -36,2 +36,3 @@ protected _flushTimer?: any;

private buildPayload;
protected addPendingPromise(promise: Promise<any>): void;
/***

@@ -38,0 +39,0 @@ *** TRACKING

@@ -76,2 +76,3 @@ import { JsonType, PosthogCoreOptions, PostHogCoreStateless, PostHogFetchOptions, PostHogFetchResponse, PosthogFlagsAndPayloadsResponse, PostHogPersistedProperty } from '../../posthog-core/src';

shutdownAsync(): Promise<void>;
private addLocalPersonAndGroupProperties;
}
{
"name": "posthog-node",
"version": "3.4.0",
"version": "3.5.0",
"description": "PostHog Node.js integration",

@@ -5,0 +5,0 @@ "repository": {

@@ -124,3 +124,3 @@ import { createHash } from 'rusha'

} else if (e instanceof Error) {
console.error(`Error computing flag locally: ${key}: ${e}`)
this.onError?.(new Error(`Error computing flag locally: ${key}: ${e}`))
}

@@ -215,5 +215,7 @@ }

if (!groupName) {
console.warn(
`[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`
)
if (this.debugMode) {
console.warn(
`[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`
)
}
throw new InconclusiveMatchError('Flag has unknown group type index')

@@ -223,3 +225,5 @@ }

if (!(groupName in groups)) {
console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`)
if (this.debugMode) {
console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`)
}
return false

@@ -389,3 +393,3 @@ }

if (!('flags' in responseJson)) {
console.error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`)
this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`))
}

@@ -392,0 +396,0 @@

@@ -108,22 +108,50 @@ import { version } from '../package.json'

if (sendFeatureFlags) {
super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip).then((flags) => {
const featureVariantProperties: Record<string, string | boolean> = {}
// :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
const capturePromise = Promise.resolve()
.then(async () => {
if (sendFeatureFlags) {
// If we are sending feature flags, we need to make sure we have the latest flags
return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
}
if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
// Otherwise we may as well check for the flags locally and include them if there
const groupsWithStringValues: Record<string, string> = {}
for (const [key, value] of Object.entries(groups || {})) {
groupsWithStringValues[key] = String(value)
}
return await this.getAllFlags(distinctId, {
groups: groupsWithStringValues,
disableGeoip,
onlyEvaluateLocally: true,
})
}
return {}
})
.then((flags) => {
// Derive the relevant flag properties to add
const additionalProperties: Record<string, any> = {}
if (flags) {
for (const [feature, variant] of Object.entries(flags)) {
if (variant !== false) {
featureVariantProperties[`$feature/${feature}`] = variant
}
additionalProperties[`$feature/${feature}`] = variant
}
}
const activeFlags = Object.keys(flags || {}).filter((flag) => flags?.[flag] !== false)
const flagProperties = {
$active_feature_flags: activeFlags || undefined,
...featureVariantProperties,
if (activeFlags.length > 0) {
additionalProperties['$active_feature_flags'] = activeFlags
}
_capture({ ...properties, $groups: groups, ...flagProperties })
return additionalProperties
})
} else {
_capture({ ...properties, $groups: groups })
}
.catch(() => {
// Something went wrong getting the flag info - we should capture the event anyways
return {}
})
.then((additionalProperties) => {
// No matter what - capture the event
_capture({ ...additionalProperties, ...properties, $groups: groups })
})
this.addPendingPromise(capturePromise)
}

@@ -160,5 +188,15 @@

): Promise<string | boolean | undefined> {
const { groups, personProperties, groupProperties, disableGeoip } = options || {}
let { onlyEvaluateLocally, sendFeatureFlagEvents } = options || {}
const { groups, disableGeoip } = options || {}
let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {}
const adjustedProperties = this.addLocalPersonAndGroupProperties(
distinctId,
groups,
personProperties,
groupProperties
)
personProperties = adjustedProperties.allPersonProperties
groupProperties = adjustedProperties.allGroupProperties
// set defaults

@@ -237,4 +275,15 @@ if (onlyEvaluateLocally == undefined) {

): Promise<JsonType | undefined> {
const { groups, personProperties, groupProperties, disableGeoip } = options || {}
let { onlyEvaluateLocally, sendFeatureFlagEvents } = options || {}
const { groups, disableGeoip } = options || {}
let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {}
const adjustedProperties = this.addLocalPersonAndGroupProperties(
distinctId,
groups,
personProperties,
groupProperties
)
personProperties = adjustedProperties.allPersonProperties
groupProperties = adjustedProperties.allGroupProperties
let response = undefined

@@ -330,5 +379,15 @@

): Promise<PosthogFlagsAndPayloadsResponse> {
const { groups, personProperties, groupProperties, disableGeoip } = options || {}
let { onlyEvaluateLocally } = options || {}
const { groups, disableGeoip } = options || {}
let { onlyEvaluateLocally, personProperties, groupProperties } = options || {}
const adjustedProperties = this.addLocalPersonAndGroupProperties(
distinctId,
groups,
personProperties,
groupProperties
)
personProperties = adjustedProperties.allPersonProperties
groupProperties = adjustedProperties.allGroupProperties
// set defaults

@@ -392,2 +451,23 @@ if (onlyEvaluateLocally == undefined) {

}
private addLocalPersonAndGroupProperties(
distinctId: string,
groups?: Record<string, string>,
personProperties?: Record<string, string>,
groupProperties?: Record<string, Record<string, string>>
): { allPersonProperties: Record<string, string>; allGroupProperties: Record<string, Record<string, string>> } {
const allPersonProperties = { $current_distinct_id: distinctId, ...(personProperties || {}) }
const allGroupProperties: Record<string, Record<string, string>> = {}
if (groups) {
for (const groupName of Object.keys(groups)) {
allGroupProperties[groupName] = {
$group_key: groups[groupName],
...(groupProperties?.[groupName] || {}),
}
}
}
return { allPersonProperties, allGroupProperties }
}
}

@@ -6,2 +6,3 @@ // import { PostHog } from '../'

import fetch from '../../src/fetch'
import { waitForPromises } from 'posthog-core/test/test-utils/test-utils'

@@ -112,2 +113,3 @@ jest.mock('../../package.json', () => ({ version: '1.2.3' }))

await waitForPromises()
jest.runOnlyPendingTimers()

@@ -114,0 +116,0 @@ const batchEvents = getLastBatchEvents()

@@ -54,3 +54,5 @@ // import { PostHog } from '../'

await waitForPromises()
jest.runOnlyPendingTimers()
const batchEvents = getLastBatchEvents()

@@ -80,2 +82,3 @@ expect(batchEvents).toEqual([

await waitForPromises()
jest.runOnlyPendingTimers()

@@ -103,2 +106,3 @@ expect(getLastBatchEvents()?.[0]).toEqual(

await waitForPromises()
jest.runOnlyPendingTimers()

@@ -179,2 +183,3 @@ expect(getLastBatchEvents()?.[0]).toEqual(

posthog.capture({ event: 'custom-time', distinctId: '123', timestamp: new Date('2021-02-03') })
await waitForPromises()
jest.runOnlyPendingTimers()

@@ -201,2 +206,3 @@ const batchEvents = getLastBatchEvents()

await waitForPromises()
jest.runOnlyPendingTimers()

@@ -220,3 +226,5 @@ const batchEvents = getLastBatchEvents()

await waitForPromises()
jest.runOnlyPendingTimers()
let batchEvents = getLastBatchEvents()

@@ -238,2 +246,3 @@ expect(batchEvents?.[0].properties).toEqual({

await waitForPromises()
jest.runOnlyPendingTimers()

@@ -258,2 +267,3 @@ batchEvents = getLastBatchEvents()

await waitForPromises()
jest.runOnlyPendingTimers()

@@ -297,2 +307,3 @@ batchEvents = getLastBatchEvents()

posthog.debug(false)
jest.useFakeTimers()
})

@@ -333,2 +344,41 @@

})
it('should shutdown cleanly with pending capture flag promises', async () => {
posthog = new PostHog('TEST_API_KEY', {
host: 'http://example.com',
fetchRetryCount: 0,
flushAt: 4,
})
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
jest.useRealTimers()
posthog.debug(true)
for (let i = 0; i < 10; i++) {
posthog.capture({ event: 'test-event', distinctId: `${i}`, sendFeatureFlags: true })
}
await posthog.shutdownAsync()
// all capture calls happen during shutdown
const batchEvents = getLastBatchEvents()
expect(batchEvents?.length).toEqual(2)
expect(batchEvents?.[batchEvents?.length - 1]).toMatchObject({
// last event in batch
distinct_id: '9',
event: 'test-event',
library: 'posthog-node',
library_version: '1.2.3',
properties: {
$lib: 'posthog-node',
$lib_version: '1.2.3',
$geoip_disable: true,
},
timestamp: expect.any(String),
type: 'capture',
})
expect(10).toEqual(logSpy.mock.calls.filter((call) => call[1].includes('capture')).length)
expect(3).toEqual(logSpy.mock.calls.filter((call) => call[1].includes('flush')).length)
jest.useFakeTimers()
logSpy.mockRestore()
})
})

@@ -397,4 +447,75 @@

const multivariateFlag = {
id: 1,
name: 'Beta Feature',
key: 'beta-feature-local',
is_simple_flag: false,
active: true,
rollout_percentage: 100,
filters: {
groups: [
{
properties: [{ key: 'email', type: 'person', value: 'test@posthog.com', operator: 'exact' }],
rollout_percentage: 100,
},
{
rollout_percentage: 50,
},
],
multivariate: {
variants: [
{ key: 'first-variant', name: 'First Variant', rollout_percentage: 50 },
{ key: 'second-variant', name: 'Second Variant', rollout_percentage: 25 },
{ key: 'third-variant', name: 'Third Variant', rollout_percentage: 25 },
],
},
payloads: { 'first-variant': 'some-payload', 'third-variant': { a: 'json' } },
},
}
const basicFlag = {
id: 1,
name: 'Beta Feature',
key: 'person-flag',
is_simple_flag: true,
active: true,
filters: {
groups: [
{
properties: [
{
key: 'region',
operator: 'exact',
value: ['USA'],
type: 'person',
},
],
rollout_percentage: 100,
},
],
payloads: { true: 300 },
},
}
const falseFlag = {
id: 1,
name: 'Beta Feature',
key: 'false-flag',
is_simple_flag: true,
active: true,
filters: {
groups: [
{
properties: [],
rollout_percentage: 0,
},
],
payloads: { true: 300 },
},
}
mockedFetch.mockImplementation(
apiImplementation({ decideFlags: mockFeatureFlags, decideFlagPayloads: mockFeatureFlagPayloads })
apiImplementation({
decideFlags: mockFeatureFlags,
decideFlagPayloads: mockFeatureFlagPayloads,
localFlags: { flags: [multivariateFlag, basicFlag, falseFlag] },
})
)

@@ -444,2 +565,5 @@

jest.runOnlyPendingTimers()
await waitForPromises()
expect(mockedFetch).toHaveBeenCalledWith(

@@ -450,6 +574,2 @@ 'http://example.com/decide/?v=3',

jest.runOnlyPendingTimers()
await waitForPromises()
expect(getLastBatchEvents()?.[0]).toEqual(

@@ -480,2 +600,109 @@ expect.objectContaining({

it('captures feature flags with locally evaluated flags', async () => {
mockedFetch.mockClear()
mockedFetch.mockClear()
expect(mockedFetch).toHaveBeenCalledTimes(0)
posthog = new PostHog('TEST_API_KEY', {
host: 'http://example.com',
flushAt: 1,
fetchRetryCount: 0,
personalApiKey: 'TEST_PERSONAL_API_KEY',
})
jest.runOnlyPendingTimers()
await waitForPromises()
posthog.capture({
distinctId: 'distinct_id',
event: 'node test event',
})
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
// no decide call
expect(mockedFetch).not.toHaveBeenCalledWith(
'http://example.com/decide/?v=3',
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: ['beta-feature-local'],
'$feature/beta-feature-local': 'third-variant',
'$feature/false-flag': false,
$lib: 'posthog-node',
$lib_version: '1.2.3',
$geoip_disable: true,
}),
})
)
expect(
Object.prototype.hasOwnProperty.call(getLastBatchEvents()?.[0].properties, '$feature/beta-feature-local')
).toBe(true)
expect(Object.prototype.hasOwnProperty.call(getLastBatchEvents()?.[0].properties, '$feature/beta-feature')).toBe(
false
)
await posthog.shutdownAsync()
})
it('doesnt add flag properties when locally evaluated flags are empty', async () => {
mockedFetch.mockClear()
expect(mockedFetch).toHaveBeenCalledTimes(0)
mockedFetch.mockImplementation(
apiImplementation({ decideFlags: { a: false, b: 'true' }, decideFlagPayloads: {}, localFlags: { flags: [] } })
)
posthog = new PostHog('TEST_API_KEY', {
host: 'http://example.com',
flushAt: 1,
fetchRetryCount: 0,
personalApiKey: 'TEST_PERSONAL_API_KEY',
})
posthog.capture({
distinctId: 'distinct_id',
event: 'node test event',
})
jest.runOnlyPendingTimers()
await waitForPromises()
expect(mockedFetch).toHaveBeenCalledWith(...anyLocalEvalCall)
// no decide call
expect(mockedFetch).not.toHaveBeenCalledWith(
'http://example.com/decide/?v=3',
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({
$lib: 'posthog-node',
$lib_version: '1.2.3',
$geoip_disable: true,
}),
})
)
expect(
Object.prototype.hasOwnProperty.call(getLastBatchEvents()?.[0].properties, '$feature/beta-feature-local')
).toBe(false)
expect(Object.prototype.hasOwnProperty.call(getLastBatchEvents()?.[0].properties, '$feature/beta-feature')).toBe(
false
)
})
it('captures feature flags with same geoip setting as capture', async () => {

@@ -499,2 +726,5 @@ mockedFetch.mockClear()

await waitForPromises()
jest.runOnlyPendingTimers()
expect(mockedFetch).toHaveBeenCalledWith(

@@ -505,6 +735,2 @@ 'http://example.com/decide/?v=3',

jest.runOnlyPendingTimers()
await waitForPromises()
expect(getLastBatchEvents()?.[0].properties).toEqual({

@@ -514,2 +740,3 @@ $active_feature_flags: ['feature-1', 'feature-2', 'feature-variant'],

'$feature/feature-2': true,
'$feature/disabled-flag': false,
'$feature/feature-variant': 'variant',

@@ -554,2 +781,3 @@ $lib: 'posthog-node',

fetchRetryCount: 0,
flushAt: 1,
})

@@ -559,6 +787,7 @@

for (let i = 0; i < 1000; i++) {
for (let i = 0; i < 100; i++) {
const distinctId = `some-distinct-id${i}`
await posthog.getFeatureFlag('beta-feature', distinctId)
await waitForPromises()
jest.runOnlyPendingTimers()

@@ -588,2 +817,4 @@

it('$feature_flag_called is called appropriately when querying flags', async () => {
mockedFetch.mockClear()
const flags = {

@@ -619,2 +850,4 @@ flags: [

jest.runOnlyPendingTimers()
expect(

@@ -625,3 +858,8 @@ await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {

).toEqual(true)
// TRICKY: There's now an extra step before events are queued, so need to wait for that to resolve
jest.runOnlyPendingTimers()
await waitForPromises()
await posthog.flushAsync()
expect(mockedFetch).toHaveBeenCalledWith('http://example.com/batch/', expect.any(Object))

@@ -653,2 +891,4 @@

jest.runOnlyPendingTimers()
await waitForPromises()
await posthog.flushAsync()

@@ -666,2 +906,4 @@ expect(mockedFetch).not.toHaveBeenCalledWith('http://example.com/batch/', expect.any(Object))

jest.runOnlyPendingTimers()
await waitForPromises()
await posthog.flushAsync()
expect(mockedFetch).toHaveBeenCalledWith('http://example.com/batch/', expect.any(Object))

@@ -694,2 +936,4 @@

jest.runOnlyPendingTimers()
await waitForPromises()
await posthog.flushAsync()
expect(mockedFetch).not.toHaveBeenCalledWith('http://example.com/batch/', expect.any(Object))

@@ -705,2 +949,4 @@

jest.runOnlyPendingTimers()
await waitForPromises()
await posthog.flushAsync()
// one to decide, one to batch

@@ -734,2 +980,4 @@ expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)

jest.runOnlyPendingTimers()
await waitForPromises()
await posthog.flushAsync()
// call decide, but not batch

@@ -780,3 +1028,152 @@ expect(mockedFetch).toHaveBeenCalledWith(...anyDecideCall)

})
it('should add default person & group properties for feature flags', async () => {
await posthog.getFeatureFlag('random_key', 'some_id', {
groups: { company: 'id:5', instance: 'app.posthog.com' },
personProperties: { x1: 'y1' },
groupProperties: { company: { x: 'y' } },
})
jest.runOnlyPendingTimers()
expect(mockedFetch).toHaveBeenCalledWith(
'http://example.com/decide/?v=3',
expect.objectContaining({
body: JSON.stringify({
token: 'TEST_API_KEY',
distinct_id: 'some_id',
groups: { company: 'id:5', instance: 'app.posthog.com' },
person_properties: {
$current_distinct_id: 'some_id',
x1: 'y1',
},
group_properties: {
company: { $group_key: 'id:5', x: 'y' },
instance: { $group_key: 'app.posthog.com' },
},
geoip_disable: true,
}),
})
)
mockedFetch.mockClear()
await posthog.getFeatureFlag('random_key', 'some_id', {
groups: { company: 'id:5', instance: 'app.posthog.com' },
personProperties: { $current_distinct_id: 'override' },
groupProperties: { company: { $group_key: 'group_override' } },
})
jest.runOnlyPendingTimers()
expect(mockedFetch).toHaveBeenCalledWith(
'http://example.com/decide/?v=3',
expect.objectContaining({
body: JSON.stringify({
token: 'TEST_API_KEY',
distinct_id: 'some_id',
groups: { company: 'id:5', instance: 'app.posthog.com' },
person_properties: {
$current_distinct_id: 'override',
},
group_properties: {
company: { $group_key: 'group_override' },
instance: { $group_key: 'app.posthog.com' },
},
geoip_disable: true,
}),
})
)
mockedFetch.mockClear()
// test nones
await posthog.getAllFlagsAndPayloads('some_id', {
groups: undefined,
personProperties: undefined,
groupProperties: undefined,
})
jest.runOnlyPendingTimers()
expect(mockedFetch).toHaveBeenCalledWith(
'http://example.com/decide/?v=3',
expect.objectContaining({
body: JSON.stringify({
token: 'TEST_API_KEY',
distinct_id: 'some_id',
groups: {},
person_properties: {
$current_distinct_id: 'some_id',
},
group_properties: {},
geoip_disable: true,
}),
})
)
mockedFetch.mockClear()
await posthog.getAllFlags('some_id', {
groups: { company: 'id:5' },
personProperties: undefined,
groupProperties: undefined,
})
jest.runOnlyPendingTimers()
expect(mockedFetch).toHaveBeenCalledWith(
'http://example.com/decide/?v=3',
expect.objectContaining({
body: JSON.stringify({
token: 'TEST_API_KEY',
distinct_id: 'some_id',
groups: { company: 'id:5' },
person_properties: {
$current_distinct_id: 'some_id',
},
group_properties: { company: { $group_key: 'id:5' } },
geoip_disable: true,
}),
})
)
mockedFetch.mockClear()
await posthog.getFeatureFlagPayload('random_key', 'some_id', undefined)
jest.runOnlyPendingTimers()
expect(mockedFetch).toHaveBeenCalledWith(
'http://example.com/decide/?v=3',
expect.objectContaining({
body: JSON.stringify({
token: 'TEST_API_KEY',
distinct_id: 'some_id',
groups: {},
person_properties: {
$current_distinct_id: 'some_id',
},
group_properties: {},
geoip_disable: true,
}),
})
)
mockedFetch.mockClear()
await posthog.isFeatureEnabled('random_key', 'some_id')
jest.runOnlyPendingTimers()
expect(mockedFetch).toHaveBeenCalledWith(
'http://example.com/decide/?v=3',
expect.objectContaining({
body: JSON.stringify({
token: 'TEST_API_KEY',
distinct_id: 'some_id',
groups: {},
person_properties: {
$current_distinct_id: 'some_id',
},
group_properties: {},
geoip_disable: true,
}),
})
)
})
})
})

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

Sorry, the diff of this file is too big to display

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