Skip to content

Commit

Permalink
Make analytics backwards compatible
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle-ssg committed Sep 10, 2024
1 parent 3c13bca commit ded79ff
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 54 deletions.
63 changes: 38 additions & 25 deletions flagsmith-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ const Flagsmith = class {

events: string[] = []

splitTestingAnalytics=false;

getFlags = () => {
const { identity, api } = this;
this.log("Get Flags")
Expand Down Expand Up @@ -216,7 +218,10 @@ const Flagsmith = class {
}
};

_parseEvaluations = (evaluations: Record<string, number>|null)=> {
_parseV2Analytics = (evaluations: Record<string, number>|null)=> {
if(!this.splitTestingAnalytics) {
return evaluations || {}
}
if(!evaluations) return {evaluations: []}
return {
evaluations: Object.keys(evaluations).map((feature_name)=>(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
}));
});
}
};

Expand All @@ -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) {
Expand Down
90 changes: 71 additions & 19 deletions test/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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/`,
Expand All @@ -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',
},
},
);
});

});
20 changes: 16 additions & 4 deletions test/test-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -64,19 +65,30 @@ export function getStateToCheck(_state: IState) {
return state;
}

export function getFlagsmith(config: Partial<IInitConfig> = {}) {
export function getFlagsmith(config: Partial<IInitConfig> = {}, 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
flagsmith.canUseStorage = true;
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
})
);
}
14 changes: 8 additions & 6 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,28 @@ export declare type LoadingState = {

export type OnChange<F extends string = string> = (previousFlags: IFlags<F> | null, params: IRetrieveInfo, loadingState:LoadingState) => void
export interface IInitConfig<F extends string = string, T extends string = string> {
environmentID: string;
AsyncStorage?: any;
angularHttpClient?: any;
api?: string;
cacheFlags?: boolean;
cacheOptions?: ICacheOptions;
datadogRum?: IDatadogRum;
defaultFlags?: IFlags<F>;
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<T>;
onChange?: OnChange<F>;
onError?: (err: Error) => void;
preventFetch?: boolean;
realtime?: boolean;
splitTestingAnalytics?: boolean;
state?: IState;
traits?: ITraits<T>;
_trigger?: () => void;
_triggerLoadingState?: () => void;
}
Expand Down Expand Up @@ -202,6 +203,7 @@ export interface IFlagsmith<F extends string = string, T extends string = string
*/
setTraits: (traits: Record<T, IFlagsmithValue>) => Promise<void>;
/**
* Only available for self hosted split testing analytics.
* Track a conversion event within your application, used for split testing analytics.
*/
trackEvent: (event: string) => Promise<void>;
Expand Down

0 comments on commit ded79ff

Please sign in to comment.