Skip to content

Commit

Permalink
Improved async error handling, added Switcher.subscribeNotifyError (#64)
Browse files Browse the repository at this point in the history
* Improved async error handling, added Switcher.subscribeNotifyError

* Removed unreachable code
  • Loading branch information
petruki committed May 3, 2024
1 parent 00e6cd5 commit b8d839c
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 38 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ await switcher
.isItOn('FEATURE01');
```
In order to capture issues that may occur during the process, it is possible to log the error by subscribing to the error events.
```js
Switcher.subscribeNotifyError((error) => {
console.log(error);
});
```
6. **Hybrid mode**
Forcing Switchers to resolve remotely can help you define exclusive features that cannot be resolved locally.
This feature is ideal if you want to run the SDK in local mode but still want to resolve a specific switcher remotely.
Expand Down
18 changes: 18 additions & 0 deletions src/lib/utils/executionLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { ResultDetail } from '../../types/index.d.ts';
const logger: ExecutionLogger[] = [];

export default class ExecutionLogger {
private static _callbackError: (err: Error) => void;

key?: string;
input?: string[][];
response: ResultDetail = { result: false };
Expand Down Expand Up @@ -60,6 +62,22 @@ export default class ExecutionLogger {
logger.splice(0, logger.length);
}

/**
* Subscribe to error notifications
*/
static subscribeNotifyError(callbackError: (err: Error) => void) {
ExecutionLogger._callbackError = callbackError;
}

/**
* Push error notification
*/
static notifyError(error: Error) {
if (ExecutionLogger._callbackError) {
ExecutionLogger._callbackError(error);
}
}

private static hasExecution(log: ExecutionLogger, key: string, input: string[][] | undefined) {
return log.key === key && JSON.stringify(log.input) === JSON.stringify(input);
}
Expand Down
36 changes: 28 additions & 8 deletions src/switcher-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export class Switcher {
try {
await Switcher._auth();
await remote.checkSwitchers(
Switcher._context.url || '',
Switcher._get(Switcher._context.url, ''),
Switcher._context.token,
switcherKeys,
);
Expand Down Expand Up @@ -327,11 +327,12 @@ export class Switcher {

if (Switcher._isTokenExpired()) {
Switcher._updateSilentToken();
remote.checkAPIHealth(Switcher._get(Switcher._context.url, '')).then((isAlive) => {
if (isAlive) {
Switcher._auth();
}
});
remote.checkAPIHealth(Switcher._get(Switcher._context.url, ''))
.then((isAlive) => {
if (isAlive) {
Switcher._auth();
}
});
}
}

Expand Down Expand Up @@ -466,6 +467,8 @@ export class Switcher {
result = await this._executeRemoteCriteria();
}
} catch (err) {
Switcher._notifyError(err);

if (Switcher._options.silentMode) {
Switcher._updateSilentToken();
return this._executeLocalCriteria();
Expand Down Expand Up @@ -543,7 +546,8 @@ export class Switcher {

if (Switcher._isTokenExpired()) {
this.prepare(this._key, this._input)
.then(() => this.executeAsyncCheckCriteria());
.then(() => this.executeAsyncCheckCriteria())
.catch((err) => Switcher._notifyError(err));
} else {
this.executeAsyncCheckCriteria();
}
Expand All @@ -555,7 +559,23 @@ export class Switcher {

private executeAsyncCheckCriteria() {
remote.checkCriteria(Switcher._context, this._key, this._input, this._showDetail)
.then((response) => ExecutionLogger.add(response, this._key, this._input));
.then((response) => ExecutionLogger.add(response, this._key, this._input))
.catch((err) => Switcher._notifyError(err));
}

private static _notifyError(err: Error) {
ExecutionLogger.notifyError(err);
}

/**
* Subscribe to notify when an asynchronous error is thrown.
*
* It is usually used when throttle and silent mode are enabled.
*
* @param callback function to be called when an error is thrown
*/
static subscribeNotifyError(callback: (err: Error) => void) {
ExecutionLogger.subscribeNotifyError(callback);
}

private async _executeApiValidation() {
Expand Down
7 changes: 4 additions & 3 deletions test/playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ const _testThrottledAPICall = async () => {
switcher = Switcher.factory();
switcher.throttle(1000);

for (let index = 0; index < 10; index++) {
setInterval(async () => {
const time = Date.now();
const result = await switcher.isItOn(SWITCHER_KEY, [checkNumeric('1')]);
console.log(`Call #${index} - ${JSON.stringify(result)}}`);
}
console.log(`- ${Date.now() - time} ms - ${JSON.stringify(result)}`);
}, 1000);

Switcher.unloadSnapshot();
};
Expand Down
71 changes: 44 additions & 27 deletions test/switcher-functional.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, afterAll, afterEach, beforeEach,
assertEquals, assertNotEquals, assertRejects, assertThrows, assertFalse,
assertEquals, assertRejects, assertThrows, assertFalse,
assertSpyCalls, spy } from './deps.ts';
import { given, givenError, tearDown, assertTrue, generateAuth, generateResult, generateDetailedResult } from './helper/utils.ts'

Expand Down Expand Up @@ -45,6 +45,10 @@ describe('Integrated test - Switcher:', function () {
tearDown();
});

beforeEach(function() {
ExecutionLogger.clearLogger();
});

it('should be valid', async function () {
// given API responding properly
given('POST@/criteria/auth', generateAuth('[auth_token]', 5));
Expand Down Expand Up @@ -81,35 +85,15 @@ describe('Integrated test - Switcher:', function () {
const switcher = Switcher.factory();
switcher.throttle(1000);

const spyAsyncRemoteCriteria = spy(switcher, '_executeAsyncRemoteCriteria');
const spyPrepare = spy(switcher, '_executeAsyncRemoteCriteria');
const spyExecutionLogger = spy(ExecutionLogger, 'add');

assertTrue(await switcher.isItOn('FLAG_1')); // sync
assertTrue(await switcher.isItOn('FLAG_1')); // async
await new Promise(resolve => setTimeout(resolve, 100)); // wait resolve async Promise

let throttledRunTimer;
for (let index = 0; index < 10; index++) {
assertTrue(await switcher.isItOn('FLAG_1'));

if (index === 0) {
// First run calls API
assertEquals(0, switcher.nextRun);
assertSpyCalls(spyExecutionLogger, 1);
} else {
// Set up throttle for next API call
assertNotEquals(0, switcher.nextRun);
throttledRunTimer = switcher.nextRun;
}
}

assertSpyCalls(spyAsyncRemoteCriteria, 9);

// Next call should call the API again as the throttle has expired
await new Promise(resolve => setTimeout(resolve, 2000));
assertTrue(await switcher.isItOn('FLAG_1'));
assertSpyCalls(spyAsyncRemoteCriteria, 10);

// Throttle expired, set up new throttle run timer
assertNotEquals(throttledRunTimer, switcher.nextRun);
assertSpyCalls(spyExecutionLogger, 2);
assertSpyCalls(spyPrepare, 1);
assertSpyCalls(spyExecutionLogger, 2); // 1st (sync) + 2nd (async)
});

it('should be valid - throttle - with details', async function () {
Expand Down Expand Up @@ -172,6 +156,35 @@ describe('Integrated test - Switcher:', function () {
assertFalse(result);
assertSpyCalls(spyPrepare, 2);
});

it('should not crash when async checkCriteria fails', async function () {
// given API responding properly
// first API call
given('POST@/criteria/auth', generateAuth('[auth_token]', 1));
given('POST@/criteria', generateResult(true)); // before token expires

// test
let asyncErrorMessage = null;
Switcher.buildContext(contextSettings);
Switcher.subscribeNotifyError((error) => asyncErrorMessage = error.message);

const switcher = Switcher.factory();
switcher.throttle(1000);

assertTrue(await switcher.isItOn('FLAG_1')); // sync
assertTrue(await switcher.isItOn('FLAG_1')); // async

// Next call should call the API again - valid token but crashes on checkCriteria
await new Promise(resolve => setTimeout(resolve, 1000));
assertEquals(asyncErrorMessage, null);

// given
given('POST@/criteria', { message: 'error' }, 500);
assertTrue(await switcher.isItOn('FLAG_1')); // async

await new Promise(resolve => setTimeout(resolve, 1000));
assertEquals(asyncErrorMessage, 'Something went wrong: [checkCriteria] failed with status 500');
});
});

describe('force remote (hybrid):', function () {
Expand Down Expand Up @@ -306,10 +319,14 @@ describe('Integrated test - Switcher:', function () {
given('POST@/criteria', { error: 'Too many requests' }, 429);

// test
let asyncErrorMessage = null;
Switcher.buildContext(contextSettings, { silentMode: '5m', regexSafe: false, snapshotLocation: './snapshot/' });
Switcher.subscribeNotifyError((error) => asyncErrorMessage = error.message);

const switcher = Switcher.factory();

assertTrue(await switcher.isItOn('FF2FOR2022'));
assertEquals(asyncErrorMessage, 'Something went wrong: [checkCriteria] failed with status 429');
});

});
Expand Down

0 comments on commit b8d839c

Please sign in to comment.