Skip to content

Commit 863a24a

Browse files
Replaced axios with fetch (FF-1976) (#57)
* Replaced axios with fetch (FF-1976) * yarn.lock changes * keep http error
1 parent b903bbb commit 863a24a

File tree

7 files changed

+230
-171
lines changed

7 files changed

+230
-171
lines changed

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "3.0.1",
3+
"version": "3.0.2",
44
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
55
"main": "dist/index.js",
66
"files": [
@@ -60,11 +60,9 @@
6060
"ts-node": "^10.9.1",
6161
"typescript": "^4.7.4",
6262
"webpack": "^5.73.0",
63-
"webpack-cli": "^4.10.0",
64-
"xhr-mock": "^2.5.1"
63+
"webpack-cli": "^4.10.0"
6564
},
6665
"dependencies": {
67-
"axios": "^1.6.0",
6866
"md5": "^2.3.0",
6967
"pino": "^8.19.0",
7068
"semver": "^7.5.4",

src/client/eppo-client.spec.ts

Lines changed: 95 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
/**
2-
* @jest-environment jsdom
3-
*/
4-
import axios from 'axios';
51
import * as td from 'testdouble';
6-
import mock, { MockResponse } from 'xhr-mock';
72

83
import {
94
IAssignmentTestCase,
@@ -19,16 +14,11 @@ import { IAssignmentLogger } from '../assignment-logger';
1914
import { IConfigurationStore } from '../configuration-store';
2015
import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
2116
import FlagConfigurationRequestor from '../flag-configuration-requestor';
22-
import HttpClient from '../http-client';
17+
import FetchHttpClient from '../http-client';
2318
import { Flag, VariationType } from '../interfaces';
2419

2520
import EppoClient, { FlagConfigurationRequestParameters, checkTypeMatch } from './eppo-client';
2621

27-
// eslint-disable-next-line @typescript-eslint/no-var-requires
28-
const packageJson = require('../../package.json');
29-
30-
const flagEndpoint = /flag-config\/v1\/config*/;
31-
3222
class TestConfigurationStore implements IConfigurationStore {
3323
private store: Record<string, string> = {};
3424
private _isInitialized = false;
@@ -53,39 +43,37 @@ class TestConfigurationStore implements IConfigurationStore {
5343
return this._isInitialized;
5444
}
5545
}
56-
export async function init(configurationStore: IConfigurationStore) {
57-
const axiosInstance = axios.create({
58-
baseURL: 'http://127.0.0.1:4000',
59-
timeout: 1000,
60-
});
61-
62-
const httpClient = new HttpClient(axiosInstance, {
63-
apiKey: 'dummy',
64-
sdkName: 'js-client-sdk-common',
65-
sdkVersion: packageJson.version,
66-
});
6746

47+
export async function init(configurationStore: IConfigurationStore) {
48+
const httpClient = new FetchHttpClient(
49+
'http://127.0.0.1:4000',
50+
{
51+
apiKey: 'dummy',
52+
sdkName: 'js-client-sdk-common',
53+
sdkVersion: '1.0.0',
54+
},
55+
1000,
56+
);
6857
const configurationRequestor = new FlagConfigurationRequestor(configurationStore, httpClient);
6958
await configurationRequestor.fetchAndStoreConfigurations();
7059
}
7160

7261
describe('EppoClient E2E test', () => {
73-
const storage = new TestConfigurationStore();
62+
global.fetch = jest.fn(() => {
63+
const ufc = readMockUFCResponse(MOCK_UFC_RESPONSE_FILE);
7464

75-
beforeAll(async () => {
76-
mock.setup();
77-
mock.get(flagEndpoint, (_req, res) => {
78-
const ufc = readMockUFCResponse(MOCK_UFC_RESPONSE_FILE);
79-
return res.status(200).body(JSON.stringify(ufc));
65+
return Promise.resolve({
66+
ok: true,
67+
status: 200,
68+
json: () => Promise.resolve(ufc),
8069
});
70+
}) as jest.Mock;
71+
const storage = new TestConfigurationStore();
8172

73+
beforeAll(async () => {
8274
await init(storage);
8375
});
8476

85-
afterAll(() => {
86-
mock.teardown();
87-
});
88-
8977
const flagKey = 'mock-flag';
9078

9179
const variationA = {
@@ -225,6 +213,22 @@ describe('EppoClient E2E test', () => {
225213
});
226214

227215
describe('UFC General Test Cases', () => {
216+
beforeAll(async () => {
217+
global.fetch = jest.fn(() => {
218+
return Promise.resolve({
219+
ok: true,
220+
status: 200,
221+
json: () => Promise.resolve(readMockUFCResponse(MOCK_UFC_RESPONSE_FILE)),
222+
});
223+
}) as jest.Mock;
224+
225+
await init(storage);
226+
});
227+
228+
afterAll(() => {
229+
jest.restoreAllMocks();
230+
});
231+
228232
it.each(readAssignmentTestData())(
229233
'test variation assignment splits',
230234
async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
@@ -261,19 +265,20 @@ describe('EppoClient E2E test', () => {
261265
});
262266

263267
describe('UFC Obfuscated Test Cases', () => {
264-
const storage = new TestConfigurationStore();
265-
266268
beforeAll(async () => {
267-
mock.setup();
268-
mock.get(flagEndpoint, (_req, res) => {
269-
const ufc = readMockUFCResponse(OBFUSCATED_MOCK_UFC_RESPONSE_FILE);
270-
return res.status(200).body(JSON.stringify(ufc));
271-
});
269+
global.fetch = jest.fn(() => {
270+
return Promise.resolve({
271+
ok: true,
272+
status: 200,
273+
json: () => Promise.resolve(readMockUFCResponse(OBFUSCATED_MOCK_UFC_RESPONSE_FILE)),
274+
});
275+
}) as jest.Mock;
276+
272277
await init(storage);
273278
});
274279

275280
afterAll(() => {
276-
mock.teardown();
281+
jest.restoreAllMocks();
277282
});
278283

279284
it.each(readAssignmentTestData())(
@@ -575,32 +580,33 @@ describe('EppoClient E2E test', () => {
575580

576581
describe('Eppo Client constructed with configuration request parameters', () => {
577582
let client: EppoClient;
578-
let storage: IConfigurationStore;
583+
let thisStorage: IConfigurationStore;
579584
let requestConfiguration: FlagConfigurationRequestParameters;
580-
let mockServerResponseFunc: (res: MockResponse) => MockResponse;
581585

582-
const ufcBody = JSON.stringify(readMockUFCResponse(MOCK_UFC_RESPONSE_FILE));
583586
const flagKey = 'numeric_flag';
584587
const subject = 'alice';
585588
const pi = 3.1415926;
586589

587590
const maxRetryDelay = POLL_INTERVAL_MS * POLL_JITTER_PCT;
588591

589-
beforeAll(() => {
590-
mock.setup();
591-
mock.get(flagEndpoint, (_req, res) => {
592-
return mockServerResponseFunc(res);
593-
});
592+
beforeAll(async () => {
593+
global.fetch = jest.fn(() => {
594+
return Promise.resolve({
595+
ok: true,
596+
status: 200,
597+
json: () => Promise.resolve(readMockUFCResponse(MOCK_UFC_RESPONSE_FILE)),
598+
});
599+
}) as jest.Mock;
594600
});
595601

596-
beforeEach(() => {
597-
storage = new TestConfigurationStore();
602+
beforeEach(async () => {
598603
requestConfiguration = {
599604
apiKey: 'dummy key',
600605
sdkName: 'js-client-sdk-common',
601-
sdkVersion: packageJson.version,
606+
sdkVersion: '1.0.0',
602607
};
603-
mockServerResponseFunc = (res) => res.status(200).body(ufcBody);
608+
609+
thisStorage = new TestConfigurationStore();
604610

605611
// We only want to fake setTimeout() and clearTimeout()
606612
jest.useFakeTimers({
@@ -629,11 +635,11 @@ describe('EppoClient E2E test', () => {
629635
});
630636

631637
afterAll(() => {
632-
mock.teardown();
638+
jest.restoreAllMocks();
633639
});
634640

635641
it('Fetches initial configuration with parameters in constructor', async () => {
636-
client = new EppoClient(storage, requestConfiguration);
642+
client = new EppoClient(thisStorage, requestConfiguration);
637643
client.setIsGracefulFailureMode(false);
638644
// no configuration loaded
639645
let variation = client.getNumericAssignment(flagKey, subject, {}, 123.4);
@@ -645,7 +651,7 @@ describe('EppoClient E2E test', () => {
645651
});
646652

647653
it('Fetches initial configuration with parameters provided later', async () => {
648-
client = new EppoClient(storage);
654+
client = new EppoClient(thisStorage);
649655
client.setIsGracefulFailureMode(false);
650656
client.setConfigurationRequestParameters(requestConfiguration);
651657
// no configuration loaded
@@ -662,22 +668,33 @@ describe('EppoClient E2E test', () => {
662668
{ pollAfterSuccessfulInitialization: true },
663669
])('retries initial configuration request with config %p', async (configModification) => {
664670
let callCount = 0;
665-
mockServerResponseFunc = (res) => {
671+
672+
global.fetch = jest.fn(() => {
666673
if (++callCount === 1) {
667-
// Throw an error for the first call
668-
return res.status(500);
674+
// Simulate an error for the first call
675+
return Promise.resolve({
676+
ok: false,
677+
status: 500,
678+
json: () => Promise.reject(new Error('Server error')),
679+
});
669680
} else {
670-
// Return a mock object for subsequent calls
671-
return res.status(200).body(ufcBody);
681+
// Return a successful response for subsequent calls
682+
return Promise.resolve({
683+
ok: true,
684+
status: 200,
685+
json: () => {
686+
return readMockUFCResponse(MOCK_UFC_RESPONSE_FILE);
687+
},
688+
});
672689
}
673-
};
690+
}) as jest.Mock;
674691

675692
const { pollAfterSuccessfulInitialization } = configModification;
676693
requestConfiguration = {
677694
...requestConfiguration,
678695
pollAfterSuccessfulInitialization,
679696
};
680-
client = new EppoClient(storage, requestConfiguration);
697+
client = new EppoClient(thisStorage, requestConfiguration);
681698
client.setIsGracefulFailureMode(false);
682699
// no configuration loaded
683700
let variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
@@ -711,15 +728,24 @@ describe('EppoClient E2E test', () => {
711728
{ pollAfterFailedInitialization: true, throwOnFailedInitialization: true },
712729
])('initial configuration request fails with config %p', async (configModification) => {
713730
let callCount = 0;
714-
mockServerResponseFunc = (res) => {
731+
732+
global.fetch = jest.fn(() => {
715733
if (++callCount === 1) {
716-
// Throw an error for initialization call
717-
return res.status(500);
734+
// Simulate an error for the first call
735+
return Promise.resolve({
736+
ok: false,
737+
status: 500,
738+
json: () => Promise.reject(new Error('Server error')),
739+
} as Response);
718740
} else {
719-
// Return a mock object for subsequent calls
720-
return res.status(200).body(ufcBody);
741+
// Return a successful response for subsequent calls
742+
return Promise.resolve({
743+
ok: true,
744+
status: 200,
745+
json: () => Promise.resolve(readMockUFCResponse(MOCK_UFC_RESPONSE_FILE)),
746+
} as Response);
721747
}
722-
};
748+
});
723749

724750
const { pollAfterFailedInitialization, throwOnFailedInitialization } = configModification;
725751

@@ -733,7 +759,7 @@ describe('EppoClient E2E test', () => {
733759
throwOnFailedInitialization,
734760
pollAfterFailedInitialization,
735761
};
736-
client = new EppoClient(storage, requestConfiguration);
762+
client = new EppoClient(thisStorage, requestConfiguration);
737763
client.setIsGracefulFailureMode(false);
738764
// no configuration loaded
739765
expect(client.getNumericAssignment(flagKey, subject, {}, 0.0)).toBe(0.0);

src/client/eppo-client.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import axios from 'axios';
2-
31
import {
42
AssignmentCache,
53
Cacheable,
@@ -19,8 +17,8 @@ import {
1917
import { decodeFlag } from '../decoding';
2018
import { EppoValue } from '../eppo_value';
2119
import { Evaluator, FlagEvaluation, noneResult } from '../evaluator';
22-
import ExperimentConfigurationRequestor from '../flag-configuration-requestor';
23-
import HttpClient from '../http-client';
20+
import FlagConfigurationRequestor from '../flag-configuration-requestor';
21+
import FetchHttpClient from '../http-client';
2422
import { Flag, ObfuscatedFlag, VariationType } from '../interfaces';
2523
import { getMD5Hash } from '../obfuscation';
2624
import initPoller, { IPoller } from '../poller';
@@ -152,16 +150,17 @@ export default class EppoClient implements IEppoClient {
152150
this.requestPoller.stop();
153151
}
154152

155-
const axiosInstance = axios.create({
156-
baseURL: this.configurationRequestParameters.baseUrl || DEFAULT_BASE_URL,
157-
timeout: this.configurationRequestParameters.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
158-
});
159-
const httpClient = new HttpClient(axiosInstance, {
160-
apiKey: this.configurationRequestParameters.apiKey,
161-
sdkName: this.configurationRequestParameters.sdkName,
162-
sdkVersion: this.configurationRequestParameters.sdkVersion,
163-
});
164-
const configurationRequestor = new ExperimentConfigurationRequestor(
153+
// todo: consider injecting the IHttpClient interface
154+
const httpClient = new FetchHttpClient(
155+
this.configurationRequestParameters.baseUrl || DEFAULT_BASE_URL,
156+
{
157+
apiKey: this.configurationRequestParameters.apiKey,
158+
sdkName: this.configurationRequestParameters.sdkName,
159+
sdkVersion: this.configurationRequestParameters.sdkVersion,
160+
},
161+
this.configurationRequestParameters.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
162+
);
163+
const configurationRequestor = new FlagConfigurationRequestor(
165164
this.configurationStore,
166165
httpClient,
167166
);

src/flag-configuration-requestor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IConfigurationStore } from './configuration-store';
2-
import HttpClient from './http-client';
2+
import { IHttpClient } from './http-client';
33
import { Flag } from './interfaces';
44

55
const UFC_ENDPOINT = '/flag-config/v1/config';
@@ -9,7 +9,7 @@ interface IUniversalFlagConfig {
99
}
1010

1111
export default class FlagConfigurationRequestor {
12-
constructor(private configurationStore: IConfigurationStore, private httpClient: HttpClient) {}
12+
constructor(private configurationStore: IConfigurationStore, private httpClient: IHttpClient) {}
1313

1414
async fetchAndStoreConfigurations(): Promise<Record<string, Flag>> {
1515
const responseData = await this.httpClient.get<IUniversalFlagConfig>(UFC_ENDPOINT);

0 commit comments

Comments
 (0)