posthog-node 4.5.1 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/index.cjs.js +33 -6
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.esm.js +33 -6
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-node/src/feature-flags.d.ts +2 -0
- package/lib/posthog-node/src/posthog-node.d.ts +1 -0
- package/package.json +1 -1
- package/src/feature-flags.ts +29 -5
- package/src/posthog-node.ts +10 -1
- package/test/feature-flags.spec.ts +79 -0
- package/test/posthog-node.spec.ts +3 -1
|
@@ -58,8 +58,10 @@ declare class FeatureFlagsPoller {
|
|
|
58
58
|
}[];
|
|
59
59
|
loadFeatureFlags(forceReload?: boolean): Promise<void>;
|
|
60
60
|
_loadFeatureFlags(): Promise<void>;
|
|
61
|
+
private getPersonalApiKeyRequestOptions;
|
|
61
62
|
_requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse>;
|
|
62
63
|
stopPoller(): void;
|
|
64
|
+
_requestRemoteConfigPayload(flagKey: string): Promise<PostHogFetchResponse>;
|
|
63
65
|
}
|
|
64
66
|
declare function matchProperty(property: FeatureFlagCondition['properties'][number], propertyValues: Record<string, any>, warnFunction?: (msg: string) => void): boolean;
|
|
65
67
|
declare function relativeDateParseForFeatureFlagMatching(value: string): Date | null;
|
|
@@ -50,6 +50,7 @@ export declare class PostHog extends PostHogCoreStateless implements PostHogNode
|
|
|
50
50
|
sendFeatureFlagEvents?: boolean;
|
|
51
51
|
disableGeoip?: boolean;
|
|
52
52
|
}): Promise<JsonType | undefined>;
|
|
53
|
+
getRemoteConfigPayload(flagKey: string): Promise<JsonType | undefined>;
|
|
53
54
|
isFeatureEnabled(key: string, distinctId: string, options?: {
|
|
54
55
|
groups?: Record<string, string>;
|
|
55
56
|
personProperties?: Record<string, string>;
|
package/package.json
CHANGED
package/src/feature-flags.ts
CHANGED
|
@@ -419,17 +419,21 @@ class FeatureFlagsPoller {
|
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const options: PostHogFetchOptions = {
|
|
426
|
-
method: 'GET',
|
|
422
|
+
private getPersonalApiKeyRequestOptions(method: 'GET' | 'POST' | 'PUT' | 'PATCH' = 'GET'): PostHogFetchOptions {
|
|
423
|
+
return {
|
|
424
|
+
method,
|
|
427
425
|
headers: {
|
|
428
426
|
...this.customHeaders,
|
|
429
427
|
'Content-Type': 'application/json',
|
|
430
428
|
Authorization: `Bearer ${this.personalApiKey}`,
|
|
431
429
|
},
|
|
432
430
|
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async _requestFeatureFlagDefinitions(): Promise<PostHogFetchResponse> {
|
|
434
|
+
const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`
|
|
435
|
+
|
|
436
|
+
const options = this.getPersonalApiKeyRequestOptions()
|
|
433
437
|
|
|
434
438
|
let abortTimeout = null
|
|
435
439
|
|
|
@@ -451,6 +455,26 @@ class FeatureFlagsPoller {
|
|
|
451
455
|
stopPoller(): void {
|
|
452
456
|
clearTimeout(this.poller)
|
|
453
457
|
}
|
|
458
|
+
|
|
459
|
+
_requestRemoteConfigPayload(flagKey: string): Promise<PostHogFetchResponse> {
|
|
460
|
+
const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config/`
|
|
461
|
+
|
|
462
|
+
const options = this.getPersonalApiKeyRequestOptions()
|
|
463
|
+
|
|
464
|
+
let abortTimeout = null
|
|
465
|
+
if (this.timeout && typeof this.timeout === 'number') {
|
|
466
|
+
const controller = new AbortController()
|
|
467
|
+
abortTimeout = safeSetTimeout(() => {
|
|
468
|
+
controller.abort()
|
|
469
|
+
}, this.timeout)
|
|
470
|
+
options.signal = controller.signal
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
return this.fetch(url, options)
|
|
474
|
+
} finally {
|
|
475
|
+
clearTimeout(abortTimeout)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
454
478
|
}
|
|
455
479
|
|
|
456
480
|
// # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
|
package/src/posthog-node.ts
CHANGED
|
@@ -135,8 +135,13 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
|
|
|
135
135
|
return await _getFlags(distinctId, groups, disableGeoip)
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
if (event === '$feature_flag_called') {
|
|
139
|
+
// If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
|
|
140
|
+
return {}
|
|
141
|
+
}
|
|
142
|
+
|
|
138
143
|
if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
|
|
139
|
-
// Otherwise we may as well check for the flags locally and include them if
|
|
144
|
+
// Otherwise we may as well check for the flags locally and include them if they are already loaded
|
|
140
145
|
const groupsWithStringValues: Record<string, string> = {}
|
|
141
146
|
for (const [key, value] of Object.entries(groups || {})) {
|
|
142
147
|
groupsWithStringValues[key] = String(value)
|
|
@@ -354,6 +359,10 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
|
|
|
354
359
|
return response
|
|
355
360
|
}
|
|
356
361
|
|
|
362
|
+
async getRemoteConfigPayload(flagKey: string): Promise<JsonType | undefined> {
|
|
363
|
+
return (await this.featureFlagsPoller?._requestRemoteConfigPayload(flagKey))?.json()
|
|
364
|
+
}
|
|
365
|
+
|
|
357
366
|
async isFeatureEnabled(
|
|
358
367
|
key: string,
|
|
359
368
|
distinctId: string,
|
|
@@ -5,6 +5,7 @@ import { matchProperty, InconclusiveMatchError, relativeDateParseForFeatureFlagM
|
|
|
5
5
|
jest.mock('../src/fetch')
|
|
6
6
|
import fetch from '../src/fetch'
|
|
7
7
|
import { anyDecideCall, anyLocalEvalCall, apiImplementation } from './test-utils'
|
|
8
|
+
import { waitForPromises } from 'posthog-core/test/test-utils/test-utils'
|
|
8
9
|
|
|
9
10
|
jest.spyOn(console, 'debug').mockImplementation()
|
|
10
11
|
|
|
@@ -1796,6 +1797,84 @@ describe('local evaluation', () => {
|
|
|
1796
1797
|
})
|
|
1797
1798
|
})
|
|
1798
1799
|
|
|
1800
|
+
describe('getFeatureFlag', () => {
|
|
1801
|
+
it('should capture $feature_flag_called when called, but not add all cached flags', async () => {
|
|
1802
|
+
const flags = {
|
|
1803
|
+
flags: [
|
|
1804
|
+
{
|
|
1805
|
+
id: 1,
|
|
1806
|
+
name: 'Beta Feature',
|
|
1807
|
+
key: 'complex-flag',
|
|
1808
|
+
active: true,
|
|
1809
|
+
filters: {
|
|
1810
|
+
groups: [
|
|
1811
|
+
{
|
|
1812
|
+
variant: null,
|
|
1813
|
+
properties: [{ key: 'region', type: 'person', value: 'USA', operator: 'exact' }],
|
|
1814
|
+
rollout_percentage: 100,
|
|
1815
|
+
},
|
|
1816
|
+
],
|
|
1817
|
+
},
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
id: 2,
|
|
1821
|
+
name: 'Gamma Feature',
|
|
1822
|
+
key: 'simple-flag',
|
|
1823
|
+
active: true,
|
|
1824
|
+
filters: {
|
|
1825
|
+
groups: [
|
|
1826
|
+
{
|
|
1827
|
+
variant: null,
|
|
1828
|
+
properties: [],
|
|
1829
|
+
rollout_percentage: 100,
|
|
1830
|
+
},
|
|
1831
|
+
],
|
|
1832
|
+
},
|
|
1833
|
+
},
|
|
1834
|
+
],
|
|
1835
|
+
}
|
|
1836
|
+
mockedFetch.mockImplementation(apiImplementation({ localFlags: flags }))
|
|
1837
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
1838
|
+
host: 'http://example.com',
|
|
1839
|
+
personalApiKey: 'TEST_PERSONAL_API_KEY',
|
|
1840
|
+
...posthogImmediateResolveOptions,
|
|
1841
|
+
})
|
|
1842
|
+
let capturedMessage: any
|
|
1843
|
+
posthog.on('capture', (message) => {
|
|
1844
|
+
capturedMessage = message
|
|
1845
|
+
})
|
|
1846
|
+
|
|
1847
|
+
expect(
|
|
1848
|
+
await posthog.getFeatureFlag('complex-flag', 'some-distinct-id', {
|
|
1849
|
+
personProperties: {
|
|
1850
|
+
region: 'USA',
|
|
1851
|
+
} as unknown as Record<string, string>,
|
|
1852
|
+
})
|
|
1853
|
+
).toEqual(true)
|
|
1854
|
+
|
|
1855
|
+
await waitForPromises()
|
|
1856
|
+
|
|
1857
|
+
expect(capturedMessage).toMatchObject({
|
|
1858
|
+
distinct_id: 'some-distinct-id',
|
|
1859
|
+
event: '$feature_flag_called',
|
|
1860
|
+
library: posthog.getLibraryId(),
|
|
1861
|
+
library_version: posthog.getLibraryVersion(),
|
|
1862
|
+
properties: {
|
|
1863
|
+
'$feature/complex-flag': true,
|
|
1864
|
+
$feature_flag: 'complex-flag',
|
|
1865
|
+
$feature_flag_response: true,
|
|
1866
|
+
$groups: undefined,
|
|
1867
|
+
$lib: posthog.getLibraryId(),
|
|
1868
|
+
$lib_version: posthog.getLibraryVersion(),
|
|
1869
|
+
locally_evaluated: true,
|
|
1870
|
+
},
|
|
1871
|
+
})
|
|
1872
|
+
|
|
1873
|
+
expect(capturedMessage.properties).not.toHaveProperty('$active_feature_flags')
|
|
1874
|
+
expect(capturedMessage.properties).not.toHaveProperty('$feature/simple-flag')
|
|
1875
|
+
})
|
|
1876
|
+
})
|
|
1877
|
+
|
|
1799
1878
|
describe('match properties', () => {
|
|
1800
1879
|
jest.useFakeTimers()
|
|
1801
1880
|
|
|
@@ -1078,7 +1078,9 @@ describe('PostHog Node.js', () => {
|
|
|
1078
1078
|
expect(mockedFetch).toHaveBeenCalledTimes(0)
|
|
1079
1079
|
|
|
1080
1080
|
await expect(posthog.getFeatureFlagPayload('false-flag', '123', true)).resolves.toEqual(300)
|
|
1081
|
-
|
|
1081
|
+
// Check no non-batch API calls were made
|
|
1082
|
+
const additionalNonBatchCalls = mockedFetch.mock.calls.filter((call) => !call[0].includes('/batch'))
|
|
1083
|
+
expect(additionalNonBatchCalls.length).toBe(0)
|
|
1082
1084
|
})
|
|
1083
1085
|
|
|
1084
1086
|
it('should not double parse json with getFeatureFlagPayloads and server eval', async () => {
|