Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable local evaluation #79

Merged
merged 9 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading