From ded79ffb4a791eaed7204207330a784fd1b824ad Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Tue, 10 Sep 2024 20:23:21 +0100 Subject: [PATCH] Make analytics backwards compatible --- flagsmith-core.ts | 63 +++++++++++++++++------------ test/analytics.test.ts | 90 +++++++++++++++++++++++++++++++++--------- test/test-constants.ts | 20 ++++++++-- types.d.ts | 14 ++++--- 4 files changed, 133 insertions(+), 54 deletions(-) diff --git a/flagsmith-core.ts b/flagsmith-core.ts index cee0da2..db5124d 100644 --- a/flagsmith-core.ts +++ b/flagsmith-core.ts @@ -85,6 +85,8 @@ const Flagsmith = class { events: string[] = [] + splitTestingAnalytics=false; + getFlags = () => { const { identity, api } = this; this.log("Get Flags") @@ -216,7 +218,10 @@ const Flagsmith = class { } }; - _parseEvaluations = (evaluations: Record|null)=> { + _parseV2Analytics = (evaluations: Record|null)=> { + if(!this.splitTestingAnalytics) { + return evaluations || {} + } if(!evaluations) return {evaluations: []} return { evaluations: Object.keys(evaluations).map((feature_name)=>( @@ -258,31 +263,33 @@ const Flagsmith = class { async init(config: IInitConfig) { try { const { - environmentID, + AsyncStorage: _AsyncStorage, + angularHttpClient, api = defaultAPI, - headers, - onChange, cacheFlags, + cacheOptions, datadogRum, - onError, defaultFlags, + enableAnalytics, + enableDynatrace, + enableLogs, + environmentID, + eventSourceUrl= "https://realtime.flagsmith.com/", fetch: fetchImplementation, + headers, + identity, + onChange, + onError, preventFetch, - enableLogs, - enableDynatrace, - enableAnalytics, + splitTestingAnalytics, realtime, - eventSourceUrl= "https://realtime.flagsmith.com/", - AsyncStorage: _AsyncStorage, - identity, - traits, state, - cacheOptions, - angularHttpClient, + traits, _trigger, _triggerLoadingState, } = config; this.environmentID = environmentID; + this.splitTestingAnalytics = !!splitTestingAnalytics; this.api = api; this.headers = headers; this.getFlagInterval = null; @@ -671,18 +678,22 @@ const Flagsmith = class { return res; }; - trackEvent = (event: string)=> { - if(!this.enableAnalytics) { - console.error("In order to track events, please configure the enableAnalytics option. See https://docs.flagsmith.com/clients/javascript/#initialisation-options.") - return Promise.reject() + trackEvent = (event: string) => { + if (!this.splitTestingAnalytics) { + const error = new Error("This feature is only enabled for self-hosted customers using split testing."); + console.error(error.message); + return Promise.reject(error); } else if (!this.identity) { - this.events.push(event) - this.log("Waiting for user to be identified before tracking event", event ) - return Promise.resolve() + this.events.push(event); + this.log("Waiting for user to be identified before tracking event", event); + return Promise.resolve(); } else { - return this.analyticsFlags().then(()=> { - return this.getJSON(this.api + 'split-testing/conversion-events/', "POST", JSON.stringify({'identity_identifier': this.identity, 'type': event})) - }) + return this.analyticsFlags().then(() => { + return this.getJSON(this.api + 'split-testing/conversion-events/', "POST", JSON.stringify({ + 'identity_identifier': this.identity, + 'type': event + })); + }); } }; @@ -703,7 +714,9 @@ const Flagsmith = class { } if (this.evaluationEvent && Object.getOwnPropertyNames(this.evaluationEvent).length !== 0 && Object.getOwnPropertyNames(this.evaluationEvent[this.environmentID]).length !== 0) { - return this.getJSON(apiVersion(`${api}`, 2) + 'analytics/flags/', 'POST', JSON.stringify(this._parseEvaluations(this.evaluationEvent[this.environmentID]))) + return this.getJSON(apiVersion(`${api}`, this.splitTestingAnalytics?2:1) + 'analytics/flags/', + 'POST', + JSON.stringify(this._parseV2Analytics(this.evaluationEvent[this.environmentID]))) .then((res) => { const state = this.getState(); if (!this.evaluationEvent) { diff --git a/test/analytics.test.ts b/test/analytics.test.ts index b6a35e7..79e535c 100644 --- a/test/analytics.test.ts +++ b/test/analytics.test.ts @@ -1,40 +1,48 @@ // Sample test -import { getFlagsmith, testIdentity } from './test-constants'; +import { defaultState, getFlagsmith, getMockFetchWithValue, mockFetch, testIdentity } from './test-constants'; -describe('Analytics', () => { +describe.only('Analytics', () => { beforeEach(() => { - // Avoid mocks, but if you need to add them here + jest.useFakeTimers(); // Mock the timers }); - test('should track analytics when trackEvent is called', async () => { + afterEach(() => { + jest.useRealTimers(); // Restore real timers after each test + }); + test('should not attempt to track events when split testing is disabled', async () => { + const { flagsmith } = getFlagsmith({ + cacheFlags: true, + identity: testIdentity, + enableAnalytics: true, + splitTestingAnalytics: false, // Disable split testing + }); + + await expect(flagsmith.trackEvent("checkout")) + .rejects.toThrow('This feature is only enabled for self-hosted customers using split testing.'); + }); + test('should track v1 analytics', async () => { const onChange = jest.fn(); + const fetchFn = getMockFetchWithValue({ + flags:[{feature:{name:"font_size"}, enabled: true}, {feature:{name:"off_value"}, enabled: false}], + }); const { flagsmith, initConfig, mockFetch } = getFlagsmith({ cacheFlags: true, identity: testIdentity, enableAnalytics: true, onChange, - }); + }, fetchFn); await flagsmith.init(initConfig); - await flagsmith.trackEvent('checkout'); flagsmith.getValue("font_size") flagsmith.hasFeature("off_value") flagsmith.hasFeature("off_value") + jest.advanceTimersByTime(10000); expect(mockFetch).toHaveBeenCalledWith( - `${flagsmith.api.replace('/v1/', '/v2/')}analytics/flags/`, + `${flagsmith.api}analytics/flags/`, { method: 'POST', body: JSON.stringify({ - 'evaluations': [{ - 'feature_name': 'font_size', - 'identity_identifier': testIdentity, - 'count': 1, - 'enabled_when_evaluated': true, - },{ - 'feature_name': 'off_value', - 'identity_identifier': testIdentity, - 'count': 2, - 'enabled_when_evaluated': false, - }], + font_size: 1, + off_value: 2, }), cache: 'no-cache', headers: { @@ -43,6 +51,24 @@ describe('Analytics', () => { }, }, ); + }); + test('should track conversion events when trackEvent is called', async () => { + const onChange = jest.fn(); + const fetchFn = getMockFetchWithValue({ + flags:[{feature:{name:"font_size"}, enabled: true}, {feature:{name:"off_value"}, enabled: false}], + }); + const { flagsmith, initConfig, mockFetch } = getFlagsmith({ + cacheFlags: true, + identity: testIdentity, + enableAnalytics: true, + splitTestingAnalytics: true, + onChange, + }, fetchFn); + await flagsmith.init(initConfig); + flagsmith.getValue("font_size") + flagsmith.hasFeature("off_value") + flagsmith.hasFeature("off_value") + await flagsmith.trackEvent('checkout'); expect(mockFetch).toHaveBeenCalledWith( `${flagsmith.api}split-testing/conversion-events/`, @@ -59,7 +85,33 @@ describe('Analytics', () => { }, }, ); - + expect(mockFetch).toHaveBeenCalledWith( + `${flagsmith.api.replace('/v1/', '/v2/')}analytics/flags/`, + { + method: 'POST', + body: JSON.stringify({ + "evaluations": [ + { + "feature_name": "font_size", + "identity_identifier": "test_identity", + "count": 1, + "enabled_when_evaluated": true + }, + { + "feature_name": "off_value", + "identity_identifier": "test_identity", + "count": 2, + "enabled_when_evaluated": false + } + ] + }), + cache: 'no-cache', + headers: { + 'x-environment-key': flagsmith.environmentID, + 'Content-Type': 'application/json; charset=utf-8', + }, + }, + ); }); }); diff --git a/test/test-constants.ts b/test/test-constants.ts index 7ca936d..8246c42 100644 --- a/test/test-constants.ts +++ b/test/test-constants.ts @@ -2,6 +2,7 @@ import { IInitConfig, IState } from '../lib/flagsmith/types'; import MockAsyncStorage from './mocks/async-storage-mock'; import { createFlagsmithInstance } from '../lib/flagsmith'; import fetch from 'isomorphic-unfetch'; +import type { ModuleMocker } from 'jest-mock'; export const environmentID = 'QjgYur4LQTwe5HpvbvhpzK'; // Flagsmith Demo Projects export const defaultState = { @@ -64,10 +65,10 @@ export function getStateToCheck(_state: IState) { return state; } -export function getFlagsmith(config: Partial = {}) { +export function getFlagsmith(config: Partial = {}, mockFetch?:ModuleMocker['fn']) { const flagsmith = createFlagsmithInstance(); const AsyncStorage = new MockAsyncStorage(); - const mockFetch = jest.fn(async (url, options) => { + const _mockFetch = mockFetch || jest.fn(async (url:string, options) => { return fetch(url, options); }); //@ts-ignore, we want to test storage even though flagsmith thinks there is none @@ -75,8 +76,19 @@ export function getFlagsmith(config: Partial = {}) { const initConfig: IInitConfig = { environmentID, AsyncStorage, - fetch: mockFetch, + fetch: _mockFetch, ...config, }; - return { flagsmith, initConfig, mockFetch, AsyncStorage }; + return { flagsmith, initConfig, mockFetch:_mockFetch, AsyncStorage }; +} + + +export function getMockFetchWithValue(resolvedValue:object, status=200) { + return jest.fn(() => + Promise.resolve({ + status, + text: () => Promise.resolve(JSON.stringify(resolvedValue)), // Mock json() to return the mock response + json: () => Promise.resolve(resolvedValue), // Mock json() to return the mock response + }) + ); } diff --git a/types.d.ts b/types.d.ts index dd6f42a..bc4e0eb 100644 --- a/types.d.ts +++ b/types.d.ts @@ -77,27 +77,28 @@ export declare type LoadingState = { export type OnChange = (previousFlags: IFlags | null, params: IRetrieveInfo, loadingState:LoadingState) => void export interface IInitConfig { + environmentID: string; AsyncStorage?: any; + angularHttpClient?: any; api?: string; cacheFlags?: boolean; cacheOptions?: ICacheOptions; datadogRum?: IDatadogRum; defaultFlags?: IFlags; - fetch?: any; - realtime?: boolean; - eventSourceUrl?: string; enableAnalytics?: boolean; enableDynatrace?: boolean; enableLogs?: boolean; - angularHttpClient?: any; - environmentID: string; + eventSourceUrl?: string; + fetch?: any; headers?: object; identity?: string; - traits?: ITraits; onChange?: OnChange; onError?: (err: Error) => void; preventFetch?: boolean; + realtime?: boolean; + splitTestingAnalytics?: boolean; state?: IState; + traits?: ITraits; _trigger?: () => void; _triggerLoadingState?: () => void; } @@ -202,6 +203,7 @@ export interface IFlagsmith) => Promise; /** + * Only available for self hosted split testing analytics. * Track a conversion event within your application, used for split testing analytics. */ trackEvent: (event: string) => Promise;