UNPKG

posthog-node

Version:
381 lines (344 loc) 12.2 kB
import { PostHog as PostHog, PostHogOptions } from '../src/posthog-node' import fetch from '../src/fetch' import { apiImplementation, apiImplementationV4 } from './test-utils' import { waitForPromises } from 'posthog-core/test/test-utils/test-utils' import { PostHogV4DecideResponse } from 'posthog-core/src/types' jest.mock('../src/fetch') jest.spyOn(console, 'debug').mockImplementation() const mockedFetch = jest.mocked(fetch, true) const posthogImmediateResolveOptions: PostHogOptions = { fetchRetryCount: 0, } describe('decide v4', () => { describe('getFeatureFlag v4', () => { it('returns undefined if the flag is not found', async () => { const decideResponse: PostHogV4DecideResponse = { flags: {}, errorsWhileComputingFlags: false, requestId: '0152a345-295f-4fba-adac-2e6ea9c91082', } mockedFetch.mockImplementation(apiImplementationV4(decideResponse)) const posthog = new PostHog('TEST_API_KEY', { host: 'http://example.com', ...posthogImmediateResolveOptions, }) let capturedMessage: any posthog.on('capture', (message) => { capturedMessage = message }) const result = await posthog.getFeatureFlag('non-existent-flag', 'some-distinct-id') expect(result).toBe(undefined) expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object)) await waitForPromises() expect(capturedMessage).toMatchObject({ distinct_id: 'some-distinct-id', event: '$feature_flag_called', library: posthog.getLibraryId(), library_version: posthog.getLibraryVersion(), properties: { '$feature/non-existent-flag': undefined, $feature_flag: 'non-existent-flag', $feature_flag_response: undefined, $feature_flag_request_id: '0152a345-295f-4fba-adac-2e6ea9c91082', $groups: undefined, $lib: posthog.getLibraryId(), $lib_version: posthog.getLibraryVersion(), locally_evaluated: false, }, }) }) it.each([ { key: 'variant-flag', expectedResponse: 'variant-value', expectedReason: 'Matched condition set 3', expectedId: 2, expectedVersion: 23, }, { key: 'boolean-flag', expectedResponse: true, expectedReason: 'Matched condition set 1', expectedId: 1, expectedVersion: 12, }, { key: 'non-matching-flag', expectedResponse: false, expectedReason: 'Did not match any condition', expectedId: 3, expectedVersion: 2, }, ])( 'captures a feature flag called event with extra metadata when the flag is found', async ({ key, expectedResponse, expectedReason, expectedId, expectedVersion }) => { const decideResponse: PostHogV4DecideResponse = { flags: { 'variant-flag': { key: 'variant-flag', enabled: true, variant: 'variant-value', reason: { code: 'variant', condition_index: 2, description: 'Matched condition set 3', }, metadata: { id: 2, version: 23, payload: '{"key": "value"}', description: 'description', }, }, 'boolean-flag': { key: 'boolean-flag', enabled: true, variant: undefined, reason: { code: 'boolean', condition_index: 1, description: 'Matched condition set 1', }, metadata: { id: 1, version: 12, payload: undefined, description: 'description', }, }, 'non-matching-flag': { key: 'non-matching-flag', enabled: false, variant: undefined, reason: { code: 'boolean', condition_index: 1, description: 'Did not match any condition', }, metadata: { id: 3, version: 2, payload: undefined, description: 'description', }, }, }, errorsWhileComputingFlags: false, requestId: '0152a345-295f-4fba-adac-2e6ea9c91082', } mockedFetch.mockImplementation(apiImplementationV4(decideResponse)) const posthog = new PostHog('TEST_API_KEY', { host: 'http://example.com', ...posthogImmediateResolveOptions, }) let capturedMessage: any posthog.on('capture', (message) => { capturedMessage = message }) const result = await posthog.getFeatureFlag(key, 'some-distinct-id') expect(result).toBe(expectedResponse) expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object)) await waitForPromises() expect(capturedMessage).toMatchObject({ distinct_id: 'some-distinct-id', event: '$feature_flag_called', library: posthog.getLibraryId(), library_version: posthog.getLibraryVersion(), properties: { [`$feature/${key}`]: expectedResponse, $feature_flag: key, $feature_flag_response: expectedResponse, $feature_flag_id: expectedId, $feature_flag_version: expectedVersion, $feature_flag_reason: expectedReason, $feature_flag_request_id: '0152a345-295f-4fba-adac-2e6ea9c91082', $groups: undefined, $lib: posthog.getLibraryId(), $lib_version: posthog.getLibraryVersion(), locally_evaluated: false, }, }) } ) describe('getFeatureFlagPayload v4', () => { it('returns payload', async () => { mockedFetch.mockImplementation( apiImplementationV4({ flags: { 'flag-with-payload': { key: 'flag-with-payload', enabled: true, variant: undefined, reason: { code: 'boolean', condition_index: 1, description: 'Matched condition set 2', }, metadata: { id: 1, version: 12, payload: '[0, 1, 2]', description: 'description', }, }, }, errorsWhileComputingFlags: false, }) ) const posthog = new PostHog('TEST_API_KEY', { host: 'http://example.com', ...posthogImmediateResolveOptions, }) let capturedMessage: any posthog.on('capture', (message) => { capturedMessage = message }) const result = await posthog.getFeatureFlagPayload('flag-with-payload', 'some-distinct-id') expect(result).toEqual([0, 1, 2]) expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object)) await waitForPromises() expect(capturedMessage).toBeUndefined() }) }) }) describe('error handling', () => { let posthog: PostHog describe.each([ { case: 'JSON error response', mock: apiImplementationV4({ status: 400, json: () => Promise.resolve({ error: 'error response' }), }), }, { case: 'undefined response', mock: apiImplementationV4({ status: 400, json: () => Promise.resolve(undefined), }), }, { case: 'null response', mock: apiImplementationV4({ status: 400, json: () => Promise.resolve(null), }), }, { case: 'empty response', mock: apiImplementationV4({ status: 400, json: () => Promise.resolve({}), }), }, { case: 'network error', mock: () => Promise.reject(new Error('Network error')), }, { case: 'invalid JSON', mock: apiImplementationV4({ status: 500, json: () => Promise.reject(new Error('Invalid JSON')), }), }, ])('when $case', ({ mock }) => { beforeEach(() => { posthog = new PostHog('TEST_API_KEY', { host: 'http://example.com', ...posthogImmediateResolveOptions, }) mockedFetch.mockImplementation(mock) }) it('getFeatureFlag returns undefined', async () => { expect(await posthog.getFeatureFlag('error-flag', 'some-distinct-id')).toBe(undefined) }) it('isFeatureEnabled returns undefined', async () => { expect(await posthog.isFeatureEnabled('error-flag', 'some-distinct-id')).toBe(undefined) }) it('getFeatureFlagPayload returns undefined', async () => { expect(await posthog.getFeatureFlagPayload('error-flag', 'some-distinct-id')).toBe(undefined) }) it('getAllFlags returns empty object', async () => { expect(await posthog.getAllFlags('some-distinct-id')).toEqual({}) }) it('getAllFlagsAndPayloads returns object with empty flags and payloads', async () => { expect(await posthog.getAllFlagsAndPayloads('some-distinct-id')).toEqual({ featureFlags: {}, featureFlagPayloads: {}, }) }) it('captures no events', async () => { let capturedMessage: any posthog.on('capture', (message) => { capturedMessage = message }) await posthog.getFeatureFlag('error-flag', 'some-distinct-id') await waitForPromises() expect(capturedMessage).toBeUndefined() }) }) }) }) describe('decide v3', () => { describe('getFeatureFlag v3', () => { it('returns undefined if the flag is not found', async () => { mockedFetch.mockImplementation(apiImplementation({ decideFlags: {} })) const posthog = new PostHog('TEST_API_KEY', { host: 'http://example.com', ...posthogImmediateResolveOptions, }) let capturedMessage: any posthog.on('capture', (message) => { capturedMessage = message }) const result = await posthog.getFeatureFlag('non-existent-flag', 'some-distinct-id') expect(result).toBe(undefined) expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object)) await waitForPromises() expect(capturedMessage).toMatchObject({ distinct_id: 'some-distinct-id', event: '$feature_flag_called', library: posthog.getLibraryId(), library_version: posthog.getLibraryVersion(), properties: { '$feature/non-existent-flag': undefined, $feature_flag: 'non-existent-flag', $feature_flag_response: undefined, $groups: undefined, $lib: posthog.getLibraryId(), $lib_version: posthog.getLibraryVersion(), locally_evaluated: false, }, }) }) }) describe('getFeatureFlagPayload v3', () => { it('returns payload', async () => { mockedFetch.mockImplementation( apiImplementation({ decideFlags: { 'flag-with-payload': true, }, decideFlagPayloads: { 'flag-with-payload': [0, 1, 2], }, }) ) const posthog = new PostHog('TEST_API_KEY', { host: 'http://example.com', ...posthogImmediateResolveOptions, }) let capturedMessage: any = undefined posthog.on('capture', (message) => { capturedMessage = message }) const result = await posthog.getFeatureFlagPayload('flag-with-payload', 'some-distinct-id') expect(result).toEqual([0, 1, 2]) expect(mockedFetch).toHaveBeenCalledWith('http://example.com/decide/?v=4', expect.any(Object)) await waitForPromises() expect(capturedMessage).toBeUndefined() }) }) })