Skip to content

Commit

Permalink
feat: enable local evaluation (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
duyhungtnn authored Jan 28, 2025
1 parent 0ba9617 commit d2e5a2f
Show file tree
Hide file tree
Showing 67 changed files with 6,386 additions and 308 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
required: true
E2E_TOKEN:
required: true
E2E_SERVER_ROLE_TOKEN:
required: true
NPM_TOKEN:
required: true

Expand Down Expand Up @@ -43,5 +45,5 @@ jobs:
run: make build
- name: e2e test
run: |
sed -i -e "s|<HOST>|${{ secrets.E2E_HOST }}|" -e "s|<TOKEN>|${{ secrets.E2E_TOKEN }}|" ava-e2e.config.mjs
sed -i -e "s|<HOST>|${{ secrets.E2E_HOST }}|" -e "s|<TOKEN>|${{ secrets.E2E_TOKEN }}|" -e "s|<SERVER_ROLE_TOKEN>|${{ secrets.E2E_SERVER_ROLE_TOKEN }}|" ava-e2e.config.mjs
make e2e
1 change: 1 addition & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,6 @@ jobs:
secrets:
E2E_HOST: ${{ secrets.E2E_HOST }}
E2E_TOKEN: ${{ secrets.E2E_TOKEN }}
E2E_SERVER_ROLE_TOKEN: ${{ secrets.E2E_SERVER_ROLE_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN}}

6 changes: 5 additions & 1 deletion ava-e2e.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ export default {
configFile: false,
},
},
files: ['__e2e/__test__/*.js'],
files: [
'__e2e/__test__/*.js',
'__e2e/__test__/local_evaluation/*.js'
],
environmentVariables: {
HOST: '<HOST>', // replace this. e.g. api-dev.bucketeer.jp
TOKEN: '<TOKEN>', // replace this.
SERVER_ROLE_TOKEN: '<SERVER_ROLE_TOKEN>', // replace this with the server role token for testing with local evaluate
},
};
19 changes: 18 additions & 1 deletion ava-test.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
export default {
failFast: true,
failWithoutAssertions: false,
babel: {
testOptions: {
babelrc: false,
configFile: false,
},
},
files: ['__test/**/__tests__/*.js'],
files: [
'__test/**/__tests__/**/*.js',
'!__test/**/__tests__/utils/**',
'!__test/**/__tests__/testdata/**',
'!__test/**/__tests__/mocks/**',
],
"typescript": {
"extensions": [
"ts",
"tsx"
],
"rewritePaths": {
"src/": "build/"
},
"compile": "tsc"
}
};
5 changes: 2 additions & 3 deletions e2e/client.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import test from 'ava'
import { initialize, DefaultLogger, BKTClientImpl } from '../lib';
import { initialize, DefaultLogger } from '../lib';
import { HOST, TOKEN, FEATURE_TAG, TARGETED_USER_ID, FEATURE_ID_BOOLEAN } from './constants/constants';
import { MetricsEvent, isMetricsEvent } from '../lib/objects/metricsEvent';
import { ApiId } from '../lib/objects/apiId';
import { BKTClientImpl } from '../lib/client';

const FORBIDDEN_ERROR_METRICS_EVENT_NAME =
'type.googleapis.com/bucketeer.event.client.ForbiddenErrorMetricsEvent';
const NOT_FOUND_ERROR_METRICS_EVENT_NAME =
'type.googleapis.com/bucketeer.event.client.NotFoundErrorMetricsEvent';
const UNKNOWN_ERROR_METRICS_EVENT_NAME =
'type.googleapis.com/bucketeer.event.client.UnknownErrorMetricsEvent';

//Note: There is a different compared to other SDK clients.
test('Using a random string in the api key setting should not throw exception', async (t) => {
Expand Down
2 changes: 2 additions & 0 deletions e2e/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export const HOST = process.env.HOST!;
export const TOKEN = process.env.TOKEN!;
export const SERVER_ROLE_TOKEN = process.env.SERVER_ROLE_TOKEN!;
export const FEATURE_TAG = 'nodejs'
export const TARGETED_USER_ID = 'bucketeer-nodejs-server-user-id-1'
export const TARGETED_SEGMENT_USER_ID = 'bucketeer-nodejs-server-user-id-2'

export const FEATURE_ID_BOOLEAN = 'feature-nodejs-server-e2e-boolean'
export const FEATURE_ID_STRING = 'feature-nodejs-server-e2e-string'
Expand Down
2 changes: 1 addition & 1 deletion e2e/evaluations_defaut_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ test('stringVariation', async (t) => {
await bktClient.stringVariationDetails(defaultUser, FEATURE_ID_STRING, ''),
{
featureId: FEATURE_ID_STRING,
featureVersion: 4,
featureVersion: 22,
userId: defaultUser.id,
variationId: '16a9db43-dfba-485c-8300-8747af5caf61',
variationName: 'variation 1',
Expand Down
108 changes: 108 additions & 0 deletions e2e/evaluations_segment_user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import anyTest, { TestFn } from 'ava';
import { Bucketeer, DefaultLogger, User, initialize } from '../lib';
import { HOST, FEATURE_TAG, TARGETED_SEGMENT_USER_ID, FEATURE_ID_BOOLEAN, FEATURE_ID_STRING, FEATURE_ID_INT, FEATURE_ID_JSON, FEATURE_ID_FLOAT, TOKEN } from './constants/constants';

const test = anyTest as TestFn<{ bktClient: Bucketeer; targetedSegmentUser: User }>;

test.before( async (t) => {
t.context = {
bktClient: initialize({
host: HOST,
token: TOKEN,
tag: FEATURE_TAG,
logger: new DefaultLogger('error'),
enableLocalEvaluation: false,
cachePollingInterval: 3000,
}),
targetedSegmentUser: { id: TARGETED_SEGMENT_USER_ID, data: {} },
};
});

test.after(async (t) => {
const { bktClient } = t.context;
bktClient.destroy();
});

test('boolVariation', async (t) => {
const { bktClient, targetedSegmentUser } = t.context;
t.is(await bktClient.booleanVariation(targetedSegmentUser, FEATURE_ID_BOOLEAN, false), true);
t.deepEqual(
await bktClient.booleanVariationDetails(targetedSegmentUser, FEATURE_ID_BOOLEAN, false),
{
featureId: FEATURE_ID_BOOLEAN,
featureVersion: 5,
userId: targetedSegmentUser.id,
variationId: 'f948b6dd-c366-4828-8ee0-72edbe2c0eea',
variationName: 'variation 1',
variationValue: true,
reason: 'DEFAULT',
}
)
});

test('stringVariation', async (t) => {
const { bktClient, targetedSegmentUser } = t.context;
t.is(await bktClient.stringVariation(targetedSegmentUser, FEATURE_ID_STRING, ''), 'value-3');
t.deepEqual(
await bktClient.stringVariationDetails(targetedSegmentUser, FEATURE_ID_STRING, 'true'),
{
featureId: FEATURE_ID_STRING,
featureVersion: 22,
userId: targetedSegmentUser.id,
variationId: 'e92fa326-2c7a-45f2-aaf7-ab9eb59f0ccf',
variationName: 'variation 3',
variationValue: 'value-3',
reason: 'RULE',
}
)
});

test('numberVariation', async (t) => {
const { bktClient, targetedSegmentUser } = t.context;
t.is(await bktClient.numberVariation(targetedSegmentUser, FEATURE_ID_INT, 0), 10);
t.deepEqual(
await bktClient.numberVariationDetails(targetedSegmentUser, FEATURE_ID_INT, 1),
{
featureId: FEATURE_ID_INT,
featureVersion: 5,
userId: targetedSegmentUser.id,
variationId: '935ac588-c3ef-4bc8-915b-666369cdcada',
variationName: 'variation 1',
variationValue: 10,
reason: 'DEFAULT',
}
)

t.is(await bktClient.numberVariation(targetedSegmentUser, FEATURE_ID_FLOAT, 0.0), 2.1);
t.deepEqual(
await bktClient.numberVariationDetails(targetedSegmentUser, FEATURE_ID_FLOAT, 1.1),
{
featureId: FEATURE_ID_FLOAT,
featureVersion: 5,
userId: targetedSegmentUser.id,
variationId: '0b04a309-31cd-471f-acf0-0ea662d16737',
variationName: 'variation 1',
variationValue: 2.1,
reason: 'DEFAULT',
}
)

});

test('objectVariation', async (t) => {
const { bktClient, targetedSegmentUser } = t.context;
t.deepEqual(await bktClient.getJsonVariation(targetedSegmentUser, FEATURE_ID_JSON, {}), { "str": "str1", "int": "int1" });
t.deepEqual(await bktClient.objectVariation(targetedSegmentUser, FEATURE_ID_JSON, {}), { "str": "str1", "int": "int1" });
t.deepEqual(
await bktClient.objectVariationDetails(targetedSegmentUser, FEATURE_ID_JSON, {}),
{
featureId: FEATURE_ID_JSON,
featureVersion: 5,
userId: targetedSegmentUser.id,
variationId: 'ff8299ed-80c9-4d30-9e92-a55750ad3ffb',
variationName: 'variation 1',
variationValue: { str: 'str1', int: 'int1' },
reason: 'DEFAULT',
}
)
});
2 changes: 1 addition & 1 deletion e2e/evaluations_targeting_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ test('stringVariation', async (t) => {
await bktClient.stringVariationDetails(targetedUser, FEATURE_ID_STRING, 'true'),
{
featureId: FEATURE_ID_STRING,
featureVersion: 4,
featureVersion: 22,
userId: targetedUser.id,
variationId: 'a3336346-931e-40f4-923a-603c642285d7',
variationName: 'variation 2',
Expand Down
25 changes: 23 additions & 2 deletions e2e/events.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import anyTest, { TestFn } from 'ava';
import { Bucketeer, DefaultLogger, User, initialize } from '../lib';
import { HOST, TOKEN, FEATURE_TAG, TARGETED_USER_ID, FEATURE_ID_BOOLEAN, FEATURE_ID_STRING, FEATURE_ID_INT, FEATURE_ID_JSON, FEATURE_ID_FLOAT, GOAL_ID, GOAL_VALUE } from './constants/constants';
import { BKTClientImpl } from '../lib';
import { BKTClientImpl } from '../lib/client';
import { isGoalEvent } from '../lib/objects/goalEvent';
import { isMetricsEvent } from '../lib/objects/metricsEvent';
import { isEvaluationEvent } from '../lib/objects/evaluationEvent';
import { isStatusErrorMetricsEvent } from '../lib/objects/status';

const test = anyTest as TestFn<{ bktClient: Bucketeer; targetedUser: User }>;

Expand Down Expand Up @@ -32,7 +33,7 @@ test('goal event', async (t) => {
t.true(events.some((e: { event: any; }) => (isGoalEvent(e.event))))
});

test('default evaluation event', async (t) => {
test('evaluation event', async (t) => {
const { bktClient, targetedUser } = t.context;
t.is(await bktClient.booleanVariation(targetedUser, FEATURE_ID_BOOLEAN, true), false);
t.deepEqual(await bktClient.getJsonVariation(targetedUser, FEATURE_ID_JSON, {}), { "str": "str2", "int": "int2" });
Expand All @@ -48,8 +49,28 @@ test('default evaluation event', async (t) => {
t.true(events.some((e) => (isMetricsEvent(e.event))));
});

test('default evaluation event', async (t) => {
const { bktClient, targetedUser } = t.context;
const notFoundFeatureId = 'not-found-feature-id';
t.is(await bktClient.booleanVariation(targetedUser, notFoundFeatureId, true), true);
t.deepEqual(await bktClient.getJsonVariation(targetedUser, notFoundFeatureId, { "str": "str2",}), { "str": "str2" });
t.deepEqual(await bktClient.objectVariation(targetedUser, notFoundFeatureId, { "str": "str2" }), { "str": "str2" });
t.is(await bktClient.numberVariation(targetedUser, notFoundFeatureId, 10), 10);
t.is(await bktClient.numberVariation(targetedUser, notFoundFeatureId, 3.3), 3.3);
t.is(await bktClient.stringVariation(targetedUser, notFoundFeatureId, 'value-9'), 'value-9');
const bktClientImpl = bktClient as BKTClientImpl
const events = bktClientImpl.eventStore.getAll()
// (DefaultEvaluationEvent, Error Event) x 6
t.is(events.length, 12);
t.true(events.some((e) => (isEvaluationEvent(e.event))));
t.true(events.some((e) => (isMetricsEvent(e.event))));
t.true(events.some((e) => (isStatusErrorMetricsEvent(e.event, NOT_FOUND_ERROR_METRICS_EVENT_NAME))));
});

test.afterEach(async (t) => {
const { bktClient } = t.context;
bktClient.destroy();
});

const NOT_FOUND_ERROR_METRICS_EVENT_NAME =
'type.googleapis.com/bucketeer.event.client.NotFoundErrorMetricsEvent';
116 changes: 116 additions & 0 deletions e2e/local_evaluation/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import test from 'ava'
import { initialize, DefaultLogger } from '../../lib';
import { HOST, TOKEN, FEATURE_TAG, TARGETED_USER_ID, FEATURE_ID_BOOLEAN, SERVER_ROLE_TOKEN } from '../constants/constants';
import { isMetricsEvent } from '../../lib/objects/metricsEvent';
import { BKTClientImpl } from '../../lib/client';

test('Using a random string in the api key setting should not throw exception', async (t) => {
const bktClient = initialize({
host: HOST,
token: "TOKEN_RANDOM",
tag: FEATURE_TAG,
cachePollingInterval: 1000,
enableLocalEvaluation: true,
logger: new DefaultLogger("error")
});

await new Promise((resolve) => {
setTimeout(resolve, 3000);
});

const user = { id: TARGETED_USER_ID, data: {} }
// The client can not load the evaluation, we will received the default value `true`
// Other SDK clients e2e test will expect the value is `false`
const result = await t.notThrowsAsync(bktClient.booleanVariation(user, FEATURE_ID_BOOLEAN, true));
t.true(result);

const bktClientImpl = bktClient as BKTClientImpl
const events = bktClientImpl.eventStore.getAll()
t.true(events.some((e) => {
return isMetricsEvent(e.event);
}));

bktClient.destroy()
});

test('altering featureTag should not affect api request', async (t) => {
const config = {
host: HOST,
token: SERVER_ROLE_TOKEN,
tag: FEATURE_TAG,
cachePollingInterval: 1000,
enableLocalEvaluation: true,
logger: new DefaultLogger("error")
}

const bktClient = initialize(config);
await new Promise((resolve) => {
setTimeout(resolve, 3000);
});

const user = { id: TARGETED_USER_ID, data: {} }
const result = await t.notThrowsAsync(bktClient.booleanVariation(user, FEATURE_ID_BOOLEAN, false));
t.true(result);
config.tag = "RANDOME"

const resultAfterAlterAPIKey = await t.notThrowsAsync(bktClient.booleanVariation(user, FEATURE_ID_BOOLEAN, false));
t.true(resultAfterAlterAPIKey);

bktClient.destroy()
});

test('Altering the api key should not affect api request', async (t) => {
const config = {
host: HOST,
token: SERVER_ROLE_TOKEN,
tag: FEATURE_TAG,
cachePollingInterval: 1000,
enableLocalEvaluation: true,
logger: new DefaultLogger("error")
}

const bktClient = initialize(config);
await new Promise((resolve) => {
setTimeout(resolve, 3000);
});

const user = { id: TARGETED_USER_ID, data: {} }
const result = await t.notThrowsAsync(bktClient.booleanVariation(user, FEATURE_ID_BOOLEAN, false));
t.true(result);
config.token = "RANDOME"

const resultAfterAlterAPIKey = await t.notThrowsAsync(bktClient.booleanVariation(user, FEATURE_ID_BOOLEAN, false));
t.true(resultAfterAlterAPIKey);

bktClient.destroy()
});

//Note: There is a different compared to other SDK clients.
test('Using a random string in the featureTag setting should affect api request', async (t) => {
const bktClient = initialize({
host: HOST,
token: SERVER_ROLE_TOKEN,
tag: "RANDOM",
cachePollingInterval: 1000,
enableLocalEvaluation: true,
logger: new DefaultLogger("error")
});

await new Promise((resolve) => {
setTimeout(resolve, 3000);
});

const user = { id: TARGETED_USER_ID, data: {} }
const result = await t.notThrowsAsync(bktClient.booleanVariation(user, FEATURE_ID_BOOLEAN, true));
// The client can not load the evaluation, we will received the default value `true`
// Other SDK clients e2e test will expect the value is `false`
t.true(result);

const bktClientImpl = bktClient as BKTClientImpl
const events = bktClientImpl.eventStore.getAll()
t.true(events.some((e) => {
return isMetricsEvent(e.event);
}));

bktClient.destroy()
});
Loading

0 comments on commit d2e5a2f

Please sign in to comment.