Compare commits

...

79 Commits

Author SHA1 Message Date
Jonathan Norris 91ba360d36
feat(multi-provider): Add Track Method Support to Multi-Provider (#1323)
Signed-off-by: Jonathan Norris <jonathan@taplytics.com>
2025-07-11 13:39:13 -04:00
github-actions[bot] 0ff5c88135
chore(main): release config-cat-provider 0.7.6 (#1349)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-08 00:22:29 +02:00
renovate[bot] 601e7de199
fix(security): update dependency configcat-common to v9.4.0 (#1348)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 15:44:05 -04:00
renovate[bot] fca83c925c
fix(security): update vulnerability-updates (#1342)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 14:53:58 -04:00
Todd Baert e2404a480f
Update renovate.json
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 14:34:13 -04:00
Todd Baert 6776e9735a
Update renovate.json
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 14:29:19 -04:00
Todd Baert 0a6e44302d
Update renovate.json
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 14:24:19 -04:00
Todd Baert d6c4817948
Update renovate.json
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 13:55:54 -04:00
Todd Baert 1a913b8dba
chore: Update renovate.json
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 13:53:23 -04:00
Todd Baert 1b5ecc0959
chore: fix renovate.json
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 13:50:31 -04:00
Todd Baert 18268f8a21
chore: more renovate troubleshooting
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 13:34:15 -04:00
renovate[bot] ddbde614e9
fix(deps): update dependency @flipt-io/flipt-client-js to v0.2.0 (#1331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 13:27:33 -04:00
renovate[bot] e501475536
fix(deps): update dependency axios to v1.10.0 (#1332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 17:18:48 +00:00
renovate[bot] ea14c71e4d
chore(deps): update libs/shared/flagd-core/spec digest to a367871 (#1328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 17:09:12 +00:00
Todd Baert 6a6d23ba32
chore: troubleshooting
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 13:01:52 -04:00
Todd Baert 82fbf1e042 chore: fix dep version
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 13:00:24 -04:00
renovate[bot] 375193081a
fix(deps): update dependency @aws-sdk/client-ssm to v3.840.0 (#1329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 16:44:03 +00:00
renovate[bot] fbc5023097
chore(config): migrate renovate config (#1327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 11:33:51 -04:00
github-actions[bot] e6766c07e5
chore(main): release flagd-core 1.1.0 (#1156)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-04 11:19:27 -04:00
renovate[bot] 9c0f895af5
chore(deps): update libs/providers/flagd/spec digest to a367871 (#1326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 11:16:50 -04:00
Todd Baert cf9fe09cc1 chore: use dep dashboard
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-07-04 11:12:33 -04:00
github-actions[bot] 4e1202982e
chore(main): release ofrep-web-provider 0.3.3 (#1320)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-23 12:38:38 -04:00
Todd Baert 6ab7f1abfc
fix: remove incorrect undici dep (#1319)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-06-23 12:17:32 -04:00
renovate[bot] 1ffe54af81
fix(deps): update dependency axios to v1.9.0 (#1314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 00:58:04 +00:00
renovate[bot] ec5309a0a9
fix(deps): update dependency @aws-sdk/client-ssm to v3.826.0 (#1313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 20:52:22 +00:00
renovate[bot] 02a671ca7c
chore(deps): update swc monorepo (#1312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 16:52:28 +00:00
renovate[bot] 4f30cda7a3
chore(deps): update dependency testcontainers to v10.28.0 (#1311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 12:38:51 +00:00
renovate[bot] 141a37df58
chore(deps): update dependency @smithy/types to v4.3.1 (#1310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 10:33:14 +00:00
renovate[bot] d3ad93e3a9
fix(deps): update dependency @flipt-io/flipt-client-js to v0.0.6 (#1309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 10:30:01 +00:00
renovate[bot] d495ee346f
chore(deps): update dependency ts-jest to v29.3.4 (#1308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 05:59:41 +00:00
renovate[bot] b9ff7e1448
chore(deps): update libs/providers/flagd/spec digest to bb2dc2c (#1305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 01:44:58 +00:00
renovate[bot] 70518ac4c1
chore(deps): update libs/shared/flagd-core/spec digest to bb2dc2c (#1306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 23:58:24 +00:00
github-actions[bot] 6edf52e7c2
chore(main): release flipt-web-provider 0.1.5 (#1304)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-06 14:52:36 +00:00
renovate[bot] 61b46c9687
chore(deps): update dependency libs/shared/flagd-core/test-harness to v2 (#1218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 14:52:14 +00:00
Philipp Jardas 9e8496a384
feat(flipt-web): update types to match flipt-client-js (#1303)
Signed-off-by: Philipp Jardas <philipp@jardas.de>
2025-06-06 10:48:46 -04:00
renovate[bot] 6dd71ac73a
chore(deps): update libs/shared/flagd-core/flagd-schemas digest to 2852d77 (#1301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 16:11:27 +00:00
renovate[bot] 8cdd5b4524
chore(deps): update libs/providers/flagd/spec digest to f014806 (#1300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 11:49:40 +00:00
renovate[bot] 4fe9c7e232
chore(deps): update libs/providers/flagd/schemas digest to 2852d77 (#1299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 05:07:23 +00:00
renovate[bot] e83fa88790
chore(deps): update libs/providers/flagd-web/schemas digest to 2852d77 (#1298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 02:26:46 +00:00
renovate[bot] 771dcd522b
chore(deps): update actions/setup-node digest to 49933ea (#1297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 02:23:16 +00:00
renovate[bot] 182077cf32
chore(deps): update libs/shared/flagd-core/spec digest to f014806 (#1206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-04 21:50:07 +00:00
Simon Schrottner fbd9f9155d
test(flagd): rework e2e tests to new format (#1129)
Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
2025-06-04 15:59:55 -04:00
github-actions[bot] 24a7e0e3bb
chore(main): release flipt-web-provider 0.1.4 (#1295)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
2025-06-04 14:54:27 -04:00
github-actions[bot] c1b00f69ee
chore(main): release aws-ssm-provider 0.1.3 (#1296)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-04 14:51:07 -04:00
Todd Baert 3045e0cac8
chore: remove non-feat-fix release config
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-06-04 14:23:49 -04:00
Todd Baert 4dbcbf1870
chore: add "type" import enforcement lint rule and apply (#1292)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-05-30 13:06:18 -04:00
Todd Baert 8d2fd484db
chore: update ci to node 20+ (#1291)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-05-29 15:36:27 -04:00
github-actions[bot] a0cdef59fd
chore(main): release go-feature-flag-web-provider 0.2.6 (#1262)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-28 08:56:07 +02:00
Thomas Poignant d9ffcec165
feat(goff web): Support tracking events (#1268)
Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org>
2025-05-26 22:35:44 +02:00
github-actions[bot] 136b1c9858
chore(main): release go-feature-flag-provider 0.7.8 (#1285)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-26 20:38:01 +02:00
simas.usas 7083655c78
feat(gofeatureflag): added cache option (#1284)
Signed-off-by: Simas Usas <simas.usas@vinted.com>
2025-05-26 08:17:38 +02:00
Todd Baert df1ec47c3f
chore: use publish env
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
2025-04-16 12:31:06 -04:00
renovate[bot] de161ad178
fix(deps): update swc monorepo (#1280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 10:57:06 +00:00
renovate[bot] a80f5ce3d7
fix(deps): update dependency lru-cache to v11.1.0 (#1279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 01:42:00 +00:00
renovate[bot] afae82c1a1
fix(deps): update dependency @aws-sdk/client-ssm to v3.787.0 (#1278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 01:38:56 +00:00
renovate[bot] b9231018c4
chore(deps): update dependency typescript to v5.8.3 (#1277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 01:35:38 +00:00
renovate[bot] 4a48caa78a
chore(deps): update dependency ts-jest to v29.3.1 (#1276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 22:24:37 +00:00
renovate[bot] 92c67281c8
chore(deps): update dependency testcontainers to v10.24.2 (#1275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 18:11:16 +00:00
github-actions[bot] 358a04f3f0
chore(main): release go-feature-flag-provider 0.7.7 (#1265)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-11 15:32:00 +02:00
Christian Bilevits ce6a8e1a80
fix(gofeatureflag): Error for configurationHasChanged when slash was missing in endpoint (#1229)
Co-authored-by: Thomas Poignant <thomas.poignant@gofeatureflag.org>
2025-04-11 15:30:13 +02:00
renovate[bot] ee676d8495
chore(deps): update dependency @smithy/types to v4.2.0 (#1274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 12:46:31 +00:00
renovate[bot] 18a0cb906a
fix(deps): update dependency axios to v1.8.4 (#1273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 12:43:06 +00:00
renovate[bot] d7f8746434
fix(deps): update dependency @flipt-io/flipt-client-js to v0.0.2 (#1272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 06:52:29 +00:00
renovate[bot] fbe9adc832
chore(deps): update libs/shared/flagd-core/flagd-schemas digest to c707f56 (#1271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 02:57:41 +00:00
renovate[bot] 29f8b84685
chore(deps): update libs/providers/flagd/spec digest to 27e4461 (#1270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 02:54:31 +00:00
renovate[bot] ca98f7c6dc
chore(deps): update libs/providers/flagd/schemas digest to c707f56 (#1269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-10 23:43:45 +00:00
renovate[bot] 241e36edf6
chore(deps): update libs/providers/flagd-web/schemas digest to c707f56 (#1254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-10 15:16:48 +00:00
renovate[bot] e6d3e9902f
chore(deps): update actions/setup-node digest to cdca736 (#1248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-10 15:13:43 +00:00
github-actions[bot] 7be62ae45f
chore(main): release flipt-web-provider 0.1.3 (#1253)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 17:13:41 +02:00
github-actions[bot] a4fa64ec24
chore(main): release flipt-web-provider 0.1.3 (#1250)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Lukas Reining <lukas.reining@codecentric.de>
2025-04-10 17:10:25 +02:00
github-actions[bot] 9323b14968
chore(main): release flipt-provider 0.1.3 (#1249)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 17:09:42 +02:00
Mark Phelps dba2a28001
chore(flipt): swap underlying flipt web sdk (#1244)
Signed-off-by: Mark Phelps <209477+markphelps@users.noreply.github.com>
Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Co-authored-by: adams85 <31276480+adams85@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
2025-04-10 17:07:05 +02:00
Simon Schrottner 4180281715
chore: update codeownership for global maintainers (#1245)
Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
2025-04-10 10:47:06 +02:00
github-actions[bot] e8774fdd8c
chore(main): release config-cat-provider 0.7.5 (#1246)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 20:51:01 +02:00
github-actions[bot] 650a598dbd
chore(main): release config-cat-web-provider 0.1.6 (#1247) 2025-04-09 19:57:59 +02:00
adams85 04256197bf
fix(config-cat): Rework error reporting (#1242) 2025-04-09 19:37:35 +02:00
github-actions[bot] a164bcaa2f
chore(main): release aws-ssm-provider 0.1.2 (#1243)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-27 15:41:45 -04:00
Giovanni De Giorgio 043be44de1
feat(aws-ssm): add decryption support for `SecureString` parameters (#1241)
Signed-off-by: Giovanni De Giorgio <giovannidegiorgio1999@gmail.com>
2025-03-27 13:22:52 -04:00
Giovanni De Giorgio ea9e62a8aa
fix(generator): update generated path `project.json`, `jest.config.ts`, `.eslintrc.json` (#1236)
Signed-off-by: Giovanni De Giorgio <giovannidegiorgio1999@gmail.com>
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
2025-03-25 13:54:10 -04:00
189 changed files with 3701 additions and 2462 deletions

View File

@ -7,6 +7,14 @@
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@typescript-eslint/consistent-type-imports": [
"error",
{
"disallowTypeAnnotations": true,
"fixStyle": "separate-type-imports",
"prefer": "type-imports"
}
],
"@nx/enforce-module-boundaries": [
"error",
{
@ -56,7 +64,7 @@
"checkObsoleteDependencies": true,
"checkVersionMismatches": true,
"ignoredDependencies": ["jest-cucumber", "jest"],
"ignoredFiles": ["**/test/**", "**/spec/**", "**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js"]
"ignoredFiles": ["**/test/**", "**/tests/*", "**/spec/**", "**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/jest.*"]
}
]
}

View File

@ -14,14 +14,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
node-version: [20.x, 22.x, 24.x]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
submodules: recursive
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ matrix.node-version }}
@ -31,9 +31,9 @@ jobs:
- run: git branch --track main origin/main || true
- run: if ! npx nx format:check ; then echo "Format check failed. Please run 'npx nx format:write'."; fi
- run: npx nx affected --target=lint --parallel=3
- run: npx nx affected --target=test --parallel=3 --ci --code-coverage
- run: npx nx affected --target=build --parallel=3
- run: npx nx affected --target=lint --parallel=3 --exclude=js-sdk-contrib
- run: npx nx affected --target=test --parallel=3 --ci --code-coverage --exclude=js-sdk-contrib
- run: npx nx affected --target=build --parallel=3 --exclude=js-sdk-contrib
e2e:
runs-on: ubuntu-latest
@ -43,10 +43,9 @@ jobs:
with:
fetch-depth: 0
submodules: recursive
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
# we need 'fetch' for this test, which is only in 18+
node-version: 18
node-version: 20
cache: 'npm'
- name: Install

View File

@ -5,6 +5,7 @@ on:
name: Run Release Please
jobs:
release-please:
environment: publish
runs-on: ubuntu-latest
# Release-please creates a PR that tracks all changes
@ -28,10 +29,10 @@ jobs:
with:
github_token: ${{ github.token }}
- name: Setup Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: ${{ steps.release.outputs.releases_created }}
with:
node-version: 18
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Build Packages
if: ${{ steps.release.outputs.releases_created }}

2
.gitmodules vendored
View File

@ -13,7 +13,7 @@
[submodule "libs/shared/flagd-core/test-harness"]
path = libs/shared/flagd-core/test-harness
url = https://github.com/open-feature/flagd-testbed
branch = v0.5.21
branch = v2.8.0
[submodule "libs/shared/flagd-core/spec"]
path = libs/shared/flagd-core/spec
url = https://github.com/open-feature/spec

View File

@ -1,25 +1,25 @@
{
"libs/hooks/open-telemetry": "0.4.0",
"libs/providers/go-feature-flag": "0.7.6",
"libs/providers/go-feature-flag": "0.7.8",
"libs/providers/flagd": "0.13.3",
"libs/providers/flagd-web": "0.7.3",
"libs/providers/env-var": "0.3.1",
"libs/providers/config-cat": "0.7.4",
"libs/providers/config-cat": "0.7.6",
"libs/providers/launchdarkly-client": "0.3.2",
"libs/providers/go-feature-flag-web": "0.2.5",
"libs/shared/flagd-core": "1.0.0",
"libs/providers/go-feature-flag-web": "0.2.6",
"libs/shared/flagd-core": "1.1.0",
"libs/shared/ofrep-core": "1.0.1",
"libs/providers/ofrep": "0.2.1",
"libs/providers/ofrep-web": "0.3.2",
"libs/providers/flipt": "0.1.2",
"libs/providers/ofrep-web": "0.3.3",
"libs/providers/flipt": "0.1.3",
"libs/providers/flagsmith-client": "0.1.3",
"libs/providers/flipt-web": "0.1.2",
"libs/providers/flipt-web": "0.1.5",
"libs/providers/multi-provider": "0.1.2",
"libs/providers/multi-provider-web": "0.0.3",
"libs/providers/growthbook-client": "0.1.2",
"libs/providers/config-cat-web": "0.1.5",
"libs/providers/config-cat-web": "0.1.6",
"libs/shared/config-cat-core": "0.1.1",
"libs/providers/unleash-web": "0.1.1",
"libs/providers/growthbook": "0.1.2",
"libs/providers/aws-ssm": "0.1.1"
"libs/providers/aws-ssm": "0.1.3"
}

View File

@ -3,4 +3,4 @@
#
# Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-javascript/workgroup.yaml
#
* @open-feature/sdk-javascript-maintainers
* @open-feature/sdk-javascript-maintainers @open-feature/maintainers

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
assets/aws-ssm/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/aws-ssm/ssm-menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,6 +1,8 @@
import { BeforeHookContext, EvaluationDetails, HookContext, StandardResolutionReasons } from '@openfeature/server-sdk';
import type { BeforeHookContext, EvaluationDetails, HookContext } from '@openfeature/server-sdk';
import { StandardResolutionReasons } from '@openfeature/server-sdk';
import opentelemetry from '@opentelemetry/api';
import { DataPoint, MeterProvider, MetricReader, ScopeMetrics } from '@opentelemetry/sdk-metrics';
import type { DataPoint, ScopeMetrics } from '@opentelemetry/sdk-metrics';
import { MeterProvider, MetricReader } from '@opentelemetry/sdk-metrics';
import {
ACTIVE_COUNT_NAME,
ERROR_TOTAL_NAME,
@ -12,7 +14,7 @@ import {
VARIANT_ATTR,
} from '../conventions';
import { MetricsHook } from './metrics-hook';
import { AttributeMapper } from '../otel-hook';
import type { AttributeMapper } from '../otel-hook';
// no-op "in-memory" reader
class InMemoryMetricReader extends MetricReader {

View File

@ -1,19 +1,18 @@
import type { BeforeHookContext, Logger } from '@openfeature/server-sdk';
import {
BeforeHookContext,
Logger,
StandardResolutionReasons,
type EvaluationDetails,
type FlagValue,
type Hook,
type HookContext,
} from '@openfeature/server-sdk';
import { Attributes, Counter, UpDownCounter, ValueType, metrics } from '@opentelemetry/api';
import type { Attributes, Counter, UpDownCounter } from '@opentelemetry/api';
import { ValueType, metrics } from '@opentelemetry/api';
import type { EvaluationAttributes, ExceptionAttributes } from '../conventions';
import {
ACTIVE_COUNT_NAME,
ERROR_TOTAL_NAME,
EXCEPTION_ATTR,
EvaluationAttributes,
ExceptionAttributes,
KEY_ATTR,
PROVIDER_NAME_ATTR,
REASON_ATTR,
@ -21,7 +20,8 @@ import {
SUCCESS_TOTAL_NAME,
VARIANT_ATTR,
} from '../conventions';
import { OpenTelemetryHook, OpenTelemetryHookOptions } from '../otel-hook';
import type { OpenTelemetryHookOptions } from '../otel-hook';
import { OpenTelemetryHook } from '../otel-hook';
type ErrorEvaluationAttributes = EvaluationAttributes & ExceptionAttributes;

View File

@ -1,5 +1,5 @@
import { FlagMetadata, Logger } from '@openfeature/server-sdk';
import { Attributes } from '@opentelemetry/api';
import type { FlagMetadata, Logger } from '@openfeature/server-sdk';
import type { Attributes } from '@opentelemetry/api';
export type AttributeMapper = (flagMetadata: FlagMetadata) => Attributes;

View File

@ -1,4 +1,4 @@
import { EvaluationDetails, HookContext } from '@openfeature/server-sdk';
import type { EvaluationDetails, HookContext } from '@openfeature/server-sdk';
const addEvent = jest.fn();
const recordException = jest.fn();

View File

@ -1,7 +1,8 @@
import { Hook, HookContext, EvaluationDetails, FlagValue, Logger } from '@openfeature/server-sdk';
import type { Hook, HookContext, EvaluationDetails, FlagValue, Logger } from '@openfeature/server-sdk';
import { trace } from '@opentelemetry/api';
import { FEATURE_FLAG, KEY_ATTR, PROVIDER_NAME_ATTR, VARIANT_ATTR } from '../conventions';
import { OpenTelemetryHook, OpenTelemetryHookOptions } from '../otel-hook';
import type { OpenTelemetryHookOptions } from '../otel-hook';
import { OpenTelemetryHook } from '../otel-hook';
export type TracingHookOptions = OpenTelemetryHookOptions;

View File

@ -1,5 +1,20 @@
# Changelog
## [0.1.3](https://github.com/open-feature/js-sdk-contrib/compare/aws-ssm-provider-v0.1.2...aws-ssm-provider-v0.1.3) (2025-06-04)
### 🐛 Bug Fixes
* **deps:** update dependency @aws-sdk/client-ssm to v3.787.0 ([#1278](https://github.com/open-feature/js-sdk-contrib/issues/1278)) ([afae82c](https://github.com/open-feature/js-sdk-contrib/commit/afae82c1a1472d33b884105edaac2976c19e7423))
* **deps:** update dependency lru-cache to v11.1.0 ([#1279](https://github.com/open-feature/js-sdk-contrib/issues/1279)) ([a80f5ce](https://github.com/open-feature/js-sdk-contrib/commit/a80f5ce3d7a6e74e762a75ba8fa9f5b70ca2a179))
## [0.1.2](https://github.com/open-feature/js-sdk-contrib/compare/aws-ssm-provider-v0.1.1...aws-ssm-provider-v0.1.2) (2025-03-27)
### ✨ New Features
* **aws-ssm:** add decryption support for `SecureString` parameters ([#1241](https://github.com/open-feature/js-sdk-contrib/issues/1241)) ([043be44](https://github.com/open-feature/js-sdk-contrib/commit/043be44de1442b89876e9857478afe619fcf0b04))
## [0.1.1](https://github.com/open-feature/js-sdk-contrib/compare/aws-ssm-provider-v0.1.0...aws-ssm-provider-v0.1.1) (2025-03-20)

View File

@ -30,9 +30,41 @@ OpenFeature.setProvider(
})
);
```
# AWS SSM Provider Configuration
## AwsSsmProviderConfig
| Property | Type | Description | Default |
|-----------------|--------------------|----------------------------------------------|---------|
| `ssmClientConfig` | `SSMClientConfig` | AWS SSM Client configuration options. | See [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ssm/) |
| `enableDecryption` | `boolean` | Enable decryption for SecureString parameters | false |
| `cacheOpts` | `LRUCacheConfig` | Configuration for the local LRU cache. | See below |
## LRUCacheConfig
| Property | Type | Description | Default |
|-----------|--------|------------------------------------------------|---------|
| `enabled` | `boolean` | Whether caching is enabled. | `false` |
| `ttl` | `number` | Time-to-live (TTL) for cached items (in ms). | `300000` (5 minutes) |
| `size` | `number` | Maximum number of items in the cache. | `1000` |
## Retrieve Feature Flag!
Create a new SSM Param called 'my-feature-flag' in your AWS Account and then retrieve it via OpenFeature Client!
Open your AWS Management Console and go to AWS System Manager service
![SSM-Menu](../../../assets/aws-ssm/search.png)
Go to Parameter Store
![Parameter-Store](../../../assets/aws-ssm/ssm-menu.png)
Create a new SSM Param called 'my-feature-flag' in your AWS Account and then retrieve it via OpenFeature Client!
![Create-Param](../../../assets/aws-ssm/create-param.png)
```
const featureFlags = OpenFeature.getClient();

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@openfeature/aws-ssm-provider",
"version": "0.1.1",
"version": "0.1.3",
"dependencies": {
"@aws-sdk/client-ssm": "^3.759.0",
"lru-cache": "^11.0.2",

View File

@ -1,6 +1,7 @@
import { OpenFeature } from '@openfeature/server-sdk';
import { AwsSsmProvider } from '../lib/aws-ssm-provider';
import { GetParameterCommand, GetParameterCommandOutput, SSMClient } from '@aws-sdk/client-ssm';
import type { GetParameterCommandOutput } from '@aws-sdk/client-ssm';
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
import { mockClient } from 'aws-sdk-client-mock';
const ssmMock = mockClient(SSMClient);

View File

@ -1,4 +1,4 @@
import { SSMClientConfig } from '@aws-sdk/client-ssm';
import type { SSMClientConfig } from '@aws-sdk/client-ssm';
import { AwsSsmProvider } from './aws-ssm-provider';
import { ErrorCode, StandardResolutionReasons } from '@openfeature/core';

View File

@ -1,13 +1,7 @@
import {
EvaluationContext,
Provider,
JsonValue,
ResolutionDetails,
StandardResolutionReasons,
ErrorCode,
} from '@openfeature/server-sdk';
import type { EvaluationContext, Provider, JsonValue, ResolutionDetails } from '@openfeature/server-sdk';
import { StandardResolutionReasons, ErrorCode } from '@openfeature/server-sdk';
import { InternalServerError } from '@aws-sdk/client-ssm';
import { AwsSsmProviderConfig } from './types';
import type { AwsSsmProviderConfig } from './types';
import { SSMService } from './ssm-service';
import { Cache } from './cache';
@ -22,7 +16,7 @@ export class AwsSsmProvider implements Provider {
cache: Cache;
constructor(config: AwsSsmProviderConfig) {
this.service = new SSMService(config.ssmClientConfig);
this.service = new SSMService(config.ssmClientConfig, config.enableDecryption);
this.cache = new Cache(config.cacheOpts);
}

View File

@ -1,5 +1,5 @@
import { ResolutionDetails } from '@openfeature/core';
import { LRUCacheConfig } from './types';
import type { ResolutionDetails } from '@openfeature/core';
import type { LRUCacheConfig } from './types';
import { LRUCache } from 'lru-cache';
export class Cache {
@ -8,10 +8,10 @@ export class Cache {
private enabled: boolean;
constructor(opts: LRUCacheConfig) {
this.cache = new LRUCache({
maxSize: opts.size,
maxSize: opts.size ?? 1000,
sizeCalculation: () => 1,
});
this.ttl = opts.ttl;
this.ttl = opts.ttl ?? 300000;
this.enabled = opts.enabled;
}

View File

@ -1,19 +1,15 @@
import { GetParameterCommand, SSMClient, SSMClientConfig } from '@aws-sdk/client-ssm';
import { ResponseMetadata } from '@smithy/types';
import {
FlagNotFoundError,
TypeMismatchError,
JsonValue,
ParseError,
ResolutionDetails,
StandardResolutionReasons,
} from '@openfeature/core';
import type { SSMClientConfig, GetParameterCommandInput } from '@aws-sdk/client-ssm';
import { GetParameterCommand, SSMClient, DescribeParametersCommand } from '@aws-sdk/client-ssm';
import type { ResponseMetadata } from '@smithy/types';
import type { JsonValue, ResolutionDetails } from '@openfeature/core';
import { FlagNotFoundError, TypeMismatchError, ParseError, StandardResolutionReasons } from '@openfeature/core';
export class SSMService {
client: SSMClient;
constructor(config: SSMClientConfig) {
enableDecryption: boolean;
constructor(config: SSMClientConfig, enableDecryption?: boolean) {
this.client = new SSMClient(config);
this.enableDecryption = enableDecryption ?? false;
}
async getBooleanValue(name: string): Promise<ResolutionDetails<boolean>> {
@ -78,10 +74,34 @@ export class SSMService {
}
}
async _isSecureString(name: string): Promise<boolean> {
const res = await this.client.send(
new DescribeParametersCommand({
ParameterFilters: [
{
Key: 'Name',
Values: [name],
},
],
}),
);
if (!res.Parameters) {
throw new FlagNotFoundError(`Unable to find an SSM Parameter with key ${name}`);
}
return res.Parameters[0].Type === 'SecureString';
}
async _getValueFromSSM(name: string): Promise<{ val: string; metadata: ResponseMetadata }> {
const command: GetParameterCommand = new GetParameterCommand({
const param: GetParameterCommandInput = {
Name: name,
});
};
if (this.enableDecryption) {
param.WithDecryption = await this._isSecureString(name);
}
const command: GetParameterCommand = new GetParameterCommand(param);
const res = await this.client.send(command);

View File

@ -1,12 +1,13 @@
import { SSMClientConfig } from '@aws-sdk/client-ssm';
import type { SSMClientConfig } from '@aws-sdk/client-ssm';
export type AwsSsmProviderConfig = {
ssmClientConfig: SSMClientConfig;
cacheOpts: LRUCacheConfig;
enableDecryption?: boolean;
};
export type LRUCacheConfig = {
enabled: boolean;
ttl: number;
size: number;
ttl?: number;
size?: number;
};

View File

@ -1,5 +1,12 @@
# Changelog
## [0.1.6](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-web-provider-v0.1.5...config-cat-web-provider-v0.1.6) (2025-04-09)
### 🐛 Bug Fixes
* **config-cat:** Rework error reporting ([#1242](https://github.com/open-feature/js-sdk-contrib/issues/1242)) ([0425619](https://github.com/open-feature/js-sdk-contrib/commit/04256197bf6e7da70afd4ac1c31bdaf55ce4b789))
## [0.1.5](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-web-provider-v0.1.4...config-cat-web-provider-v0.1.5) (2025-03-14)

View File

@ -1,12 +1,12 @@
{
"name": "@openfeature/config-cat-web-provider",
"version": "0.1.5",
"version": "0.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openfeature/config-cat-web-provider",
"version": "0.1.5",
"version": "0.1.6",
"peerDependencies": {
"@openfeature/web-sdk": "^1.0.0",
"configcat-js-ssr": "^8.4.3"

View File

@ -1,6 +1,6 @@
{
"name": "@openfeature/config-cat-web-provider",
"version": "0.1.5",
"version": "0.1.6",
"license": "Apache-2.0",
"scripts": {
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",

View File

@ -1,13 +1,7 @@
import { ConfigCatWebProvider } from './config-cat-web-provider';
import {
createConsoleLogger,
createFlagOverridesFromMap,
HookEvents,
ISettingUnion,
LogLevel,
OverrideBehaviour,
} from 'configcat-js-ssr';
import { EventEmitter } from 'events';
import type { HookEvents, IConfigCatCache, ISettingUnion } from 'configcat-js-ssr';
import { createConsoleLogger, createFlagOverridesFromMap, LogLevel, OverrideBehaviour } from 'configcat-js-ssr';
import type { EventEmitter } from 'events';
import { ProviderEvents, ParseError, FlagNotFoundError, TypeMismatchError } from '@openfeature/web-sdk';
describe('ConfigCatWebProvider', () => {
@ -81,30 +75,47 @@ describe('ConfigCatWebProvider', () => {
});
});
it('should emit PROVIDER_ERROR event', () => {
const handler = jest.fn();
const eventData: [string, unknown] = ['error', { error: 'error' }];
it("should emit PROVIDER_READY event when underlying client is initialized after provider's initialize", async () => {
const cacheValue = '253370761200000\nW/"12345678-90a"\n{"f":{"booleanTrue":{"t":0,"v":{"b":true}}}}';
provider.events.addHandler(ProviderEvents.Error, handler);
configCatEmitter.emit('clientError', ...eventData);
const fakeSharedCache = new (class implements IConfigCatCache {
private _value?: string;
get(key: string) {
return this._value;
}
set(key: string, value: string) {
this._value = value;
}
})();
expect(handler).toHaveBeenCalledWith({
message: eventData[0],
metadata: eventData[1],
const provider = ConfigCatWebProvider.create('configcat-sdk-1/1234567890123456789012/1234567890123456789012', {
cache: fakeSharedCache,
logger: createConsoleLogger(LogLevel.Off),
offline: true,
maxInitWaitTimeSeconds: 1,
});
});
it('should emit PROVIDER_READY event after successful evaluation during ERROR condition', async () => {
const errorHandler = jest.fn();
provider.events.addHandler(ProviderEvents.Error, errorHandler);
configCatEmitter.emit('clientError', 'error', { error: 'error' });
expect(errorHandler).toHaveBeenCalled();
const readyHandler = jest.fn();
provider.events.addHandler(ProviderEvents.Ready, readyHandler);
await provider.resolveBooleanEvaluation('booleanTrue', false, { targetingKey });
try {
await provider.initialize();
} catch (err) {
expect((err as Error).message).toContain('underlying ConfigCat client could not initialize');
}
expect(readyHandler).toHaveBeenCalledTimes(0);
fakeSharedCache.set('', cacheValue);
// Make sure that the internal cache is refreshed.
await provider.configCatClient?.forceRefreshAsync();
provider.resolveBooleanEvaluation('booleanTrue', false, { targetingKey });
// Wait a little while for the Ready event to be emitted.
await new Promise((resolve) => setTimeout(resolve, 100));
expect(readyHandler).toHaveBeenCalled();
});
});

View File

@ -1,36 +1,20 @@
import type { EvaluationContext, JsonValue, Paradigm, Provider, ResolutionDetails } from '@openfeature/web-sdk';
import {
EvaluationContext,
JsonValue,
OpenFeatureEventEmitter,
Paradigm,
ParseError,
Provider,
ProviderEvents,
ProviderNotReadyError,
ResolutionDetails,
TypeMismatchError,
} from '@openfeature/web-sdk';
import {
isType,
parseError,
PrimitiveType,
PrimitiveTypeName,
toResolutionDetails,
transformContext,
} from '@openfeature/config-cat-core';
import {
getClient,
IConfig,
IConfigCatClient,
OptionsForPollingMode,
PollingMode,
SettingValue,
} from 'configcat-js-ssr';
import type { PrimitiveType, PrimitiveTypeName } from '@openfeature/config-cat-core';
import { isType, parseError, toResolutionDetails, transformContext } from '@openfeature/config-cat-core';
import type { IConfig, IConfigCatClient, OptionsForPollingMode, SettingValue } from 'configcat-js-ssr';
import { ClientCacheState, getClient, PollingMode } from 'configcat-js-ssr';
export class ConfigCatWebProvider implements Provider {
public readonly events = new OpenFeatureEventEmitter();
private readonly _clientFactory: (provider: ConfigCatWebProvider) => IConfigCatClient;
private _hasError = false;
private _isProviderReady = false;
private _client?: IConfigCatClient;
public runsOn: Paradigm = 'client';
@ -53,19 +37,11 @@ export class ConfigCatWebProvider implements Provider {
options.setupHooks = (hooks) => {
oldSetupHooks?.(hooks);
hooks.on('configChanged', (projectConfig: IConfig | undefined) =>
hooks.on('configChanged', (config: IConfig) =>
provider.events.emit(ProviderEvents.ConfigurationChanged, {
flagsChanged: projectConfig ? Object.keys(projectConfig.settings) : undefined,
flagsChanged: Object.keys(config.settings),
}),
);
hooks.on('clientError', (message: string, error) => {
provider._hasError = true;
provider.events.emit(ProviderEvents.Error, {
message: message,
metadata: error,
});
});
};
return getClient(sdkKey, PollingMode.AutoPoll, options);
@ -74,8 +50,19 @@ export class ConfigCatWebProvider implements Provider {
public async initialize(): Promise<void> {
const client = this._clientFactory(this);
await client.waitForReady();
const clientCacheState = await client.waitForReady();
this._client = client;
if (clientCacheState !== ClientCacheState.NoFlagData) {
this._isProviderReady = true;
} else {
// OpenFeature provider defines ready state like this: "The provider is ready to resolve flags."
// However, ConfigCat client's behavior is different: in some cases ready state may be reached
// even if the client's internal, in-memory cache hasn't been populated yet, that is,
// the client is not able to evaluate feature flags yet. In such cases we throw an error to
// prevent the provider from being set ready right away, and check for the ready state later.
throw Error('The underlying ConfigCat client could not initialize within maxInitWaitTimeSeconds.');
}
}
public get configCatClient() {
@ -137,13 +124,22 @@ export class ConfigCatWebProvider implements Provider {
const configCatDefaultValue = flagType !== 'object' ? (defaultValue as SettingValue) : JSON.stringify(defaultValue);
const { value, ...evaluationData } = this._client
.snapshot()
.getValueDetails(flagKey, configCatDefaultValue, transformContext(context));
const snapshot = this._client.snapshot();
if (this._hasError && !evaluationData.errorMessage && !evaluationData.errorException) {
this._hasError = false;
this.events.emit(ProviderEvents.Ready);
const { value, ...evaluationData } = snapshot.getValueDetails(
flagKey,
configCatDefaultValue,
transformContext(context),
);
if (!this._isProviderReady && snapshot.cacheState !== ClientCacheState.NoFlagData) {
// Ideally, we would check ConfigCat client's initialization state in its "background" polling loop.
// This is not possible at the moment, so as a workaround, we do the check on feature flag evaluation.
// There are plans to improve this situation, so let's revise this
// as soon as ConfigCat SDK implements the necessary event.
this._isProviderReady = true;
setTimeout(() => this.events.emit(ProviderEvents.Ready), 0);
}
if (evaluationData.isDefaultValue) {

View File

@ -1,5 +1,19 @@
# Changelog
## [0.7.6](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.7.5...config-cat-provider-v0.7.6) (2025-07-04)
### 🐛 Bug Fixes
* **security:** update dependency configcat-common to v9.4.0 ([#1348](https://github.com/open-feature/js-sdk-contrib/issues/1348)) ([601e7de](https://github.com/open-feature/js-sdk-contrib/commit/601e7de19948bc826778a076f27b46a8cb1fabca))
## [0.7.5](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.7.4...config-cat-provider-v0.7.5) (2025-04-09)
### 🐛 Bug Fixes
* **config-cat:** Rework error reporting ([#1242](https://github.com/open-feature/js-sdk-contrib/issues/1242)) ([0425619](https://github.com/open-feature/js-sdk-contrib/commit/04256197bf6e7da70afd4ac1c31bdaf55ce4b789))
## [0.7.4](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.7.3...config-cat-provider-v0.7.4) (2025-03-14)

View File

@ -1,12 +1,12 @@
{
"name": "@openfeature/config-cat-provider",
"version": "0.7.4",
"version": "0.7.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openfeature/config-cat-provider",
"version": "0.7.4",
"version": "0.7.6",
"peerDependencies": {
"@openfeature/server-sdk": "^1.13.5",
"configcat-node": "^11.3.1"

View File

@ -1,6 +1,6 @@
{
"name": "@openfeature/config-cat-provider",
"version": "0.7.4",
"version": "0.7.6",
"license": "Apache-2.0",
"scripts": {
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
@ -10,6 +10,6 @@
"@openfeature/server-sdk": "^1.13.5",
"configcat-node": "^11.3.1",
"@openfeature/config-cat-core": "0.1.1",
"configcat-common": "9.3.1"
"configcat-common": "9.4.0"
}
}

View File

@ -1,15 +1,14 @@
import { ConfigCatProvider } from './config-cat-provider';
import { ProviderEvents, ParseError, FlagNotFoundError, TypeMismatchError } from '@openfeature/web-sdk';
import type { HookEvents, IConfigCatCache, ISettingUnion } from 'configcat-js-ssr';
import {
createConsoleLogger,
createFlagOverridesFromMap,
HookEvents,
ISettingUnion,
LogLevel,
OverrideBehaviour,
PollingMode,
} from 'configcat-js-ssr';
import { EventEmitter } from 'events';
import type { EventEmitter } from 'events';
describe('ConfigCatProvider', () => {
const targetingKey = 'abc';
@ -82,30 +81,51 @@ describe('ConfigCatProvider', () => {
});
});
it('should emit PROVIDER_ERROR event', () => {
const handler = jest.fn();
const eventData: [string, unknown] = ['error', { error: 'error' }];
it("should emit PROVIDER_READY event when underlying client is initialized after provider's initialize", async () => {
const cacheValue = '253370761200000\nW/"12345678-90a"\n{"f":{"booleanTrue":{"t":0,"v":{"b":true}}}}';
provider.events.addHandler(ProviderEvents.Error, handler);
configCatEmitter.emit('clientError', ...eventData);
const fakeSharedCache = new (class implements IConfigCatCache {
private _value?: string;
get(key: string) {
return this._value;
}
set(key: string, value: string) {
this._value = value;
}
})();
expect(handler).toHaveBeenCalledWith({
message: eventData[0],
metadata: eventData[1],
});
});
it('should emit PROVIDER_READY event after successful evaluation during ERROR condition', async () => {
const errorHandler = jest.fn();
provider.events.addHandler(ProviderEvents.Error, errorHandler);
configCatEmitter.emit('clientError', 'error', { error: 'error' });
expect(errorHandler).toHaveBeenCalled();
const provider = ConfigCatProvider.create(
'configcat-sdk-1/1234567890123456789012/1234567890123456789012',
PollingMode.AutoPoll,
{
cache: fakeSharedCache,
logger: createConsoleLogger(LogLevel.Off),
offline: true,
maxInitWaitTimeSeconds: 1,
},
);
const readyHandler = jest.fn();
provider.events.addHandler(ProviderEvents.Ready, readyHandler);
await provider.resolveBooleanEvaluation('booleanTrue', false, { targetingKey });
try {
await provider.initialize();
} catch (err) {
expect((err as Error).message).toContain('underlying ConfigCat client could not initialize');
}
expect(readyHandler).toHaveBeenCalledTimes(0);
fakeSharedCache.set('', cacheValue);
// Make sure that the internal cache is refreshed.
await provider.configCatClient?.forceRefreshAsync();
provider.resolveBooleanEvaluation('booleanTrue', false, { targetingKey });
// Wait a little while for the Ready event to be emitted.
await new Promise((resolve) => setTimeout(resolve, 100));
expect(readyHandler).toHaveBeenCalled();
});
});

View File

@ -1,30 +1,23 @@
import type { EvaluationContext, JsonValue, Provider, ResolutionDetails, Paradigm } from '@openfeature/server-sdk';
import {
EvaluationContext,
JsonValue,
OpenFeatureEventEmitter,
Provider,
ProviderEvents,
ResolutionDetails,
Paradigm,
ProviderNotReadyError,
TypeMismatchError,
ParseError,
} from '@openfeature/server-sdk';
import {
isType,
parseError,
PrimitiveType,
PrimitiveTypeName,
toResolutionDetails,
transformContext,
} from '@openfeature/config-cat-core';
import { PollingMode, SettingValue } from 'configcat-common';
import { IConfigCatClient, getClient, IConfig, OptionsForPollingMode } from 'configcat-node';
import type { PrimitiveType, PrimitiveTypeName } from '@openfeature/config-cat-core';
import { isType, parseError, toResolutionDetails, transformContext } from '@openfeature/config-cat-core';
import type { SettingValue } from 'configcat-common';
import { ClientCacheState, PollingMode } from 'configcat-common';
import type { IConfigCatClient, IConfig, OptionsForPollingMode } from 'configcat-node';
import { getClient } from 'configcat-node';
export class ConfigCatProvider implements Provider {
public readonly events = new OpenFeatureEventEmitter();
private readonly _clientFactory: (provider: ConfigCatProvider) => IConfigCatClient;
private _hasError = false;
private readonly _pollingMode: PollingMode;
private _isProviderReady = false;
private _client?: IConfigCatClient;
public runsOn: Paradigm = 'server';
@ -33,8 +26,9 @@ export class ConfigCatProvider implements Provider {
name: ConfigCatProvider.name,
};
protected constructor(clientFactory: (provider: ConfigCatProvider) => IConfigCatClient) {
protected constructor(clientFactory: (provider: ConfigCatProvider) => IConfigCatClient, pollingMode: PollingMode) {
this._clientFactory = clientFactory;
this._pollingMode = pollingMode;
}
public static create<TMode extends PollingMode>(
@ -50,29 +44,32 @@ export class ConfigCatProvider implements Provider {
options.setupHooks = (hooks) => {
oldSetupHooks?.(hooks);
hooks.on('configChanged', (projectConfig: IConfig | undefined) =>
hooks.on('configChanged', (config: IConfig) =>
provider.events.emit(ProviderEvents.ConfigurationChanged, {
flagsChanged: projectConfig ? Object.keys(projectConfig.settings) : undefined,
flagsChanged: Object.keys(config.settings),
}),
);
hooks.on('clientError', (message: string, error) => {
provider._hasError = true;
provider.events.emit(ProviderEvents.Error, {
message: message,
metadata: error,
});
});
};
return getClient(sdkKey, pollingMode, options);
});
}, pollingMode ?? PollingMode.AutoPoll);
}
public async initialize(): Promise<void> {
const client = this._clientFactory(this);
await client.waitForReady();
const clientCacheState = await client.waitForReady();
this._client = client;
if (this._pollingMode !== PollingMode.AutoPoll || clientCacheState !== ClientCacheState.NoFlagData) {
this._isProviderReady = true;
} else {
// OpenFeature provider defines ready state like this: "The provider is ready to resolve flags."
// However, ConfigCat client's behavior is different: in some cases ready state may be reached
// even if the client's internal, in-memory cache hasn't been populated yet, that is,
// the client is not able to evaluate feature flags yet. In such cases we throw an error to
// prevent the provider from being set ready right away, and check for the ready state later.
throw Error('The underlying ConfigCat client could not initialize within maxInitWaitTimeSeconds.');
}
}
public get configCatClient() {
@ -140,9 +137,14 @@ export class ConfigCatProvider implements Provider {
transformContext(context),
);
if (this._hasError && !evaluationData.errorMessage && !evaluationData.errorException) {
this._hasError = false;
this.events.emit(ProviderEvents.Ready);
if (!this._isProviderReady && this._client.snapshot().cacheState !== ClientCacheState.NoFlagData) {
// Ideally, we would check ConfigCat client's initialization state in its "background" polling loop.
// This is not possible at the moment, so as a workaround, we do the check on feature flag evaluation.
// There are plans to improve this situation, so let's revise this
// as soon as ConfigCat SDK implements the necessary event.
this._isProviderReady = true;
setTimeout(() => this.events.emit(ProviderEvents.Ready), 0);
}
if (evaluationData.isDefaultValue) {

View File

@ -1,11 +1,5 @@
import {
FlagNotFoundError,
JsonValue,
ParseError,
Provider,
ResolutionDetails,
StandardResolutionReasons,
} from '@openfeature/server-sdk';
import type { JsonValue, Provider, ResolutionDetails } from '@openfeature/server-sdk';
import { FlagNotFoundError, ParseError, StandardResolutionReasons } from '@openfeature/server-sdk';
import { constantCase } from './constant-case';
export type Config = {

View File

@ -10,7 +10,7 @@
"@openfeature/web-sdk": "^1.0.0"
},
"dependencies": {
"@openfeature/flagd-core": "1.0.0",
"@openfeature/flagd-core": "1.1.0",
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-web": "^1.4.0",
"@bufbuild/protobuf": "^1.2.0"

@ -1 +1 @@
Subproject commit bb763438abc5b0e97dc26a795bc72b7a9f3e4020
Subproject commit 2852d7772e6b8674681a6ee6b88db10dbe3f6899

View File

@ -2,4 +2,7 @@ import { getGherkinTestPath } from '@openfeature/flagd-core';
export const FLAGD_NAME = 'flagd';
export const GHERKIN_EVALUATION_FEATURE = getGherkinTestPath('flagd.feature');
export const GHERKIN_EVALUATION_FEATURE = getGherkinTestPath(
'evaluation.feature',
'spec/specification/assets/gherkin/',
);

View File

@ -1,12 +1,6 @@
import { StepDefinitions } from 'jest-cucumber';
import {
EvaluationDetails,
FlagValue,
JsonObject,
OpenFeature,
ProviderEvents,
StandardResolutionReasons,
} from '@openfeature/web-sdk';
import type { StepDefinitions } from 'jest-cucumber';
import type { EvaluationContext, EvaluationDetails, FlagValue, JsonObject } from '@openfeature/web-sdk';
import { OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/web-sdk';
import { E2E_CLIENT_NAME } from '@openfeature/flagd-core';
export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then }) => {
@ -14,6 +8,7 @@ export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then })
let value: FlagValue;
let details: EvaluationDetails<FlagValue>;
let fallback: FlagValue;
let context: EvaluationContext;
const client = OpenFeature.getClient(E2E_CLIENT_NAME);
@ -23,7 +18,7 @@ export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then })
});
});
given('a provider is registered', () => undefined);
given('a stable provider', () => undefined);
given('a flagd provider is set', () => undefined);
when(
@ -85,6 +80,32 @@ export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then })
value = client.getObjectValue(key, defaultValue);
});
and(/^a flag with key "(.*)" is evaluated with default value "(.*)"$/, async (key, defaultValue) => {
await OpenFeature.setContext(context);
flagKey = key;
fallback = defaultValue;
value = client.getStringValue(flagKey, fallback as string);
});
when(
/^context contains keys "(.*)", "(.*)", "(.*)", "(.*)" with values "(.*)", "(.*)", (\d+), "(.*)"$/,
(key0, key1, key2, key3, stringVal1, stringVal2, intVal, boolVal) => {
context = {
[key0]: stringVal1,
[key1]: stringVal2,
[key2]: Number.parseInt(intVal),
[key3]: boolVal === true,
};
},
);
and(/^the resolved flag value is "(.*)" when the context is empty$/, async (expectedValue) => {
context = {};
await OpenFeature.setContext(context);
value = client.getStringValue(flagKey, fallback as string);
expect(value).toEqual(expectedValue);
});
then(
/^the resolved object value should be contain fields "(.*)", "(.*)", and "(.*)", with values "(.*)", "(.*)" and (\d+), respectively$/,
(field1: string, field2: string, field3: string, boolValue: string, stringValue: string, intValue: string) => {

View File

@ -1,6 +1,7 @@
import assert from 'assert';
import { OpenFeature } from '@openfeature/web-sdk';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import type { StartedTestContainer } from 'testcontainers';
import { GenericContainer } from 'testcontainers';
import { FlagdWebProvider } from '../../lib/flagd-web-provider';
import { autoBindSteps, loadFeature } from 'jest-cucumber';
import { FLAGD_NAME, GHERKIN_EVALUATION_FEATURE } from '../constants';

View File

@ -1,16 +1,11 @@
import { CallbackClient, Code, ConnectError, PromiseClient } from '@connectrpc/connect';
import type { CallbackClient, ConnectError, PromiseClient } from '@connectrpc/connect';
import { Code } from '@connectrpc/connect';
import { Struct } from '@bufbuild/protobuf';
import {
Client,
ErrorCode,
JsonValue,
OpenFeature,
ProviderEvents,
StandardResolutionReasons,
} from '@openfeature/web-sdk';
import type { Client, JsonValue } from '@openfeature/web-sdk';
import { ErrorCode, OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/web-sdk';
import fetchMock from 'jest-fetch-mock';
import { Service } from '../proto/ts/flagd/evaluation/v1/evaluation_connect';
import { AnyFlag, EventStreamResponse, ResolveAllResponse } from '../proto/ts/flagd/evaluation/v1/evaluation_pb';
import type { Service } from '../proto/ts/flagd/evaluation/v1/evaluation_connect';
import type { AnyFlag, EventStreamResponse, ResolveAllResponse } from '../proto/ts/flagd/evaluation/v1/evaluation_pb';
import { FlagdWebProvider } from './flagd-web-provider';
const EVENT_CONFIGURATION_CHANGE = 'configuration_change';

View File

@ -1,23 +1,27 @@
import { CallbackClient, createCallbackClient, createPromiseClient, PromiseClient } from '@connectrpc/connect';
import type { CallbackClient, PromiseClient } from '@connectrpc/connect';
import { createCallbackClient, createPromiseClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-web';
import { Struct } from '@bufbuild/protobuf';
import {
import type {
EvaluationContext,
FlagNotFoundError,
FlagValue,
JsonValue,
Logger,
Provider,
ResolutionDetails,
} from '@openfeature/web-sdk';
import {
FlagNotFoundError,
OpenFeature,
OpenFeatureEventEmitter,
Provider,
ProviderEvents,
ResolutionDetails,
StandardResolutionReasons,
TypeMismatchError,
} from '@openfeature/web-sdk';
import { Service } from '../proto/ts/flagd/evaluation/v1/evaluation_connect';
import { AnyFlag } from '../proto/ts/flagd/evaluation/v1/evaluation_pb';
import { FlagdProviderOptions, getOptions } from './options';
import type { AnyFlag } from '../proto/ts/flagd/evaluation/v1/evaluation_pb';
import type { FlagdProviderOptions } from './options';
import { getOptions } from './options';
export const ERROR_DISABLED = 'DISABLED';

@ -1 +1 @@
Subproject commit bb763438abc5b0e97dc26a795bc72b7a9f3e4020
Subproject commit 2852d7772e6b8674681a6ee6b88db10dbe3f6899

@ -1 +1 @@
Subproject commit a69f748db2edfec7015ca6bb702ca22fd8c5ef30
Subproject commit a3678719bf9f62880375ba835ebe1bb7a77de409

View File

@ -4,9 +4,15 @@ export const FLAGD_NAME = 'flagd';
export const UNSTABLE_CLIENT_NAME = 'unstable';
export const UNAVAILABLE_CLIENT_NAME = 'unavailable';
export const GHERKIN_FLAGD_FEATURE = getGherkinTestPath('flagd.feature');
export const GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE = getGherkinTestPath('flagd-json-evaluator.feature');
export const GHERKIN_FLAGD_RECONNECT_FEATURE = getGherkinTestPath('flagd-reconnect.feature');
export const GHERKIN_FLAGD = getGherkinTestPath('*.feature');
export const CONNECTION_FEATURE = getGherkinTestPath('connection.feature');
export const CONTEXT_ENRICHMENT_FEATURE = getGherkinTestPath('contextEnrichment.feature');
export const EVALUATION_FEATURE = getGherkinTestPath('evaluation.feature');
export const EVENTS_FEATURE = getGherkinTestPath('events.feature');
export const METADATA_FEATURE = getGherkinTestPath('metadata.feature');
export const RPC_CACHING_FEATURE = getGherkinTestPath('rpc-caching.feature');
export const SELECTOR_FEATURE = getGherkinTestPath('selector.feature');
export const TARGETING_FEATURE = getGherkinTestPath('targeting.feature');
export const GHERKIN_EVALUATION_FEATURE = getGherkinTestPath(
'evaluation.feature',
'spec/specification/assets/gherkin/',

View File

@ -1,2 +1 @@
export * from './constants';
export * from './step-definitions';

View File

@ -0,0 +1,47 @@
import type { StepsDefinitionCallbackOptions } from 'jest-cucumber/dist/src/feature-definition-creation';
import type { State, Steps } from './state';
import { CacheOption, getConfig, ResolverType } from '../../lib/configuration';
import { mapValueToType } from './utils';
export const configSteps: Steps = (state: State) => {
function mapName(name: string): string {
switch (name) {
case 'resolver':
return 'resolverType';
default:
return name;
}
}
return ({ given, when, then }: StepsDefinitionCallbackOptions) => {
beforeEach(() => {
state.options = {};
});
given(/^an option "(.*)" of type "(.*)" with value "(.*)"$/, (name: string, type: string, value: string) => {
state.options[mapName(name)] = mapValueToType(value, type);
});
given(/^an environment variable "(.*)" with value "(.*)"$/, (name, value) => {
process.env[name] = value;
});
when('a config was initialized', () => {
state.config = getConfig(state.options);
});
then(
/^the option "(.*)" of type "(.*)" should have the value "(.*)"$/,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(_name: string, _type: string, _value: string) => {
// TODO: implement with configuration unification, see: https://github.com/open-feature/js-sdk-contrib/issues/1096
// const expected = mapValueToType(value, type);
// const propertyName = mapName(name);
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// // @ts-ignore
// const configElement = state.config[propertyName];
// expect(configElement).toBe(expected);
},
);
// eslint-disable-next-line @typescript-eslint/no-empty-function
then('we should have an error', () => {});
};
};

View File

@ -0,0 +1,33 @@
import type { JsonObject } from '@openfeature/server-sdk';
import type { State, Steps } from './state';
import { mapValueToType } from './utils';
export const contextSteps: Steps =
(state: State) =>
({ given, when, then }) => {
beforeEach(() => (state.context = undefined));
given(
/^a context containing a key "(.*)", with type "(.*)" and with value "(.*)"$/,
(key: string, type: string, value: string) => {
if (state.context == undefined) {
state.context = {};
}
state.context[key] = mapValueToType(value, type);
},
);
given(
/^a context containing a nested property with outer key "(.*)" and inner key "(.*)", with value "(.*)"$/,
(outer: string, inner: string, value) => {
if (state.context == undefined) {
state.context = {};
}
state.context[outer] = { ...(state.context[outer] as JsonObject), [inner]: value };
},
);
given(/^a context containing a targeting key with value "(.*)"$/, (key) => {
if (state.context == undefined) {
state.context = {};
}
state.context.targetingKey = key;
});
};

View File

@ -0,0 +1,49 @@
import { ServerProviderEvents } from '@openfeature/server-sdk';
import type { State, Steps } from './state';
import { waitFor } from './utils';
export const eventSteps: Steps =
(state: State) =>
({ given, when, then }) => {
function map(eventType: string): ServerProviderEvents {
switch (eventType) {
case 'error':
return ServerProviderEvents.Error;
case 'ready':
return ServerProviderEvents.Ready;
case 'stale':
return ServerProviderEvents.Stale;
case 'change':
return ServerProviderEvents.ConfigurationChanged;
default:
throw new Error('unknown eventtype');
}
}
given(/a (.*) event handler/, async (type: string) => {
state.client?.addHandler(map(type), (details) => {
state.events.push({ type, details });
});
});
then(/^the (.*) event handler should have been executed$/, async (type: string) => {
await waitFor(() => expect(state.events.find((value) => value.type == type)).toBeDefined(), { timeout: 20000 });
expect(state.events.find((value) => value.type == type)).toBeDefined();
});
then(/^the (.*) event handler should have been executed within (\d+)ms$/, async (type: string, ms: number) => {
await waitFor(() => expect(state.events.find((value) => value.type == type)).toBeDefined(), { timeout: ms });
const actual = state.events.find((value) => value.type == type);
expect(actual).toBeDefined();
});
when(/^a (.*) event was fired$/, async (type: string) => {
await waitFor(() => expect(state.events.find((value) => value.type == type)), { timeout: 2000 });
expect(state.events.find((value) => value.type == type)).toBeDefined();
});
then('the flag should be part of the event payload', async () => {
await waitFor(() => expect(state.events.find((value) => value.type == 'change')), { timeout: 2000 });
});
};

View File

@ -1,13 +1,6 @@
import { StepDefinitions } from 'jest-cucumber';
import {
EvaluationContext,
EvaluationDetails,
FlagValue,
JsonObject,
OpenFeature,
ProviderEvents,
StandardResolutionReasons,
} from '@openfeature/server-sdk';
import type { StepDefinitions } from 'jest-cucumber';
import type { EvaluationContext, EvaluationDetails, FlagValue, JsonObject } from '@openfeature/server-sdk';
import { OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/server-sdk';
import { E2E_CLIENT_NAME } from '@openfeature/flagd-core';
export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then }) => {

View File

@ -0,0 +1,77 @@
import type { State, Steps } from './state';
import { mapValueToType } from './utils';
export const flagSteps: Steps =
(state: State) =>
({ given, when, then }) => {
beforeEach(() => {
state.flag = undefined;
});
given(
/^a (.*)-flag with key "(.*)" and a default value "(.*)"$/,
(type: string, name: string, defaultValue: string) => {
state.flag = {
name,
type,
defaultValue: mapValueToType(defaultValue, type),
};
},
);
when('the flag was evaluated with details', async () => {
switch (state.flag?.type) {
case 'Boolean':
state.details = await state.client?.getBooleanDetails(
state.flag?.name,
<boolean>state.flag?.defaultValue,
state.context,
);
break;
case 'String':
state.details = await state.client?.getStringDetails(
state.flag?.name,
<string>state.flag?.defaultValue,
state.context,
);
break;
case 'Integer':
case 'Float':
state.details = await state.client?.getNumberDetails(
state.flag?.name,
<number>state.flag?.defaultValue,
state.context,
);
break;
case 'Object':
state.details = await state.client?.getObjectDetails(
state.flag?.name,
<boolean>state.flag?.defaultValue,
state.context,
);
break;
default:
throw new Error('unknown type');
}
});
then(/^the resolved details value should be "(.*)"$/, (arg0) => {
expect(state.details?.value).toEqual(mapValueToType(arg0, state.flag!.type));
});
then(/^the reason should be "(.*)"$/, (expectedReason) => {
expect(state.details?.reason).toBe(expectedReason);
});
then(/^the variant should be "(.*)"$/, (expectedVariant) => {
expect(state.details?.variant).toBe(expectedVariant);
});
then('the resolved metadata should contain', (table) => {
// TODO: implement metadata tests, https://github.com/open-feature/js-sdk-contrib/issues/1290
});
then('the resolved metadata is empty', () => {
// TODO: implement metadata tests, https://github.com/open-feature/js-sdk-contrib/issues/1290
});
};

View File

@ -1,2 +0,0 @@
export * from './flag';
export * from './reconnect';

View File

@ -0,0 +1,78 @@
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdContainer } from '../tests/flagdContainer';
import type { State, Steps } from './state';
import { FlagdProvider } from '../../lib/flagd-provider';
import type { FlagdProviderOptions } from '../../lib/configuration';
export const providerSteps: Steps =
(state: State) =>
({ given, when, then }) => {
const container: FlagdContainer = FlagdContainer.build();
beforeAll(async () => {
console.log('Setting flagd provider...');
return await container.start();
}, 50000);
afterAll(async () => {
await OpenFeature.close();
await container.stop();
});
afterEach(async () => {
// everything breaks without this
await OpenFeature.close();
if (state.client) {
await fetch('http://' + container.getLaunchpadUrl() + '/stop');
await new Promise((r) => setTimeout(r, 100));
}
return Promise.resolve();
}, 50000);
given(/a (.*) flagd provider/, async (providerType: string) => {
const flagdOptions: FlagdProviderOptions = {
resolverType: state.resolverType,
deadlineMs: 2000,
};
let type = 'default';
switch (providerType) {
default:
flagdOptions['port'] = container.getPort(state.resolverType);
if (state?.options?.['selector']) {
flagdOptions['selector'] = state?.options?.['selector'] as string;
}
break;
case 'unavailable':
flagdOptions['port'] = 9999;
break;
case 'ssl':
// TODO: modify this to support ssl
flagdOptions['port'] = container.getPort(state.resolverType);
if (state?.config?.selector) {
flagdOptions['selector'] = state.config.selector;
}
type = 'ssl';
break;
}
await fetch('http://' + container.getLaunchpadUrl() + '/start?config=' + type);
await new Promise((r) => setTimeout(r, 50));
if (providerType == 'unavailable') {
OpenFeature.setProvider(providerType, new FlagdProvider(flagdOptions));
} else {
await OpenFeature.setProviderAndWait(providerType, new FlagdProvider(flagdOptions));
}
state.client = OpenFeature.getClient(providerType);
state.providerType = providerType;
});
when(/^the connection is lost for (\d+)s$/, async (time) => {
console.log('stopping flagd');
await fetch('http://' + container.getLaunchpadUrl() + '/restart?seconds=' + time);
});
when('the flag was modified', async () => {
await fetch('http://' + container.getLaunchpadUrl() + '/change');
});
};

View File

@ -1,4 +1,4 @@
import { StepDefinitions } from 'jest-cucumber';
import type { StepDefinitions } from 'jest-cucumber';
import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk';
import { UNAVAILABLE_CLIENT_NAME, UNSTABLE_CLIENT_NAME } from '../constants';

View File

@ -0,0 +1,39 @@
import type { StepDefinitions } from 'jest-cucumber';
import type { CacheOption, ResolverType } from '../../lib/configuration';
import type { Client, EvaluationContext, EvaluationDetails, EventDetails, FlagValue } from '@openfeature/server-sdk';
interface Flag {
name: string;
type: string;
defaultValue: unknown;
}
interface Event {
type: string;
details?: EventDetails;
}
export interface State {
flagsChanged?: string[];
providerType?: string;
details?: EvaluationDetails<FlagValue>;
client?: Client;
resolverType: ResolverType;
context?: EvaluationContext;
config?: {
cache?: CacheOption;
socketPath?: string;
port: number;
maxCacheSize?: number;
resolverType?: ResolverType;
host: string;
offlineFlagSourcePath?: string;
tls: boolean;
selector?: string;
};
options: Record<string, unknown>;
events: Event[];
flag?: Flag;
}
export type Steps = (state: State) => StepDefinitions;

View File

@ -0,0 +1,53 @@
import type { CacheOption, ResolverType } from '../../lib/configuration';
export function mapValueToType(value: string, type: string): any {
switch (type) {
case 'String':
if (value == 'null') {
return undefined;
}
return value;
case 'Integer':
return Number.parseInt(value);
case 'Float':
return Number.parseFloat(value);
case 'Long':
return Number.parseFloat(value);
case 'Boolean':
return value.toLowerCase() === 'true';
case 'ResolverType':
return value.toLowerCase() as ResolverType;
case 'CacheType':
return value as CacheOption;
case 'Object':
if (value == 'null') {
return undefined;
}
return JSON.parse(value);
default:
throw new Error('type not supported');
}
}
export function waitFor<T>(check: () => T, options: { timeout?: number; interval?: number } = {}): Promise<T> {
const { timeout = 5000, interval = 50 } = options; // Default 5s timeout, 50ms polling interval
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkCondition = () => {
try {
const result = check();
resolve(result); // If condition passes, resolve the promise
} catch (error) {
if (Date.now() - startTime > timeout) {
reject(new Error('Timeout while waiting for condition'));
} else {
setTimeout(checkCondition, interval); // Retry after interval
}
}
};
checkCondition();
});
}

View File

@ -0,0 +1,66 @@
import type { StartedTestContainer, StoppedTestContainer } from 'testcontainers';
import { GenericContainer } from 'testcontainers';
import fs from 'fs';
import * as path from 'node:path';
import type { ResolverType } from '../../lib/configuration';
export class FlagdContainer extends GenericContainer {
private static imageBase = 'ghcr.io/open-feature/flagd-testbed';
private started: StartedTestContainer | undefined;
private stopped: StoppedTestContainer | undefined;
public static build() {
return new FlagdContainer(this.generateImageName());
}
private constructor(image: string) {
super(image);
this.withExposedPorts(8080, 8013, 8014, 8015, 8016);
}
isStarted(): boolean {
return this.started !== undefined;
}
async start(): Promise<StartedTestContainer> {
if (this.isStarted()) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return Promise.resolve(this.started);
}
const containerPromise = super.start();
this.started = await containerPromise;
this.stopped = undefined;
return containerPromise;
}
async stop(): Promise<StoppedTestContainer> {
if (!this.started) {
throw new Error('container not started');
}
const containerPromise = this.started.stop();
this.stopped = await containerPromise;
this.started = undefined;
return containerPromise;
}
getLaunchpadUrl() {
return this.started?.getHost() + ':' + this.started?.getMappedPort(8080);
}
private static generateImageName(): string {
const image = this.imageBase;
const file = path.join(__dirname, './../../../../../shared/flagd-core/test-harness/', 'version.txt');
const version = fs.readFileSync(file, 'utf8').trim();
return `${image}:v${version}`;
}
getPort(resolverType: ResolverType) {
switch (resolverType) {
default:
return this.started?.getMappedPort(8013);
case 'in-process':
return this.started?.getMappedPort(8015);
}
}
}

View File

@ -1,69 +0,0 @@
import assert from 'assert';
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '../../lib/flagd-provider';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { autoBindSteps, loadFeature } from 'jest-cucumber';
import {
FLAGD_NAME,
GHERKIN_FLAGD_RECONNECT_FEATURE,
UNAVAILABLE_CLIENT_NAME,
UNSTABLE_CLIENT_NAME,
} from '../constants';
import { IMAGE_VERSION } from '@openfeature/flagd-core';
import { reconnectStepDefinitions } from '../step-definitions';
// register the flagd provider before the tests.
async function setup() {
const containers: StartedTestContainer[] = [];
console.log('Setting flagd provider...');
const unstable = await new GenericContainer(`ghcr.io/open-feature/flagd-testbed-unstable:${IMAGE_VERSION}`)
.withExposedPorts(8015)
.start();
containers.push(unstable);
await OpenFeature.setProviderAndWait(
UNSTABLE_CLIENT_NAME,
new FlagdProvider({ resolverType: 'in-process', host: 'localhost', port: unstable.getFirstMappedPort() }),
);
OpenFeature.setProvider(
UNAVAILABLE_CLIENT_NAME,
new FlagdProvider({ resolverType: 'in-process', host: 'localhost', port: 9092 }),
);
assert(
OpenFeature.getProviderMetadata(UNSTABLE_CLIENT_NAME).name === FLAGD_NAME,
new Error(
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
OpenFeature.getProviderMetadata(UNSTABLE_CLIENT_NAME).name
}`,
),
);
assert(
OpenFeature.getProviderMetadata(UNAVAILABLE_CLIENT_NAME).name === FLAGD_NAME,
new Error(
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
OpenFeature.getProviderMetadata(UNAVAILABLE_CLIENT_NAME).name
}`,
),
);
console.log('flagd provider configured!');
return containers;
}
jest.setTimeout(30000);
describe('in process', () => {
let containers: StartedTestContainer[] = [];
beforeAll(async () => {
containers = await setup();
}, 60000);
afterAll(async () => {
await OpenFeature.close();
for (const container of containers) {
await container.stop();
}
});
const features = [loadFeature(GHERKIN_FLAGD_RECONNECT_FEATURE)];
autoBindSteps(features, [reconnectStepDefinitions]);
});

View File

@ -1,60 +1,32 @@
import assert from 'assert';
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '../../lib/flagd-provider';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { autoBindSteps, loadFeature } from 'jest-cucumber';
import {
FLAGD_NAME,
GHERKIN_EVALUATION_FEATURE,
GHERKIN_FLAGD_FEATURE,
GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE,
} from '../constants';
import { flagStepDefinitions } from '../step-definitions';
import { E2E_CLIENT_NAME, IMAGE_VERSION } from '@openfeature/flagd-core';
import { autoBindSteps, loadFeatures } from 'jest-cucumber';
import { GHERKIN_FLAGD } from '../constants';
import { providerSteps } from '../step-definitions/providerSteps';
import { configSteps } from '../step-definitions/configSteps';
import type { State } from '../step-definitions/state';
import { eventSteps } from '../step-definitions/eventSteps';
import { flagSteps } from '../step-definitions/flagSteps';
import { contextSteps } from '../step-definitions/contextSteps';
// register the flagd provider before the tests.
async function setup() {
const containers: StartedTestContainer[] = [];
const steps = [providerSteps, configSteps, eventSteps, flagSteps, contextSteps];
console.log('Setting flagd provider...');
const stable = await new GenericContainer(`ghcr.io/open-feature/flagd-testbed:${IMAGE_VERSION}`)
.withExposedPorts(8015)
.start();
containers.push(stable);
OpenFeature.setProvider(
E2E_CLIENT_NAME,
new FlagdProvider({ resolverType: 'in-process', host: 'localhost', port: stable.getFirstMappedPort() }),
jest.setTimeout(50000);
describe('in-process', () => {
const state: State = {
resolverType: 'in-process',
options: {},
config: undefined,
events: [],
};
autoBindSteps(
loadFeatures(GHERKIN_FLAGD, {
// remove filters as we add support for features
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
tagFilter:
'@in-process and not @targetURI and not @customCert and not @events and not @sync and not @grace and not @metadata and not @contextEnrichment',
scenarioNameTemplate: (vars) => {
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
},
}),
steps.map((step) => step(state)),
);
assert(
OpenFeature.getProviderMetadata(E2E_CLIENT_NAME).name === FLAGD_NAME,
new Error(
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
OpenFeature.getProviderMetadata(E2E_CLIENT_NAME).name
}`,
),
);
console.log('flagd provider configured!');
return containers;
}
describe('in process', () => {
let containers: StartedTestContainer[] = [];
beforeAll(async () => {
containers = await setup();
}, 60000);
afterAll(async () => {
await OpenFeature.close();
for (const container of containers) {
await container.stop();
}
});
const features = [
loadFeature(GHERKIN_FLAGD_FEATURE),
loadFeature(GHERKIN_EVALUATION_FEATURE),
loadFeature(GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE),
];
autoBindSteps(features, [flagStepDefinitions]);
});

View File

@ -1,69 +0,0 @@
import assert from 'assert';
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '../../lib/flagd-provider';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { autoBindSteps, loadFeature } from 'jest-cucumber';
import {
FLAGD_NAME,
GHERKIN_FLAGD_RECONNECT_FEATURE,
UNAVAILABLE_CLIENT_NAME,
UNSTABLE_CLIENT_NAME,
} from '../constants';
import { reconnectStepDefinitions } from '../step-definitions';
import { IMAGE_VERSION } from '@openfeature/flagd-core';
// register the flagd provider before the tests.
async function setup() {
const containers: StartedTestContainer[] = [];
console.log('Setting flagd provider...');
const unstable = await new GenericContainer(`ghcr.io/open-feature/flagd-testbed-unstable:${IMAGE_VERSION}`)
.withExposedPorts(8013)
.start();
containers.push(unstable);
OpenFeature.setProvider(
UNSTABLE_CLIENT_NAME,
new FlagdProvider({ cache: 'disabled', port: unstable.getFirstMappedPort() }),
);
OpenFeature.setProvider(UNAVAILABLE_CLIENT_NAME, new FlagdProvider({ cache: 'disabled', port: 8015 }));
assert(
OpenFeature.getProviderMetadata(UNSTABLE_CLIENT_NAME).name === FLAGD_NAME,
new Error(
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
OpenFeature.getProviderMetadata(UNSTABLE_CLIENT_NAME).name
}`,
),
);
assert(
OpenFeature.getProviderMetadata(UNAVAILABLE_CLIENT_NAME).name === FLAGD_NAME,
new Error(
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
OpenFeature.getProviderMetadata(UNAVAILABLE_CLIENT_NAME).name
}`,
),
);
console.log('flagd provider configured!');
return containers;
}
jest.setTimeout(30000);
describe('rpc', () => {
let containers: StartedTestContainer[] = [];
beforeAll(async () => {
containers = await setup();
}, 60000);
afterAll(async () => {
await OpenFeature.close();
for (const container of containers) {
try {
await container.stop();
} catch {
console.warn(`Failed to stop container ${container.getName()}`);
}
}
});
const features = [loadFeature(GHERKIN_FLAGD_RECONNECT_FEATURE)];
autoBindSteps(features, [reconnectStepDefinitions]);
});

View File

@ -1,56 +1,33 @@
import assert from 'assert';
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '../../lib/flagd-provider';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { autoBindSteps, loadFeature } from 'jest-cucumber';
import {
FLAGD_NAME,
GHERKIN_EVALUATION_FEATURE,
GHERKIN_FLAGD_FEATURE,
GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE,
} from '../constants';
import { E2E_CLIENT_NAME, IMAGE_VERSION } from '@openfeature/flagd-core';
import { flagStepDefinitions } from '../step-definitions';
import { autoBindSteps, loadFeatures } from 'jest-cucumber';
import { providerSteps } from '../step-definitions/providerSteps';
import { configSteps } from '../step-definitions/configSteps';
import type { State } from '../step-definitions/state';
import { eventSteps } from '../step-definitions/eventSteps';
import { flagSteps } from '../step-definitions/flagSteps';
import { contextSteps } from '../step-definitions/contextSteps';
import { GHERKIN_FLAGD } from '../constants';
// register the flagd provider before the tests.
async function setup() {
const containers: StartedTestContainer[] = [];
const steps = [providerSteps, configSteps, eventSteps, flagSteps, contextSteps];
console.log('Setting flagd provider...');
const stable = await new GenericContainer(`ghcr.io/open-feature/flagd-testbed:${IMAGE_VERSION}`)
.withExposedPorts(8013)
.start();
containers.push(stable);
OpenFeature.setProvider(E2E_CLIENT_NAME, new FlagdProvider({ cache: 'disabled', port: stable.getFirstMappedPort() }));
assert(
OpenFeature.getProviderMetadata(E2E_CLIENT_NAME).name === FLAGD_NAME,
new Error(
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
OpenFeature.getProviderMetadata(E2E_CLIENT_NAME).name
}`,
),
);
console.log('flagd provider configured!');
return containers;
}
jest.setTimeout(50000);
describe('rpc', () => {
let containers: StartedTestContainer[] = [];
beforeAll(async () => {
containers = await setup();
}, 60000);
afterAll(async () => {
await OpenFeature.close();
for (const container of containers) {
await container.stop();
}
});
const features = [
loadFeature(GHERKIN_FLAGD_FEATURE),
loadFeature(GHERKIN_EVALUATION_FEATURE),
loadFeature(GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE),
];
autoBindSteps(features, [flagStepDefinitions]);
const state: State = {
resolverType: 'rpc',
options: {},
config: undefined,
events: [],
};
autoBindSteps(
loadFeatures(GHERKIN_FLAGD, {
tagFilter:
// remove filters as we add support for features
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
'@rpc and not @targetURI and not @customCert and not @events and not @stream and not @grace and not @metadata and not @contextEnrichment and not @caching',
scenarioNameTemplate: (vars) => {
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
},
}),
steps.map((step) => step(state)),
);
});

View File

@ -1,4 +1,5 @@
import { Config, FlagdProviderOptions, getConfig } from './configuration';
import type { Config, FlagdProviderOptions } from './configuration';
import { getConfig } from './configuration';
import { DEFAULT_MAX_CACHE_SIZE } from './constants';
describe('Configuration', () => {
@ -18,6 +19,7 @@ describe('Configuration', () => {
cache: 'lru',
resolverType: 'rpc',
selector: '',
deadlineMs: 500,
});
});
@ -55,6 +57,7 @@ describe('Configuration', () => {
selector,
offlineFlagSourcePath,
defaultAuthority,
deadlineMs: 500,
});
});
@ -68,6 +71,7 @@ describe('Configuration', () => {
resolverType: 'rpc',
selector: '',
defaultAuthority: '',
deadlineMs: 500,
};
process.env['FLAGD_HOST'] = 'override';

View File

@ -18,6 +18,13 @@ export interface Config {
*/
port: number;
/**
* The deadline for connections.
*
* @default 500
*/
deadlineMs: number;
/**
* Determines if TLS should be used.
*
@ -79,6 +86,7 @@ export interface Config {
export type FlagdProviderOptions = Partial<Config>;
const DEFAULT_CONFIG: Omit<Config, 'port' | 'resolverType'> = {
deadlineMs: 500,
host: 'localhost',
tls: false,
selector: '',
@ -93,6 +101,7 @@ const DEFAULT_IN_PROCESS_CONFIG: Config = { ...DEFAULT_CONFIG, resolverType: 'in
enum ENV_VAR {
FLAGD_HOST = 'FLAGD_HOST',
FLAGD_PORT = 'FLAGD_PORT',
FLAGD_DEADLINE_MS = 'FLAGD_DEADLINE_MS',
FLAGD_TLS = 'FLAGD_TLS',
FLAGD_SOCKET_PATH = 'FLAGD_SOCKET_PATH',
FLAGD_CACHE = 'FLAGD_CACHE',
@ -103,38 +112,60 @@ enum ENV_VAR {
FLAGD_DEFAULT_AUTHORITY = 'FLAGD_DEFAULT_AUTHORITY',
}
const getEnvVarConfig = (): Partial<Config> => ({
...(process.env[ENV_VAR.FLAGD_HOST] && {
host: process.env[ENV_VAR.FLAGD_HOST],
}),
...(Number(process.env[ENV_VAR.FLAGD_PORT]) && {
port: Number(process.env[ENV_VAR.FLAGD_PORT]),
}),
...(process.env[ENV_VAR.FLAGD_TLS] && {
tls: process.env[ENV_VAR.FLAGD_TLS]?.toLowerCase() === 'true',
}),
...(process.env[ENV_VAR.FLAGD_SOCKET_PATH] && {
socketPath: process.env[ENV_VAR.FLAGD_SOCKET_PATH],
}),
...((process.env[ENV_VAR.FLAGD_CACHE] === 'lru' || process.env[ENV_VAR.FLAGD_CACHE] === 'disabled') && {
cache: process.env[ENV_VAR.FLAGD_CACHE],
}),
...(process.env[ENV_VAR.FLAGD_MAX_CACHE_SIZE] && {
maxCacheSize: Number(process.env[ENV_VAR.FLAGD_MAX_CACHE_SIZE]),
}),
...(process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR] && {
selector: process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR],
}),
...((process.env[ENV_VAR.FLAGD_RESOLVER] === 'rpc' || process.env[ENV_VAR.FLAGD_RESOLVER] === 'in-process') && {
resolverType: process.env[ENV_VAR.FLAGD_RESOLVER],
}),
...(process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH] && {
offlineFlagSourcePath: process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH],
}),
...(process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY] && {
defaultAuthority: process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY],
}),
});
function checkEnvVarResolverType() {
return (
process.env[ENV_VAR.FLAGD_RESOLVER] &&
(process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase() === 'rpc' ||
process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase() === 'in-process')
);
}
const getEnvVarConfig = (): Partial<Config> => {
let provider = undefined;
if (
process.env[ENV_VAR.FLAGD_RESOLVER] &&
(process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase() === 'rpc' ||
process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase() === 'in-process')
) {
provider = process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase();
}
return {
...(process.env[ENV_VAR.FLAGD_HOST] && {
host: process.env[ENV_VAR.FLAGD_HOST],
}),
...(Number(process.env[ENV_VAR.FLAGD_PORT]) && {
port: Number(process.env[ENV_VAR.FLAGD_PORT]),
}),
...(Number(process.env[ENV_VAR.FLAGD_DEADLINE_MS]) && {
deadlineMs: Number(process.env[ENV_VAR.FLAGD_DEADLINE_MS]),
}),
...(process.env[ENV_VAR.FLAGD_TLS] && {
tls: process.env[ENV_VAR.FLAGD_TLS]?.toLowerCase() === 'true',
}),
...(process.env[ENV_VAR.FLAGD_SOCKET_PATH] && {
socketPath: process.env[ENV_VAR.FLAGD_SOCKET_PATH],
}),
...((process.env[ENV_VAR.FLAGD_CACHE] === 'lru' || process.env[ENV_VAR.FLAGD_CACHE] === 'disabled') && {
cache: process.env[ENV_VAR.FLAGD_CACHE],
}),
...(process.env[ENV_VAR.FLAGD_MAX_CACHE_SIZE] && {
maxCacheSize: Number(process.env[ENV_VAR.FLAGD_MAX_CACHE_SIZE]),
}),
...(process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR] && {
selector: process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR],
}),
...(provider && {
resolverType: provider as ResolverType,
}),
...(process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH] && {
offlineFlagSourcePath: process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH],
}),
...(process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY] && {
defaultAuthority: process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY],
}),
};
};
export function getConfig(options: FlagdProviderOptions = {}) {
const envVarConfig = getEnvVarConfig();

View File

@ -1,16 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ServiceError, status } from '@grpc/grpc-js';
import {
Client,
ErrorCode,
EvaluationContext,
FlagMetadata,
OpenFeature,
ProviderEvents,
StandardResolutionReasons,
} from '@openfeature/server-sdk';
import type { ServiceError } from '@grpc/grpc-js';
import { status } from '@grpc/grpc-js';
import type { Client, EvaluationContext, FlagMetadata } from '@openfeature/server-sdk';
import { ErrorCode, OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/server-sdk';
import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
import {
import type {
EventStreamResponse,
ResolveBooleanRequest,
ResolveBooleanResponse,
@ -22,10 +16,11 @@ import {
ResolveObjectResponse,
ResolveStringRequest,
ResolveStringResponse,
ServiceClient,
} from '../proto/ts/flagd/evaluation/v1/evaluation';
import { ServiceClient } from '../proto/ts/flagd/evaluation/v1/evaluation';
import { FlagdProvider } from './flagd-provider';
import { FlagChangeMessage, GRPCService } from './service/grpc/grpc-service';
import type { FlagChangeMessage } from './service/grpc/grpc-service';
import { GRPCService } from './service/grpc/grpc-service';
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
import { EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from './constants';
@ -149,7 +144,7 @@ describe(FlagdProvider.name, () => {
new FlagdProvider(
undefined,
undefined,
new GRPCService({ host: '', port: 123, tls: false }, basicServiceClientMock),
new GRPCService({ deadlineMs: 100, host: '', port: 123, tls: false }, basicServiceClientMock),
),
);
client = OpenFeature.getClient('basic test');
@ -306,7 +301,10 @@ describe(FlagdProvider.name, () => {
new FlagdProvider(
undefined,
undefined,
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
new GRPCService(
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
streamingServiceClientMock,
),
)
.initialize()
.then(() => {
@ -331,7 +329,10 @@ describe(FlagdProvider.name, () => {
new FlagdProvider(
undefined,
undefined,
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
new GRPCService(
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
streamingServiceClientMock,
),
),
);
// fire message saying provider is ready
@ -380,7 +381,10 @@ describe(FlagdProvider.name, () => {
new FlagdProvider(
undefined,
undefined,
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
new GRPCService(
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
streamingServiceClientMock,
),
),
);
// fire message saying provider is ready
@ -412,7 +416,10 @@ describe(FlagdProvider.name, () => {
new FlagdProvider(
undefined,
undefined,
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
new GRPCService(
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
streamingServiceClientMock,
),
),
);
// fire message saying provider is ready
@ -501,7 +508,10 @@ describe(FlagdProvider.name, () => {
const provider = new FlagdProvider(
undefined,
undefined,
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
new GRPCService(
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
streamingServiceClientMock,
),
);
provider.initialize().catch(() => {
// ignore
@ -579,7 +589,7 @@ describe(FlagdProvider.name, () => {
new FlagdProvider(
undefined,
undefined,
new GRPCService({ host: '', port: 123, tls: false }, errServiceClientMock),
new GRPCService({ deadlineMs: 100, host: '', port: 123, tls: false }, errServiceClientMock),
),
);
client = OpenFeature.getClient('errors test');
@ -666,7 +676,7 @@ describe(FlagdProvider.name, () => {
new FlagdProvider(
undefined,
undefined,
new GRPCService({ host: '', port: 123, tls: false }, errServiceClientMock),
new GRPCService({ deadlineMs: 100, host: '', port: 123, tls: false }, errServiceClientMock),
),
);
});

View File

@ -1,15 +1,9 @@
import {
EvaluationContext,
JsonValue,
Logger,
OpenFeatureEventEmitter,
Provider,
ProviderEvents,
ResolutionDetails,
} from '@openfeature/server-sdk';
import { FlagdProviderOptions, getConfig } from './configuration';
import type { EvaluationContext, JsonValue, Logger, Provider, ResolutionDetails } from '@openfeature/server-sdk';
import { OpenFeatureEventEmitter, ProviderEvents } from '@openfeature/server-sdk';
import type { FlagdProviderOptions } from './configuration';
import { getConfig } from './configuration';
import { GRPCService } from './service/grpc/grpc-service';
import { Service } from './service/service';
import type { Service } from './service/service';
import { InProcessService } from './service/in-process/in-process-service';
export class FlagdProvider implements Provider {

View File

@ -1,4 +1,4 @@
import { ClientReadableStream } from '@grpc/grpc-js';
import type { ClientReadableStream } from '@grpc/grpc-js';
export const closeStreamIfDefined = (stream: ClientReadableStream<unknown> | undefined) => {
/**

View File

@ -1,20 +1,17 @@
import { ClientReadableStream, ClientUnaryCall, ServiceError, credentials, status, ClientOptions } from '@grpc/grpc-js';
import type { ClientOptions, ClientReadableStream, ClientUnaryCall, ServiceError } from '@grpc/grpc-js';
import { credentials, status } from '@grpc/grpc-js';
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
import type { EvaluationContext, FlagValue, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
import {
EvaluationContext,
FlagNotFoundError,
FlagValue,
GeneralError,
JsonValue,
Logger,
ParseError,
ResolutionDetails,
StandardResolutionReasons,
TypeMismatchError,
} from '@openfeature/server-sdk';
import { LRUCache } from 'lru-cache';
import { promisify } from 'node:util';
import {
import type {
EventStreamResponse,
ResolveBooleanRequest,
ResolveBooleanResponse,
@ -26,12 +23,12 @@ import {
ResolveObjectResponse,
ResolveStringRequest,
ResolveStringResponse,
ServiceClient,
} from '../../../proto/ts/flagd/evaluation/v1/evaluation';
import { Config } from '../../configuration';
import { ServiceClient } from '../../../proto/ts/flagd/evaluation/v1/evaluation';
import type { Config } from '../../configuration';
import { DEFAULT_MAX_CACHE_SIZE, EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from '../../constants';
import { FlagdProvider } from '../../flagd-provider';
import { Service } from '../service';
import type { Service } from '../service';
import { closeStreamIfDefined } from '../common';
type AnyResponse =
@ -70,6 +67,8 @@ export class GRPCService implements Service {
private _cache: LRUCache<string, ResolutionDetails<FlagValue>> | undefined;
private _cacheEnabled = false;
private _eventStream: ClientReadableStream<EventStreamResponse> | undefined = undefined;
private _deadline: number;
private get _cacheActive() {
// the cache is "active" (able to be used) if the config enabled it, AND the gRPC stream is live
return this._cacheEnabled && this._client.getChannel().getConnectivityState(false) === ConnectivityState.READY;
@ -95,6 +94,7 @@ export class GRPCService implements Service {
tls ? credentials.createSsl() : credentials.createInsecure(),
clientOptions,
);
this._deadline = config.deadlineMs;
if (config.cache === 'lru') {
this._cacheEnabled = true;
@ -165,7 +165,7 @@ export class GRPCService implements Service {
// close the previous stream if we're reconnecting
closeStreamIfDefined(this._eventStream);
const stream = this._client.eventStream({}, {});
const stream = this._client.eventStream({ waitForReady: true }, {});
stream.on('error', (err: Error) => {
rejectConnect?.(err);
this.handleError(reconnectCallback, changedCallback, disconnectCallback);

View File

@ -1,7 +1,7 @@
import fs from 'fs';
import { FileFetch } from './file-fetch';
import { FlagdCore } from '@openfeature/flagd-core';
import { Logger } from '@openfeature/server-sdk';
import type { Logger } from '@openfeature/server-sdk';
jest.mock('fs', () => ({
...jest.requireActual('fs'),

View File

@ -1,5 +1,6 @@
import { Logger, OpenFeatureError, GeneralError } from '@openfeature/server-sdk';
import { DataFetch } from '../data-fetch';
import type { Logger } from '@openfeature/server-sdk';
import { OpenFeatureError, GeneralError } from '@openfeature/server-sdk';
import type { DataFetch } from '../data-fetch';
import { promises as fsPromises, watchFile, unwatchFile } from 'fs';
const encoding = 'utf8';

View File

@ -1,6 +1,6 @@
import { GrpcFetch } from './grpc-fetch';
import { Config } from '../../../configuration';
import { FlagSyncServiceClient, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
import type { Config } from '../../../configuration';
import type { FlagSyncServiceClient, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
let watchStateCallback: () => void = () => ({});
@ -44,7 +44,14 @@ const serviceMock: FlagSyncServiceClient = {
} as unknown as FlagSyncServiceClient;
describe('grpc fetch', () => {
const cfg: Config = { host: 'localhost', port: 8000, tls: false, socketPath: '', defaultAuthority: 'test-authority' };
const cfg: Config = {
deadlineMs: 500,
host: 'localhost',
port: 8000,
tls: false,
socketPath: '',
defaultAuthority: 'test-authority',
};
afterEach(() => {
jest.clearAllMocks();

View File

@ -1,9 +1,12 @@
import { ClientReadableStream, ServiceError, credentials, ClientOptions } from '@grpc/grpc-js';
import { GeneralError, Logger } from '@openfeature/server-sdk';
import { FlagSyncServiceClient, SyncFlagsRequest, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
import { Config } from '../../../configuration';
import type { ClientReadableStream, ServiceError, ClientOptions } from '@grpc/grpc-js';
import { credentials } from '@grpc/grpc-js';
import type { Logger } from '@openfeature/server-sdk';
import { GeneralError } from '@openfeature/server-sdk';
import type { SyncFlagsRequest, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
import { FlagSyncServiceClient } from '../../../../proto/ts/flagd/sync/v1/sync';
import type { Config } from '../../../configuration';
import { closeStreamIfDefined } from '../../common';
import { DataFetch } from '../data-fetch';
import type { DataFetch } from '../data-fetch';
/**
* Implements the gRPC sync contract to fetch flag data.

View File

@ -1,4 +1,4 @@
import { DataFetch } from './data-fetch';
import type { DataFetch } from './data-fetch';
import { InProcessService } from './in-process-service';
describe('In-process-service', () => {
@ -13,7 +13,7 @@ describe('In-process-service', () => {
it('should sync and allow to resolve flags', async () => {
// given
const service = new InProcessService({ host: '', port: 0, tls: false }, dataFetcher);
const service = new InProcessService({ deadlineMs: 500, host: '', port: 0, tls: false }, dataFetcher);
// when
await service.connect(jest.fn, jest.fn, jest.fn);
@ -31,7 +31,7 @@ describe('In-process-service', () => {
it('should include scope as flag metadata', async () => {
// given
const selector = 'devFlags';
const service = new InProcessService({ host: '', port: 0, tls: false, selector }, dataFetcher);
const service = new InProcessService({ deadlineMs: 500, host: '', port: 0, tls: false, selector }, dataFetcher);
// when
await service.connect(jest.fn, jest.fn, jest.fn);
@ -44,7 +44,7 @@ describe('In-process-service', () => {
it('should not override existing scope in flag metadata', async () => {
// given
const selector = 'devFlags';
const service = new InProcessService({ host: '', port: 0, tls: false, selector }, dataFetcher);
const service = new InProcessService({ deadlineMs: 500, host: '', port: 0, tls: false, selector }, dataFetcher);
// when
await service.connect(jest.fn, jest.fn, jest.fn);

View File

@ -7,9 +7,9 @@ import type {
Logger,
ResolutionDetails,
} from '@openfeature/server-sdk';
import { Config } from '../../configuration';
import { Service } from '../service';
import { DataFetch } from './data-fetch';
import type { Config } from '../../configuration';
import type { Service } from '../service';
import type { DataFetch } from './data-fetch';
import { FileFetch } from './file/file-fetch';
import { GrpcFetch } from './grpc/grpc-fetch';

View File

@ -1,4 +1,4 @@
import { EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
import type { EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
export interface Service {
connect(

View File

@ -1,19 +1,18 @@
import {
import type {
EvaluationContext,
FlagValue,
JsonValue,
Logger,
OpenFeatureEventEmitter,
Provider,
ProviderEvents,
ProviderMetadata,
ResolutionDetails,
ResolutionReason,
TypeMismatchError,
} from '@openfeature/web-sdk';
import { OpenFeatureEventEmitter, ProviderEvents, TypeMismatchError } from '@openfeature/web-sdk';
import { createFlagsmithInstance } from 'flagsmith';
import { IFlagsmith, IInitConfig, IState } from 'flagsmith/types';
import { FlagType, typeFactory } from './type-factory';
import type { IFlagsmith, IInitConfig, IState } from 'flagsmith/types';
import type { FlagType } from './type-factory';
import { typeFactory } from './type-factory';
export class FlagsmithClientProvider implements Provider {
readonly metadata: ProviderMetadata = {

View File

@ -1,4 +1,4 @@
import { IFlagsmithResponse, IInitConfig } from 'flagsmith/types';
import type { IFlagsmithResponse, IInitConfig } from 'flagsmith/types';
type Flatten<T> = T extends unknown[] ? T[number] : T;
type FeatureResponse = Flatten<IFlagsmithResponse['flags']>;
type Callback = (err: Error | null, val: string | null) => void;

View File

@ -1,4 +1,4 @@
import { FlagValue } from '@openfeature/web-sdk';
import type { FlagValue } from '@openfeature/web-sdk';
export type FlagType = 'string' | 'number' | 'object' | 'boolean';

View File

@ -1,5 +1,26 @@
# Changelog
## [0.1.5](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.4...flipt-web-provider-v0.1.5) (2025-06-06)
### ✨ New Features
* **flipt-web:** update types to match flipt-client-js ([#1303](https://github.com/open-feature/js-sdk-contrib/issues/1303)) ([9e8496a](https://github.com/open-feature/js-sdk-contrib/commit/9e8496a384e65bee8d6cb096bdfaa909e2bfa311))
## [0.1.4](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.3...flipt-web-provider-v0.1.4) (2025-06-04)
### 🐛 Bug Fixes
* **deps:** update dependency @flipt-io/flipt-client-js to v0.0.2 ([#1272](https://github.com/open-feature/js-sdk-contrib/issues/1272)) ([d7f8746](https://github.com/open-feature/js-sdk-contrib/commit/d7f8746434f58333a2458418c35749c125932369))
## [0.1.3](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.2...flipt-web-provider-v0.1.3) (2025-04-10)
### 🧹 Chore
* **flipt:** swap underlying flipt web sdk ([#1244](https://github.com/open-feature/js-sdk-contrib/issues/1244)) ([dba2a28](https://github.com/open-feature/js-sdk-contrib/commit/dba2a280014e998341487fc2cb1fcb410275d8d6))
## [0.1.2](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.1...flipt-web-provider-v0.1.2) (2025-02-11)

View File

@ -2,7 +2,7 @@
[Flipt](https://www.flipt.io/) is an open source developer friendly feature flagging solution, that allows for easy management and fast feature evaluation.
This provider is an implementation on top of the official [Flipt Browser Client Side SDK](https://www.npmjs.com/package/@flipt-io/flipt-client-browser).
This provider is an implementation on top of the official [Flipt JavaScript Client Side SDK](https://www.npmjs.com/package/@flipt-io/flipt-client-js).
The main difference between this provider and [`@openfeature/flipt-provider`](https://www.npmjs.com/package/@openfeature/flipt-provider) is that it uses a **static evaluation context**.
This provider is more sustainable for client-side implementation.

View File

@ -1,27 +1,30 @@
{
"name": "@openfeature/flipt-web-provider",
"version": "0.1.2",
"version": "0.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openfeature/flipt-web-provider",
"version": "0.1.2",
"dependencies": {
"@flipt-io/flipt-client-browser": "^0.3.1",
"tslib": "^2.3.0"
},
"devDependencies": {
"undici": "^6.13.0"
},
"version": "0.1.5",
"license": "Apache-2.0",
"peerDependencies": {
"@flipt-io/flipt-client-js": "^0.0.1 || ^0.0.2 || ^0.0.6 || ^0.2.0",
"@openfeature/web-sdk": "^1.0.0"
}
},
"node_modules/@flipt-io/flipt-client-browser": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@flipt-io/flipt-client-browser/-/flipt-client-browser-0.3.1.tgz",
"integrity": "sha512-1MFuQuHRENnzVooxrfQjFBLNBfE5uGBJmF2NuPFXTYMZn+sGelFovuNVuKlHqegI3Dqzz9Al2qJlkeFo+MhHxg=="
"node_modules/@flipt-io/flipt-client-js": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@flipt-io/flipt-client-js/-/flipt-client-js-0.2.0.tgz",
"integrity": "sha512-3VIU2pDUyHafp3ahgxHOde81fKP+5hvSm9Q+8hqt9+D7MCIWNvHTLqFo5hc87WSgq3TlR8ILRWfFC18BE1AmeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"node-fetch": "^3.3.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@openfeature/core": {
"version": "1.4.0",
@ -38,18 +41,94 @@
"@openfeature/core": "1.4.0"
}
},
"node_modules/tslib": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
},
"node_modules/undici": {
"version": "6.20.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz",
"integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==",
"dev": true,
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"peer": true,
"engines": {
"node": ">=18.17"
"node": ">= 12"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"peer": true,
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"peer": true,
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"peer": true,
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"peer": true,
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"peer": true,
"engines": {
"node": ">= 8"
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@openfeature/flipt-web-provider",
"version": "0.1.2",
"version": "0.1.5",
"license": "Apache-2.0",
"main": "./src/index.js",
"typings": "./src/index.d.ts",
@ -9,13 +9,7 @@
"current-version": "echo $npm_package_version"
},
"peerDependencies": {
"@openfeature/web-sdk": "^1.0.0"
},
"devDependencies": {
"undici": "^6.13.0"
},
"dependencies": {
"@flipt-io/flipt-client-browser": "^0.3.1",
"undici": "^5.0.0"
"@openfeature/web-sdk": "^1.0.0",
"@flipt-io/flipt-client-js": "^0.0.1 || ^0.0.2 || ^0.0.6 || ^0.2.0"
}
}

View File

@ -1 +1,2 @@
export * from './lib/flipt-web-provider';
export * from './lib/models';

View File

@ -1,4 +1,4 @@
import { EvaluationContext } from '@openfeature/server-sdk';
import type { EvaluationContext } from '@openfeature/server-sdk';
import { transformContext } from './context-transformer';
describe('context-transformer', () => {

View File

@ -1,4 +1,4 @@
import { EvaluationContext } from '@openfeature/web-sdk';
import type { EvaluationContext } from '@openfeature/web-sdk';
export function transformContext(context: EvaluationContext): Record<string, string> {
const evalContext: Record<string, string> = {};

View File

@ -28,12 +28,12 @@ describe('FliptWebProvider', () => {
describe('method resolveStringEvaluation', () => {
it('should throw general error for non-existent flag', () => {
expect(() => {
provider.resolveStringEvaluation('nonExistent', 'default', { fizz: 'buzz' });
provider.resolveStringEvaluation('nonExistent', 'default', { targetingKey: '1234', fizz: 'buzz' });
}).toThrow(GeneralError);
});
it('should return right value if key exists', () => {
const value = provider.resolveStringEvaluation('flag_string', 'default', { fizz: 'buzz' });
const value = provider.resolveStringEvaluation('flag_string', 'default', { targetingKey: '1234', fizz: 'buzz' });
expect(value).toHaveProperty('value', 'variant1');
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
});
@ -42,12 +42,12 @@ describe('FliptWebProvider', () => {
describe('method resolveNumberEvaluation', () => {
it('should throw general error for non-existent flag', () => {
expect(() => {
provider.resolveNumberEvaluation('nonExistent', 1, { fizz: 'buzz' });
provider.resolveNumberEvaluation('nonExistent', 1, { targetingKey: '1234', fizz: 'buzz' });
}).toThrow(GeneralError);
});
it('should return right value if key exists', () => {
const value = provider.resolveNumberEvaluation('flag_number', 0, { fizz: 'buzz' });
const value = provider.resolveNumberEvaluation('flag_number', 0, { targetingKey: '1234', fizz: 'buzz' });
expect(value).toHaveProperty('value', 5);
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
});
@ -56,12 +56,12 @@ describe('FliptWebProvider', () => {
describe('method resolveBooleanEvaluation', () => {
it('should throw general error for non-existent flag', () => {
expect(() => {
provider.resolveBooleanEvaluation('nonExistent', false, { fizz: 'buzz' });
provider.resolveBooleanEvaluation('nonExistent', false, { targetingKey: '1234', fizz: 'buzz' });
}).toThrow(GeneralError);
});
it('should return right value if key exists', () => {
const value = provider.resolveBooleanEvaluation('flag_boolean', false, { fizz: 'buzz' });
const value = provider.resolveBooleanEvaluation('flag_boolean', false, { targetingKey: '1234', fizz: 'buzz' });
expect(value).toHaveProperty('value', true);
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
});
@ -70,12 +70,16 @@ describe('FliptWebProvider', () => {
describe('method resolveObjectEvaluation', () => {
it('should throw general error for non-existent flag', () => {
expect(() => {
provider.resolveObjectEvaluation('nonExistent', {}, { fizz: 'buzz' });
provider.resolveObjectEvaluation('nonExistent', {}, { targetingKey: '1234', fizz: 'buzz' });
}).toThrow(GeneralError);
});
it('should return right value if key exists', () => {
const value = provider.resolveObjectEvaluation('flag_object', { fizz: 'buzz' }, { fizz: 'buzz' });
const value = provider.resolveObjectEvaluation(
'flag_object',
{ fizz: 'buzz' },
{ targetingKey: '1234', fizz: 'buzz' },
);
expect(value).toHaveProperty('value', { foo: 'bar' });
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
});
@ -83,7 +87,7 @@ describe('FliptWebProvider', () => {
it('should throw TypeMismatchError on non-number value', () => {
expect(() => {
provider.resolveNumberEvaluation('flag_string', 0, { fizz: 'buzz' });
provider.resolveNumberEvaluation('flag_string', 0, { targetingKey: '1234', fizz: 'buzz' });
}).toThrow(TypeMismatchError);
});
});

View File

@ -1,16 +1,8 @@
import {
EvaluationContext,
Provider,
JsonValue,
ResolutionDetails,
Logger,
StandardResolutionReasons,
TypeMismatchError,
GeneralError,
ProviderFatalError,
} from '@openfeature/web-sdk';
import { FliptEvaluationClient } from '@flipt-io/flipt-client-browser';
import { EvaluationReason, FliptWebProviderOptions } from './models';
import type { EvaluationContext, Provider, JsonValue, ResolutionDetails, Logger } from '@openfeature/web-sdk';
import { StandardResolutionReasons, TypeMismatchError, GeneralError, ProviderFatalError } from '@openfeature/web-sdk';
import { FliptClient } from '@flipt-io/flipt-client-js/browser';
import type { FliptWebProviderOptions } from './models';
import { EvaluationReason } from './models';
import { transformContext } from './context-transformer';
export class FliptWebProvider implements Provider {
@ -28,7 +20,7 @@ export class FliptWebProvider implements Provider {
private _options?: FliptWebProviderOptions;
// client is the Flipt client reference
private _client?: FliptEvaluationClient;
private _client?: FliptClient;
readonly runsOn = 'client';
@ -48,7 +40,8 @@ export class FliptWebProvider implements Provider {
async initializeClient() {
try {
this._client = await FliptEvaluationClient.init(this._namespace || 'default', {
this._client = await FliptClient.init({
namespace: this._namespace || 'default',
url: this._options?.url || 'http://localhost:8080',
fetcher: this._options?.fetcher,
authentication: this._options?.authentication,
@ -70,7 +63,11 @@ export class FliptWebProvider implements Provider {
const evalContext: Record<string, string> = transformContext(context);
try {
const result = this._client?.evaluateBoolean(flagKey, context.targetingKey ?? '', evalContext);
const result = this._client?.evaluateBoolean({
flagKey,
entityId: context.targetingKey ?? '',
context: evalContext,
});
switch (result?.reason) {
case EvaluationReason.DEFAULT:
@ -135,7 +132,11 @@ export class FliptWebProvider implements Provider {
const evalContext: Record<string, string> = transformContext(context);
try {
const result = this._client?.evaluateVariant(flagKey, context.targetingKey ?? '', evalContext);
const result = this._client?.evaluateVariant({
flagKey,
entityId: context.targetingKey ?? '',
context: evalContext,
});
if (result?.reason === EvaluationReason.FLAG_DISABLED) {
return {

View File

@ -1,7 +1,13 @@
export interface FliptWebProviderOptions {
url?: string;
authentication?: FliptWebProviderAuthentication;
fetcher?: () => Promise<Response>;
fetcher?: FliptFetcher;
}
export type FliptFetcher = (args?: FliptFetcherOptions) => Promise<Response>;
export interface FliptFetcherOptions {
etag?: string;
}
export interface FliptClientTokenAuthentication {

View File

@ -1,5 +1,12 @@
# Changelog
## [0.1.3](https://github.com/open-feature/js-sdk-contrib/compare/flipt-provider-v0.1.2...flipt-provider-v0.1.3) (2025-04-10)
### 🧹 Chore
* update nx packages ([#1147](https://github.com/open-feature/js-sdk-contrib/issues/1147)) ([7f310fe](https://github.com/open-feature/js-sdk-contrib/commit/7f310fe87101b8aa793e1436e63c7602ccc202e3))
## [0.1.2](https://github.com/open-feature/js-sdk-contrib/compare/flipt-provider-v0.1.1...flipt-provider-v0.1.2) (2024-10-23)

View File

@ -1,12 +1,12 @@
{
"name": "@openfeature/flipt-provider",
"version": "0.1.2",
"version": "0.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openfeature/flipt-provider",
"version": "0.1.2",
"version": "0.1.3",
"dependencies": {
"tslib": "^2.3.0"
},

View File

@ -1,6 +1,6 @@
{
"name": "@openfeature/flipt-provider",
"version": "0.1.2",
"version": "0.1.3",
"license": "Apache-2.0",
"main": "./src/index.js",
"typings": "./src/index.d.ts",

View File

@ -1,4 +1,4 @@
import { EvaluationContext } from '@openfeature/server-sdk';
import type { EvaluationContext } from '@openfeature/server-sdk';
import { transformContext } from './context-transformer';
describe('context-transformer', () => {

View File

@ -1,4 +1,4 @@
import { EvaluationContext } from '@openfeature/server-sdk';
import type { EvaluationContext } from '@openfeature/server-sdk';
export function transformContext(context: EvaluationContext): Record<string, string> {
const evalContext: Record<string, string> = {};

View File

@ -1,11 +1,14 @@
import { AuthenticationStrategy, FliptClient } from '@flipt-io/flipt';
import {
import type { AuthenticationStrategy } from '@flipt-io/flipt';
import { FliptClient } from '@flipt-io/flipt';
import type {
EvaluationContext,
Provider,
JsonValue,
ResolutionDetails,
ProviderStatus,
ProviderMetadata,
} from '@openfeature/server-sdk';
import {
ProviderStatus,
TypeMismatchError,
ProviderNotReadyError,
StandardResolutionReasons,

View File

@ -1,5 +1,17 @@
# Changelog
## [0.2.6](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-web-provider-v0.2.5...go-feature-flag-web-provider-v0.2.6) (2025-05-26)
### ✨ New Features
* **goff web:** Support tracking events ([#1268](https://github.com/open-feature/js-sdk-contrib/issues/1268)) ([d9ffcec](https://github.com/open-feature/js-sdk-contrib/commit/d9ffcec1652aa96eefccc45dfe079ca126d55142))
### 🧹 Chore
* **main:** release flipt-web-provider 0.1.3 ([#1253](https://github.com/open-feature/js-sdk-contrib/issues/1253)) ([7be62ae](https://github.com/open-feature/js-sdk-contrib/commit/7be62ae45f4dfbaefecc6205a28060698fdd884d))
## [0.2.5](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-web-provider-v0.2.4...go-feature-flag-web-provider-v0.2.5) (2025-01-23)

View File

@ -1,12 +1,12 @@
{
"name": "@openfeature/go-feature-flag-web-provider",
"version": "0.2.5",
"version": "0.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openfeature/go-feature-flag-web-provider",
"version": "0.2.5",
"version": "0.2.6",
"peerDependencies": {
"@openfeature/web-sdk": "*"
}

View File

@ -1,6 +1,6 @@
{
"name": "@openfeature/go-feature-flag-web-provider",
"version": "0.2.5",
"version": "0.2.6",
"license": "Apache-2.0",
"scripts": {
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",

View File

@ -0,0 +1,71 @@
import type { Logger } from '@openfeature/web-sdk';
import type { ExporterMetadataValue, FeatureEvent, GoFeatureFlagWebProviderOptions, TrackingEvent } from './model';
import { GoffApiController } from './controller/goff-api';
import { CollectorError } from './errors/collector-error';
import { copy } from 'copy-anything';
type Timer = ReturnType<typeof setInterval>;
export class CollectorManager {
// bgSchedulerId contains the id of the setInterval that is running.
private bgScheduler?: Timer;
// dataCollectorBuffer contains all the FeatureEvents that we need to send to the relay-proxy for data collection.
private dataCollectorBuffer?: Array<FeatureEvent<any> | TrackingEvent>;
// dataFlushInterval interval time (in millisecond) we use to call the relay proxy to collect data.
private readonly dataFlushInterval: number;
// logger is the Open Feature logger to use
private logger?: Logger;
// dataCollectorMetadata are the metadata used when calling the data collector endpoint
private readonly dataCollectorMetadata: Record<string, ExporterMetadataValue>;
private readonly goffApiController: GoffApiController;
constructor(options: GoFeatureFlagWebProviderOptions, logger?: Logger) {
this.dataFlushInterval = options.dataFlushInterval || 1000 * 60;
this.logger = logger;
this.goffApiController = new GoffApiController(options);
this.dataCollectorMetadata = {
provider: 'web',
openfeature: true,
...options.exporterMetadata,
};
}
init() {
this.bgScheduler = setInterval(async () => await this.callGoffDataCollection(), this.dataFlushInterval);
this.dataCollectorBuffer = [];
}
async close() {
clearInterval(this.bgScheduler);
// We call the data collector with what is still in the buffer.
await this.callGoffDataCollection();
}
add(event: FeatureEvent<any> | TrackingEvent) {
if (this.dataCollectorBuffer) {
this.dataCollectorBuffer.push(event);
}
}
/**
* callGoffDataCollection is a function called periodically to send the usage of the flag to the
* central service in charge of collecting the data.
*/
async callGoffDataCollection() {
const dataToSend = copy(this.dataCollectorBuffer) || [];
this.dataCollectorBuffer = [];
try {
await this.goffApiController.collectData(dataToSend, this.dataCollectorMetadata);
} catch (e) {
if (!(e instanceof CollectorError)) {
throw e;
}
this.logger?.error(e);
// if we have an issue calling the collector, we put the data back in the buffer
this.dataCollectorBuffer = [...this.dataCollectorBuffer, ...dataToSend];
return;
}
}
}

View File

@ -1,6 +1,7 @@
import { GoFeatureFlagEvaluationContext } from './model';
import type { GoFeatureFlagEvaluationContext } from './model';
import { transformContext } from './context-transformer';
import { TargetingKeyMissingError, EvaluationContext } from '@openfeature/web-sdk';
import type { EvaluationContext } from '@openfeature/web-sdk';
import { TargetingKeyMissingError } from '@openfeature/web-sdk';
describe('contextTransformer', () => {
it('should use the targetingKey as user key', () => {

View File

@ -1,5 +1,6 @@
import { GoFeatureFlagEvaluationContext } from './model';
import { TargetingKeyMissingError, EvaluationContext } from '@openfeature/web-sdk';
import type { GoFeatureFlagEvaluationContext } from './model';
import type { EvaluationContext } from '@openfeature/web-sdk';
import { TargetingKeyMissingError } from '@openfeature/web-sdk';
/**
* transformContext takes the raw OpenFeature context returns a GoFeatureFlagEvaluationContext.

View File

@ -1,6 +1,6 @@
import fetchMock from 'fetch-mock-jest';
import { GoffApiController } from './goff-api';
import { GoFeatureFlagWebProviderOptions } from '../model';
import type { GoFeatureFlagWebProviderOptions } from '../model';
describe('Collect Data API', () => {
beforeEach(() => {

Some files were not shown because too many files have changed in this diff Show More