Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
01d43fd
lint: Add custom ESLint rule: no-spread-after-defaults
duyhungtnn Aug 11, 2025
17fc53a
refactor: config merging and add ESLint test target
duyhungtnn Aug 11, 2025
016ac23
fix: typo in initialization comment
duyhungtnn Aug 11, 2025
4f52f71
test: add tests for defineBKTConfig function
duyhungtnn Aug 11, 2025
f252dea
refactor: config to support BKTConfig and internal mapping
duyhungtnn Aug 13, 2025
5f88667
refactor: internal config and add comprehensive tests
duyhungtnn Aug 13, 2025
b7b67b4
tests: for defineBKTConfig and deprecate defaultConfig
duyhungtnn Aug 13, 2025
6eac182
chore: add convertConfigToBKTConfig and update client config usage
duyhungtnn Aug 13, 2025
4866d90
chore: add refactor_plan
duyhungtnn Aug 14, 2025
49ffc83
chore: update Node version and clean up test imports
duyhungtnn Aug 14, 2025
68e5a36
test: update logger test to check for exact instance
duyhungtnn Aug 18, 2025
8ccaa64
test: add logger to config test for provided values
duyhungtnn Aug 18, 2025
90d084e
test: refactor and expand defineBKTConfig test coverage
duyhungtnn Aug 18, 2025
24c24db
fix: config flush interval and refactor event usage
duyhungtnn Aug 19, 2025
a214546
test: update default eventsFlushInterval in tests to 30000
duyhungtnn Aug 19, 2025
7210767
refactor: config exports for improved encapsulation
duyhungtnn Aug 19, 2025
1f3d222
feat: add sourceId parameter to event creation functions
duyhungtnn Aug 20, 2025
8c1b68c
feat: add sourceId to metrics event creation functions
duyhungtnn Aug 21, 2025
233abb5
feat: add sourceId parameter to API client methods
duyhungtnn Aug 21, 2025
a6ef42a
feat: add sourceId to gRPC requests and processors
duyhungtnn Aug 22, 2025
bdbac71
chore: mark initialize as deprecated in Bucketeer SDK
duyhungtnn Aug 22, 2025
dd488de
refactor: tests to use defineBKTConfig and initializeBKTClient
duyhungtnn Aug 22, 2025
ef94689
refactor: imports to use public API for config and client
duyhungtnn Aug 22, 2025
0a42461
Add tests for deprecated client init and event sourceId
duyhungtnn Aug 22, 2025
7caeeef
test: update e2e tests for wrapper SDK sourceId and sdkVersion
duyhungtnn Aug 22, 2025
405ef75
fix: SourceId import paths in e2e tests
duyhungtnn Aug 22, 2025
41eac88
refactor: error exports and optimize SourceId lookup
duyhungtnn Aug 25, 2025
f3408a3
refactor: to use explicit sdkVersion in event and API calls
duyhungtnn Aug 25, 2025
feedb82
chore: add sdkVersion to processor and event handling
duyhungtnn Aug 25, 2025
5822c3c
fix: replace version with nodeSDKVersion in event tests
duyhungtnn Aug 26, 2025
535bdaf
chore: add sdkVersion param and fix interface formatting
duyhungtnn Aug 26, 2025
e972fee
fix: argument order in createInternalSdkErrorMetricsEvent call
duyhungtnn Aug 26, 2025
28d175d
refactor: internal_config tests for clarity and type safety
duyhungtnn Aug 26, 2025
735ef96
chore: add logging for invalid config values and refactor formatting
duyhungtnn Aug 26, 2025
590b6cf
chore: format and clean up sourceId type definitions
duyhungtnn Aug 26, 2025
8c110f1
Add type-check step to CI and update configs
duyhungtnn Sep 3, 2025
1147369
Delete REFACTOR_PLAN.md
duyhungtnn Sep 3, 2025
fce7ccf
fix: lint fail
duyhungtnn Sep 3, 2025
d8d80c4
fix: trigger cache update immediately on processor start
duyhungtnn Sep 3, 2025
c68681d
Add test-single target to Makefile and update README
duyhungtnn Sep 3, 2025
5909ac9
Increase cachePollingInterval in local evaluation tests
duyhungtnn Sep 3, 2025
a9daeec
Update CI workflow and dependencies
duyhungtnn Sep 3, 2025
d268fbb
Update GitHub Actions to use specific commit SHAs
duyhungtnn Sep 3, 2025
3a9d776
Fix import path for FEATURE_FLAG_REQUESTED_AT in tests
duyhungtnn Sep 3, 2025
e9e726e
Update README.md
duyhungtnn Sep 3, 2025
ecc5e46
Remove unused TypeScript config options
duyhungtnn Sep 3, 2025
7b1072e
Fix typo in cachePollingInterval validation comment
duyhungtnn Sep 3, 2025
d038eb5
Update README.md
duyhungtnn Sep 3, 2025
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
6 changes: 5 additions & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ jobs:
run: make fmt
- name: Lint
run: make lint
- name: Check types
run: make type-check
- name: Check custom ESLint rules
run: make test-eslint

unit-test:
name: Unit test
Expand Down Expand Up @@ -117,4 +121,4 @@ jobs:
run: make init
- name: Example Build
working-directory: ${{ env.EXAMPLE_PATH }}
run: make build
run: make build
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.13.0
18.18.2
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ test:
$(NPM_BIN_DIR)/babel src --extensions '.ts' --config-file "$(CURDIR)/babel-test.config.js" --out-dir "__test"
$(NPM_BIN_DIR)/ava --config ava-test.config.mjs

.PHONY: test-single
test-single:
rm -rf $(CURDIR)/__test
$(NPM_BIN_DIR)/babel src --extensions '.ts' --config-file "$(CURDIR)/babel-test.config.js" --out-dir "__test"
$(NPM_BIN_DIR)/ava --config ava-test.config.mjs $(filter-out $@,$(MAKECMDGOALS))

.PHONY: test-eslint
test-eslint:
node eslint-rules/no-spread-after-defaults.test.mjs

.PHONY: type-check
type-check:
$(NPM_BIN_DIR)/tsc --noEmit --project tsconfig.json
$(NPM_BIN_DIR)/tsc --noEmit --project tsconfig.test.json

.PHONY: e2e
e2e: copy-genfiles
rm -rf $(CURDIR)/__e2e
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ make init
make fmt
```

#### Lint
#### Lint & Type check

```bash
make lint
make type-check
```

#### Build
Expand Down Expand Up @@ -99,4 +100,10 @@ export NPM_TOKEN=<YOUR_NPM_TOKEN>
make publish
```

### Write tests
- Write tests in the `src/__tests__` directory. The test files should following snake_case naming convention. Its differ from the library code which uses camelCase.
- Use `ava` as the test runner. You can find the configuration in the [ava.config.mjs](./ava.config.mjs) file.
- Run all tests using `make test` command.
- Run single test using `make test-single <path-to-test-file>` command.

**Note:** The publishing process is automated using [GitHub Actions](https://github.com/bucketeer-io/node-server-sdk/blob/master/.github/workflows/release.yml)https://github.com/bucketeer-io/node-server-sdk/blob/master/.github/workflows/release.yml to publish it when the released tag is created.
174 changes: 112 additions & 62 deletions e2e/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import test from 'ava'
import { initialize, DefaultLogger } from '../lib';
import { HOST, TOKEN, FEATURE_TAG, TARGETED_USER_ID, FEATURE_ID_BOOLEAN } from './constants/constants';
import test from 'ava';
import { initializeBKTClient, defineBKTConfig, DefaultLogger, initialize } 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';
import { Config } from '../src';

const FORBIDDEN_ERROR_METRICS_EVENT_NAME =
'type.googleapis.com/bucketeer.event.client.ForbiddenErrorMetricsEvent';
Expand All @@ -12,93 +19,136 @@ const NOT_FOUND_ERROR_METRICS_EVENT_NAME =

//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) => {
const bktClient = initialize({
host: HOST,
token: "TOKEN_RANDOM",
tag: FEATURE_TAG,
logger: new DefaultLogger("error")
const config = defineBKTConfig({
apiEndpoint: HOST,
apiKey: 'TOKEN_RANDOM',
featureTag: FEATURE_TAG,
logger: new DefaultLogger('error'),
});
const user = { id: TARGETED_USER_ID, data: {} }
const bktClient = initializeBKTClient(config);
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()
const bktClientImpl = bktClient as BKTClientImpl;
const events = bktClientImpl.eventStore.getAll();
// The SDK skips generating error events for unauthorized errors, so no error events should be present
t.false(events.some((e) => {
if (isMetricsEvent(e.event)) {
const metrics = e.event as MetricsEvent
return metrics.event?.['@type'] === FORBIDDEN_ERROR_METRICS_EVENT_NAME && metrics.event?.apiId === ApiId.GET_EVALUATION
}
return false;
}));

bktClient.destroy()
t.false(
events.some((e) => {
if (isMetricsEvent(e.event)) {
const metrics = e.event as MetricsEvent;
return (
metrics.event?.['@type'] === FORBIDDEN_ERROR_METRICS_EVENT_NAME &&
metrics.event?.apiId === ApiId.GET_EVALUATION
);
}
return false;
}),
);

bktClient.destroy();
});

test('altering featureTag should not affect api request', async (t) => {
const config = {
host: HOST,
token: TOKEN,
tag: FEATURE_TAG,
logger: new DefaultLogger("error")
}
const bktClient = initialize(config);
const user = { id: TARGETED_USER_ID, data: {} }
const result = await t.notThrowsAsync(bktClient.booleanVariation(user, FEATURE_ID_BOOLEAN, false));
const config = defineBKTConfig({
apiEndpoint: HOST,
apiKey: TOKEN,
featureTag: FEATURE_TAG,
logger: new DefaultLogger('error'),
});
const bktClient = initializeBKTClient(config);
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"
config.featureTag = 'RANDOME';

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

bktClient.destroy()
bktClient.destroy();
});

test('Altering the api key should not affect api request', async (t) => {
const config = {
host: HOST,
token: TOKEN,
tag: FEATURE_TAG,
logger: new DefaultLogger("error")
}
const bktClient = initialize(config);
const user = { id: TARGETED_USER_ID, data: {} }
const result = await t.notThrowsAsync(bktClient.booleanVariation(user, FEATURE_ID_BOOLEAN, false));
const config = defineBKTConfig({
apiEndpoint: HOST,
apiKey: TOKEN,
featureTag: FEATURE_TAG,
logger: new DefaultLogger('error'),
});
const bktClient = initializeBKTClient(config);
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"
config.apiKey = 'RANDOME';

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

bktClient.destroy()
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: TOKEN,
tag: "RANDOM",
logger: new DefaultLogger("error")
const config = defineBKTConfig({
apiEndpoint: HOST,
apiKey: TOKEN,
featureTag: 'RANDOM',
logger: new DefaultLogger('error'),
});
const user = { id: TARGETED_USER_ID, data: {} }
const bktClient = initializeBKTClient(config);
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) => {
if (isMetricsEvent(e.event)) {
const metrics = e.event as MetricsEvent
return metrics.event?.['@type'] === NOT_FOUND_ERROR_METRICS_EVENT_NAME && metrics.event?.apiId === ApiId.GET_EVALUATION
}
return false;
}));

bktClient.destroy()
});
const bktClientImpl = bktClient as BKTClientImpl;
const events = bktClientImpl.eventStore.getAll();
t.true(
events.some((e) => {
if (isMetricsEvent(e.event)) {
const metrics = e.event as MetricsEvent;
return (
metrics.event?.['@type'] === NOT_FOUND_ERROR_METRICS_EVENT_NAME &&
metrics.event?.apiId === ApiId.GET_EVALUATION
);
}
return false;
}),
);

bktClient.destroy();
});

test('The deprecated function for initializing the client should still work.', async (t) => {
const config: Config = {
host: HOST,
token: TOKEN,
tag: FEATURE_TAG,
logger: new DefaultLogger('error'),
};
const bktClient = initialize(config);
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();
});
58 changes: 36 additions & 22 deletions e2e/evaluations_defaut_strategy.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import anyTest, { TestFn } from 'ava';
import { Bucketeer, DefaultLogger, User, initialize } from '../lib';
import { HOST, TOKEN, FEATURE_TAG, FEATURE_ID_BOOLEAN, FEATURE_ID_STRING, FEATURE_ID_INT, FEATURE_ID_JSON, FEATURE_ID_FLOAT } from './constants/constants';
import { Bucketeer, DefaultLogger, User, defineBKTConfig, initializeBKTClient } from '../lib';
import {
HOST,
TOKEN,
FEATURE_TAG,
FEATURE_ID_BOOLEAN,
FEATURE_ID_STRING,
FEATURE_ID_INT,
FEATURE_ID_JSON,
FEATURE_ID_FLOAT,
} from './constants/constants';
import { assetEvaluationDetails } from './utils/assert';

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

test.beforeEach((t) => {
const config = defineBKTConfig({
apiEndpoint: HOST,
apiKey: TOKEN,
featureTag: FEATURE_TAG,
logger: new DefaultLogger('error'),
});
t.context = {
bktClient: initialize({
host: HOST,
token: TOKEN,
tag: FEATURE_TAG,
logger: new DefaultLogger("error")
}),
bktClient: initializeBKTClient(config),
defaultUser: { id: 'user-1', data: {} },
};
});
Expand All @@ -31,8 +41,8 @@ test('boolVariation', async (t) => {
variationName: 'variation 1',
variationValue: true,
reason: 'DEFAULT',
}
)
},
);
});

test('stringVariation', async (t) => {
Expand All @@ -49,8 +59,8 @@ test('stringVariation', async (t) => {
variationName: 'variation 1',
variationValue: 'value-1',
reason: 'DEFAULT',
}
)
},
);
});

test('numberVariation', async (t) => {
Expand All @@ -67,8 +77,8 @@ test('numberVariation', async (t) => {
variationName: 'variation 1',
variationValue: 10,
reason: 'DEFAULT',
}
)
},
);

t.is(await bktClient.numberVariation(defaultUser, FEATURE_ID_FLOAT, 0.0), 2.1);
assetEvaluationDetails(
Expand All @@ -82,15 +92,20 @@ test('numberVariation', async (t) => {
variationName: 'variation 1',
variationValue: 2.1,
reason: 'DEFAULT',
}
)

},
);
});

test('objectVariation', async (t) => {
const { bktClient, defaultUser } = t.context;
t.deepEqual(await bktClient.getJsonVariation(defaultUser, FEATURE_ID_JSON, {}), { "str": "str1", "int": "int1" });
t.deepEqual(await bktClient.objectVariation(defaultUser, FEATURE_ID_JSON, {}), { "str": "str1", "int": "int1" });
t.deepEqual(await bktClient.getJsonVariation(defaultUser, FEATURE_ID_JSON, {}), {
str: 'str1',
int: 'int1',
});
t.deepEqual(await bktClient.objectVariation(defaultUser, FEATURE_ID_JSON, {}), {
str: 'str1',
int: 'int1',
});
assetEvaluationDetails(
t,
await bktClient.objectVariationDetails(defaultUser, FEATURE_ID_JSON, {}),
Expand All @@ -102,12 +117,11 @@ test('objectVariation', async (t) => {
variationName: 'variation 1',
variationValue: { str: 'str1', int: 'int1' },
reason: 'DEFAULT',
}
)
},
);
});

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

Loading