Compare commits

..

19 Commits

Author SHA1 Message Date
semantic-release-bot 543df23111 release(version): Release open-telemetry 1.0.0-alpha.7 [skip ci]
# [1.0.0-alpha.7](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.6...open-telemetry-v1.0.0-alpha.7) (2022-06-17)

### Features

* Touch otel ([#19](https://github.com/open-feature/node-sdk-contrib/issues/19)) ([d2fe992](d2fe992871))
2022-06-17 19:05:45 +00:00
Michael Beemer d2fe992871
feat: Touch otel (#19) 2022-06-17 15:04:37 -04:00
semantic-release-bot ea91cf4044 release(version): Release flagd-rest 1.0.0-alpha.1 [skip ci]
# 1.0.0-alpha.1 (2022-06-17)

### Bug Fixes

* Comment for clarity ([#18](https://github.com/open-feature/node-sdk-contrib/issues/18)) ([cb25a0e](cb25a0e571))
* Flagd tag fix ([#17](https://github.com/open-feature/node-sdk-contrib/issues/17)) ([2c0b892](2c0b892035))

### Features

* Flagd http provider ([#15](https://github.com/open-feature/node-sdk-contrib/issues/15)) ([ea6e51c](ea6e51c997))
* remove trace name and version ([#10](https://github.com/open-feature/node-sdk-contrib/issues/10)) ([a8f3ef1](a8f3ef119c))
* set license to apache 2 ([#13](https://github.com/open-feature/node-sdk-contrib/issues/13)) ([56cb146](56cb14630d))
* set publish config access to public ([#12](https://github.com/open-feature/node-sdk-contrib/issues/12)) ([30308f6](30308f69ae))
2022-06-17 18:56:04 +00:00
semantic-release-bot 97353c0e89 release(version): Release open-telemetry 1.0.0-alpha.6 [skip ci]
# [1.0.0-alpha.6](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.5...open-telemetry-v1.0.0-alpha.6) (2022-06-17)

### Bug Fixes

* Comment for clarity ([#18](https://github.com/open-feature/node-sdk-contrib/issues/18)) ([cb25a0e](cb25a0e571))
2022-06-17 18:56:04 +00:00
Michael Beemer cb25a0e571
fix: Comment for clarity (#18) 2022-06-17 14:54:55 -04:00
semantic-release-bot 4e7edfeb23 release(version): Release flagd-rest 1.0.0-alpha.1 [skip ci]
# 1.0.0-alpha.1 (2022-06-17)

### Bug Fixes

* Flagd tag fix ([#17](https://github.com/open-feature/node-sdk-contrib/issues/17)) ([2c0b892](2c0b892035))

### Features

* Flagd http provider ([#15](https://github.com/open-feature/node-sdk-contrib/issues/15)) ([ea6e51c](ea6e51c997))
* remove trace name and version ([#10](https://github.com/open-feature/node-sdk-contrib/issues/10)) ([a8f3ef1](a8f3ef119c))
* set license to apache 2 ([#13](https://github.com/open-feature/node-sdk-contrib/issues/13)) ([56cb146](56cb14630d))
* set publish config access to public ([#12](https://github.com/open-feature/node-sdk-contrib/issues/12)) ([30308f6](30308f69ae))
2022-06-17 18:40:11 +00:00
semantic-release-bot f38d302537 release(version): Release open-telemetry 1.0.0-alpha.5 [skip ci]
# [1.0.0-alpha.5](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.4...open-telemetry-v1.0.0-alpha.5) (2022-06-17)

### Bug Fixes

* Flagd tag fix ([#17](https://github.com/open-feature/node-sdk-contrib/issues/17)) ([2c0b892](2c0b892035))
2022-06-17 18:40:11 +00:00
Michael Beemer 2c0b892035
fix: Flagd tag fix (#17) 2022-06-17 14:39:03 -04:00
semantic-release-bot 63fa06b290 release(version): Release open-telemetry 1.0.0-alpha.4 [skip ci]
# [1.0.0-alpha.4](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.3...open-telemetry-v1.0.0-alpha.4) (2022-06-17)

### Features

* Flagd http provider ([#15](https://github.com/open-feature/node-sdk-contrib/issues/15)) ([ea6e51c](ea6e51c997))
2022-06-17 18:23:15 +00:00
Michael Beemer ea6e51c997
feat: Flagd http provider (#15) 2022-06-17 14:21:59 -04:00
semantic-release-bot bf93c42c1b release(version): Release open-telemetry 1.0.0-alpha.3 [skip ci]
# [1.0.0-alpha.3](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.2...open-telemetry-v1.0.0-alpha.3) (2022-06-15)

### Features

* set license to apache 2 ([#13](https://github.com/open-feature/node-sdk-contrib/issues/13)) ([56cb146](56cb14630d))
2022-06-15 20:41:47 +00:00
Michael Beemer 56cb14630d
feat: set license to apache 2 (#13)
set license to apache 2
2022-06-15 16:34:58 -04:00
semantic-release-bot b9e620ac80 release(version): Release open-telemetry 1.0.0-alpha.2 [skip ci]
# [1.0.0-alpha.2](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.1...open-telemetry-v1.0.0-alpha.2) (2022-06-15)

### Features

* set publish config access to public ([#12](https://github.com/open-feature/node-sdk-contrib/issues/12)) ([30308f6](30308f69ae))
2022-06-15 20:27:19 +00:00
Michael Beemer 30308f69ae
feat: set publish config access to public (#12) 2022-06-15 16:25:59 -04:00
semantic-release-bot b941afed7b release(version): Release open-telemetry 1.0.0-alpha.1 [skip ci]
# 1.0.0-alpha.1 (2022-06-15)

### Features

* remove trace name and version ([#10](https://github.com/open-feature/node-sdk-contrib/issues/10)) ([a8f3ef1](a8f3ef119c))
2022-06-15 20:18:46 +00:00
Michael Beemer 936939ab0b
ci: remove dry run release flag (#11) 2022-06-15 16:17:47 -04:00
Michael Beemer a8f3ef119c
feat: remove trace name and version (#10) 2022-06-15 16:10:53 -04:00
Michael Beemer 2a1567d58b Add semantic release logic 2022-06-15 15:45:05 -04:00
Michael Beemer f1d7c4cabd ci: add pr lint github action 2022-06-15 15:31:24 -04:00
550 changed files with 26433 additions and 49616 deletions

View File

@ -1,21 +1,12 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"plugins": ["@nrwl/nx"],
"overrides": [
{
"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": [
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
@ -32,42 +23,13 @@
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": "*.json",
"parser": "jsonc-eslint-parser",
"extends": ["plugin:@nrwl/nx/typescript"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"buildTargets": ["lint"],
"includeTransitiveDependencies": false,
"checkMissingDependencies": true,
"checkObsoleteDependencies": true,
"checkVersionMismatches": true,
"ignoredDependencies": ["jest-cucumber", "jest"],
"ignoredFiles": ["**/test/**", "**/tests/*", "**/spec/**", "**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/jest.*"]
}
]
}
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nrwl/nx/javascript"],
"rules": {}
}
]
}

View File

@ -1,28 +0,0 @@
---
name: Add hook
about: Add provider template
title: Add {my-behavior} hook
labels: enhancement
assignees: ''
---
# Add {my-behavior} hook
## Reasoning
I want to add a hook which performs {my-behavior}.
- it would be great if {my-behavior} could be included in some flag evaluations
- {my-behavior} would enable developers to be more productive!
- other reasons!
## Requirements:
- [ ] generate hook via [tooling](https://github.com/open-feature/js-sdk-contrib/blob/main/CONTRIBUTING.md#adding-a-module)
- [ ] implement `Hook` interface
- [ ] add tests
- [ ] complete README
- [ ] add the new provider to the [OpenFeature docs](https://github.com/open-feature/openfeature.dev/issues/new/choose)
Keep in mind the CONTRIBUTING guidelines: https://github.com/open-feature/js-sdk-contrib/blob/main/CONTRIBUTING.md

View File

@ -1,32 +0,0 @@
---
name: Add provider
about: Add provider template
title: Add {my-system} provider
labels: enhancement
assignees: ''
---
# Add {my-system} provider
## Reasoning
I want to add an OpenFeature provider for {my-system}. This would be valuable because...
- everybody uses {my-system}!
- {my system} supports {these things}!
- other reasons!
## Requirements:
- [ ] generate provider via [tooling](https://github.com/open-feature/js-sdk-contrib/blob/main/CONTRIBUTING.md#adding-a-module)
- [ ] implement `Provider` interface
- [ ] add tests
- [ ] complete README
- [ ] add the new provider to the [OpenFeature docs](https://github.com/open-feature/openfeature.dev/issues/new/choose)
## Resources
- {useful-link}
Keep in mind the CONTRIBUTING guidelines: https://github.com/open-feature/js-sdk-contrib/blob/main/CONTRIBUTING.md

View File

@ -1,39 +0,0 @@
# Keep all in alphabetical order
components:
libs/hooks/open-telemetry:
- beeme1mr
- toddbaert
libs/providers/aws-ssm:
- gdegiorgio
libs/providers/config-cat:
- lukas-reining
- adams85
libs/providers/config-cat-web:
- lukas-reining
- adams85
libs/providers/env-var:
- beeme1mr
- toddbaert
libs/providers/flagd:
- beeme1mr
- toddbaert
libs/providers/flagd-web:
- beeme1mr
- toddbaert
libs/providers/go-feature-flag:
- thomaspoignant
libs/providers/go-feature-flag-web:
- thomaspoignant
libs/providers/launchdarkly-client:
- kinyoklion
- mateoc
- sago2k8
libs/providers/flipt:
- markphelps
libs/providers/flipt-web:
- markphelps
libs/providers/unleash-web:
- jarebudev
ignored-authors:
- renovate-bot

View File

@ -1,55 +1,36 @@
name: CI
on:
push:
branches: ['main']
branches:
- main
- beta
- alpha
pull_request:
branches: ['main']
branches:
- main
- beta
- alpha
jobs:
lint-test-build:
# Needed for nx-set-shas when run on the main branch
permissions:
actions: read
contents: read
main:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x, 24.x]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: recursive
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ matrix.node-version }}
- uses: nrwl/nx-set-shas@v2
- run: npm ci
- uses: nrwl/nx-set-shas@v3
# This line is needed for nx affected to work when CI is running on a PR
- run: git branch --track main origin/main || true
- run: npx nx workspace-lint
- 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 --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
- 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
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
submodules: recursive
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: 'npm'
- name: Install
run: npm ci
- name: e2e
run: npm run e2e
- name: Release
if: ${{ success() && (github.event_name != 'pull_request' || github.event.action == 'closed' && github.event.pull_request.merged == true) }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx nx affected --target release --output-style stream

View File

@ -1,18 +0,0 @@
name: 'Component Owners'
on:
pull_request_target:
permissions:
contents: read # to read changed files
issues: write # to read/write issue assignees
pull-requests: write # to read/write PR reviewers
jobs:
run_self:
runs-on: ubuntu-latest
name: Auto Assign Owners
steps:
- uses: dyladan/component-owners@58bd86e9814d23f1525d0a970682cead459fa783
with:
config-file: .github/component_owners.yml
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,4 +1,4 @@
name: 'Lint PR'
name: "Lint PR"
on:
pull_request_target:
@ -12,29 +12,6 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5
id: lint_pr_title
- uses: amannn/action-semantic-pull-request@v4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Hey there and thank you for opening this pull request! 👋🏼
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
```
${{ steps.lint_pr_title.outputs.error_message }}
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2
with:
header: pr-title-lint-error
delete: true

View File

@ -1,50 +0,0 @@
on:
push:
branches:
- main
name: Run Release Please
jobs:
release-please:
environment: publish
runs-on: ubuntu-latest
# Release-please creates a PR that tracks all changes
steps:
- uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3
id: release
with:
command: manifest
token: ${{secrets.GITHUB_TOKEN}}
default-branch: main
# The logic below handles the npm publication:
- name: Checkout Repository
if: ${{ steps.release.outputs.releases_created }}
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
submodules: recursive
- uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0
with:
github_token: ${{ github.token }}
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: ${{ steps.release.outputs.releases_created }}
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Build Packages
if: ${{ steps.release.outputs.releases_created }}
run: |
npm install
npm run package
# Release Please has already incremented versions and published tags, so we just
# need to publish all unpublished versions to NPM here
# Our scripts only publish versions that do not already exist.
- name: Publish to NPM
if: ${{ steps.release.outputs.releases_created }}
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
run: npm run publish

13
.gitignore vendored
View File

@ -33,20 +33,7 @@ npm-debug.log
yarn-error.log
testem.log
/typings
.nx
# System Files
.DS_Store
Thumbs.db
# generated files
proto
.nx
# yalc stuff
.yalc
yalc.lock
# Generated by @nx/js
.verdaccio

19
.gitmodules vendored
View File

@ -1,19 +0,0 @@
[submodule "libs/providers/flagd/schemas"]
path = libs/providers/flagd/schemas
url = https://github.com/open-feature/flagd-schemas.git
[submodule "libs/providers/flagd-web/schemas"]
path = libs/providers/flagd-web/schemas
url = https://github.com/open-feature/flagd-schemas.git
[submodule "libs/providers/flagd/spec"]
path = libs/providers/flagd/spec
url = https://github.com/open-feature/spec.git
[submodule "libs/shared/flagd-core/flagd-schemas"]
path = libs/shared/flagd-core/flagd-schemas
url = https://github.com/open-feature/flagd-schemas.git
[submodule "libs/shared/flagd-core/test-harness"]
path = libs/shared/flagd-core/test-harness
url = https://github.com/open-feature/flagd-testbed
branch = v2.8.0
[submodule "libs/shared/flagd-core/spec"]
path = libs/shared/flagd-core/spec
url = https://github.com/open-feature/spec

2
.nvmrc
View File

@ -1 +1 @@
20
16

View File

@ -2,6 +2,3 @@
/dist
/coverage
/.nx/cache
/.nx/workspace-data

View File

@ -1,4 +1,3 @@
{
"printWidth": 120,
"singleQuote": true
}

View File

@ -1,25 +0,0 @@
{
"libs/hooks/open-telemetry": "0.4.0",
"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.6",
"libs/providers/launchdarkly-client": "0.3.2",
"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.3",
"libs/providers/flipt": "0.1.3",
"libs/providers/flagsmith-client": "0.1.3",
"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.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.3"
}

13
.releaserc.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
branches: [
'main',
{
name: 'beta',
prerelease: true,
},
{
name: 'alpha',
prerelease: true,
},
],
};

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"firsttris.vscode-jest-runner"
]
}

View File

@ -1,6 +0,0 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence
#
# 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/maintainers

View File

@ -1,49 +0,0 @@
# Contributing
## System Requirements
node 16+, npm 8+ are recommended.
## Compilation target(s)
We target `es2015`, and require all modules to publish both ES-modules and CommonJS modules. The generators described below will configure this automatically.
## Adding a module
The project is a monorepo that uses NX to manage it's modules.
The project has some NX generators for creating [hooks](https://openfeature.dev/docs/reference/concepts/hooks) and [providers](https://openfeature.dev/docs/reference/concepts/provider).
`npm run generate-hook` <- generates a hook module
`npm run generate-provider` <- generates a provider module
The script will create the basic code scaffolding, and infrastructure to publish the artifact.
## Documentation
Any published modules must have documentation in their root directory, explaining the basic purpose of the module as well as installation and usage instructions.
Instructions for how to develop a module should also be included (required system dependencies, instructions for testing locally, etc).
## Testing
Any published modules must have reasonable test coverage.
The NX scaffolding will generate stub tests for you when you create your project.
Use `npm run test` to test the entire project.
Use `npx nx test {MODULE NAME}` to test just a single module.
Module names can be listed using `npx nx show projects`.
## Releases
This repo uses _Release Please_ to release packages.
Release Please sets up a running PR that tracks all changes for the library components, and maintains the versions according to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), generated when [PRs are merged](https://github.com/amannn/action-semantic-pull-request).
When Release Please's running PR is merged, any changed artifacts are published.
Breaking changes should be identified by using a semantic PR title.
## Dependencies
Keep dependencies to a minimum, especially non-dev dependencies.
The JS-SDK should be a _peer dependency_ of your module.
Run `npm run package`, and then verify the dependencies in `dist/libs/{MODULE_PATH}/package.json` are appropriate.
Keep in mind, though one version of the JS-SDK is used for all modules in testing, each module may have a different peer-dependency requirement for the JS-SDK (e.g: one module may require ^1.2.0 while another might require ^1.4.0).
Be sure to properly express the JS-SDK peer dependency version your module requires.

View File

@ -1,24 +1,15 @@
# OpenFeature JS Contributions
# OpenFeature Node Contributions
This repository is intended for OpenFeature contributions which are not included in the [OpenFeature SDK](https://github.com/open-feature/js-sdk).
![Experimental](https://img.shields.io/badge/experimental-breaking%20changes%20allowed-yellow)
![Alpha](https://img.shields.io/badge/alpha-release-red)
This repository is intended for OpenFeature contributions which are not included in the [OpenFeature SDK](https://github.com/open-feature/node-sdk).
The project includes:
- [Providers](./libs/providers)
- [Hooks](./libs/hooks)
## Contributing
Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide.
## Useful links
* For more information on OpenFeature, visit [openfeature.dev](https://openfeature.dev)
* For help or feedback on this project, join us on [Slack][slack] or create a [GitHub issue][github-issue].
## License
[Apache License 2.0](LICENSE)
[github-issue]: https://github.com/open-feature/js-sdk-contrib/issues/new/choose
[slack]: https://cloud-native.slack.com/archives/C0344AANLA1
Apache 2.0 - See [LICENSE](./license) for more information.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,3 +0,0 @@
{
"module": "commonjs"
}

View File

@ -1,3 +0,0 @@
{
"babelrcRoots": ["*"]
}

View File

@ -1,5 +1,5 @@
import { getJestProjectsAsync } from '@nx/jest';
import { getJestProjects } from '@nrwl/jest';
export default async () => ({
projects: await getJestProjectsAsync(),
});
export default {
projects: getJestProjects(),
};

View File

@ -1,3 +1,3 @@
const nxPreset = require('@nx/jest/preset').default;
const nxPreset = require('@nrwl/jest/preset').default;
module.exports = { ...nxPreset };

View File

@ -1,6 +1,6 @@
# OpenFeature JavaScript Hooks
# OpenFeature NodeJS Hooks
Hooks are a mechanism whereby application developers can add arbitrary behavior to flag evaluation. They operate similarly to middleware in many web frameworks. Please see the [spec](https://openfeature.dev/docs/specification/sections/hooks) for more details.
Hooks are a mechanism whereby application developers can add arbitrary behavior to flag evaluation. They operate similarly to middleware in many web frameworks. Please see the [spec](https://github.com/open-feature/spec/blob/main/specification/flag-evaluation/hooks.md) for more details.
## Add a new hook

View File

@ -1,209 +1,48 @@
# Changelog
## [0.4.0](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hooks-v0.3.0...open-telemetry-hooks-v0.4.0) (2024-03-25)
### ⚠ BREAKING CHANGES
* update OpenFeature SDK peer ([#798](https://github.com/open-feature/js-sdk-contrib/issues/798))
### ✨ New Features
* update OpenFeature SDK peer ([#798](https://github.com/open-feature/js-sdk-contrib/issues/798)) ([ebd16b9](https://github.com/open-feature/js-sdk-contrib/commit/ebd16b9630bcc6b253a7061a144e8d476cd8b586))
### 🧹 Chore
* address lint issues ([#642](https://github.com/open-feature/js-sdk-contrib/issues/642)) ([bbd9aee](https://github.com/open-feature/js-sdk-contrib/commit/bbd9aee896dc4a0817f379b799a1b8d331ee76c6))
* fix lint issues and bump server sdk version ([#715](https://github.com/open-feature/js-sdk-contrib/issues/715)) ([bd57177](https://github.com/open-feature/js-sdk-contrib/commit/bd571770f3a1a01bd62663dc3473273449f96c5c))
## [0.3.0](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hooks-v0.2.4...open-telemetry-hooks-v0.3.0) (2023-10-11)
### ⚠ BREAKING CHANGES
* use @openfeature/server-sdk peer ([#608](https://github.com/open-feature/js-sdk-contrib/issues/608))
### 🐛 Bug Fixes
* packaging issues impacting babel/react ([#596](https://github.com/open-feature/js-sdk-contrib/issues/596)) ([0446eab](https://github.com/open-feature/js-sdk-contrib/commit/0446eab5cf9b45ce7de251b4f5feb8df1d499b9d))
### 🧹 Chore
* update nx, run migrations ([#552](https://github.com/open-feature/js-sdk-contrib/issues/552)) ([a88d8fc](https://github.com/open-feature/js-sdk-contrib/commit/a88d8fc097789fd7f56011e6ebb66070f52c6e56))
* use @openfeature/server-sdk peer ([#608](https://github.com/open-feature/js-sdk-contrib/issues/608)) ([ae3732a](https://github.com/open-feature/js-sdk-contrib/commit/ae3732a9068f684517db28ea1ae27b29a35e6b16))
## [0.2.4](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hooks-v0.2.3...open-telemetry-hooks-v0.2.4) (2023-08-03)
### ✨ New Features
* add custom attrs to traces ([#520](https://github.com/open-feature/js-sdk-contrib/issues/520)) ([28fbd12](https://github.com/open-feature/js-sdk-contrib/commit/28fbd12f206202ab626d30bdfbbe5b04e75626af))
## [0.2.3](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hooks-v0.2.2...open-telemetry-hooks-v0.2.3) (2023-07-28)
### ✨ New Features
* custom attribute support ([#499](https://github.com/open-feature/js-sdk-contrib/issues/499)) ([c2deddf](https://github.com/open-feature/js-sdk-contrib/commit/c2deddf288e1eb9e55d56ea58eba5f8afb8cccc5))
## [0.2.2](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hooks-v0.2.1...open-telemetry-hooks-v0.2.2) (2023-07-19)
### 🐛 Bug Fixes
* otel metric semantic convs ([#475](https://github.com/open-feature/js-sdk-contrib/issues/475)) ([6febfb0](https://github.com/open-feature/js-sdk-contrib/commit/6febfb0d09849fb4a722af2c1333ebb4b2386684))
## [0.2.1](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hooks-v0.2.0...open-telemetry-hooks-v0.2.1) (2023-07-12)
### 🐛 Bug Fixes
* update README to remove deprecation ([#465](https://github.com/open-feature/js-sdk-contrib/issues/465)) ([ac5b91b](https://github.com/open-feature/js-sdk-contrib/commit/ac5b91b60eaf39b31fc9899f20ad4fef792a50e8))
## [0.2.0](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hooks-v0.1.0...open-telemetry-hooks-v0.2.0) (2023-07-12)
### ⤴️ Upgrade Instructions
* if upgrading from `@openfeature/open-telemetry-hook`, import and use `TracingHook` instead of `OpenTelemetryHook`
### ✨ New Features
* add metrics hook ([#448](https://github.com/open-feature/js-sdk-contrib/issues/448)) ([131db1e](https://github.com/open-feature/js-sdk-contrib/commit/131db1ef47962288e1c7723e768296307d06837b))
## [6.0.2](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v6.0.1...open-telemetry-hook-v6.0.2) (2023-07-12)
### ⚠️ Deprecation warning
* the `@openfeature/open-telemetry-hook` package is now deprecated, use `@openfeature/open-telemetry-hooks` instead
### 🧹 Chore
* correct publish executor ([#378](https://github.com/open-feature/js-sdk-contrib/issues/378)) ([395ed18](https://github.com/open-feature/js-sdk-contrib/commit/395ed186de8811ae249f087821fdbdf8899c19f2))
* **deps:** update dependency @openfeature/js-sdk to v1.3.1 ([#409](https://github.com/open-feature/js-sdk-contrib/issues/409)) ([5bf9932](https://github.com/open-feature/js-sdk-contrib/commit/5bf993208825e3e1eded941decc067125935d912))
* migrate to nx 16 ([#366](https://github.com/open-feature/js-sdk-contrib/issues/366)) ([7a9c201](https://github.com/open-feature/js-sdk-contrib/commit/7a9c201d16fd7f070a1bcd2e359487ba6e7b78d7))
### 🐛 Bug Fixes
* deprecate otel hook ([#449](https://github.com/open-feature/js-sdk-contrib/issues/449)) ([58aa56c](https://github.com/open-feature/js-sdk-contrib/commit/58aa56cdc13ee5177b64a0a1e126b9d31c8d5756))
## [6.0.1](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v6.0.0...open-telemetry-hook-v6.0.1) (2023-01-19)
### Bug Fixes
* module issues with types ([#212](https://github.com/open-feature/js-sdk-contrib/issues/212)) ([d2b97dd](https://github.com/open-feature/js-sdk-contrib/commit/d2b97dd24c952661ce08724a84e4b312860a9211))
## [6.0.0](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v5.1.1...open-telemetry-hook-v6.0.0) (2022-12-29)
### ⚠ BREAKING CHANGES
* update the otel hook to be spec compliant ([#179](https://github.com/open-feature/js-sdk-contrib/issues/179))
### Features
* update the otel hook to be spec compliant ([#179](https://github.com/open-feature/js-sdk-contrib/issues/179)) ([69b2163](https://github.com/open-feature/js-sdk-contrib/commit/69b2163be1729697ebc69549aa8fb6e61be1b94d))
### Bug Fixes
* fix ESM and web polyfills issue ([#201](https://github.com/open-feature/js-sdk-contrib/issues/201)) ([acee6e1](https://github.com/open-feature/js-sdk-contrib/commit/acee6e1817a7846251f456455a7218bf98efb00e))
## [5.1.1](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v5.1.0...open-telemetry-hook-v5.1.1) (2022-12-09)
### Bug Fixes
* correct dependencies ([#182](https://github.com/open-feature/js-sdk-contrib/issues/182)) ([16cbe42](https://github.com/open-feature/js-sdk-contrib/commit/16cbe421d6255bd95a78c3914890a63adcce831e))
## [5.0.0](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v4.0.0...open-telemetry-hook-v5.0.0) (2022-10-19)
### ⚠ BREAKING CHANGES
* update OpenFeature SDK version (#137)
### Miscellaneous Chores
* update OpenFeature SDK version ([#137](https://github.com/open-feature/js-sdk-contrib/issues/137)) ([245f024](https://github.com/open-feature/js-sdk-contrib/commit/245f02441d62f7f42627174737943f1556a6a326))
## [4.0.0](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v3.0.0...open-telemetry-hook-v4.0.0) (2022-10-03)
### ⚠ BREAKING CHANGES
* migrate to sdk 0.5.0 (#114)
### Features
* migrate to sdk 0.5.0 ([#114](https://github.com/open-feature/js-sdk-contrib/issues/114)) ([f9e9a55](https://github.com/open-feature/js-sdk-contrib/commit/f9e9a55ad5a16e99bb169fdf1a8d11c959520f7b))
## [3.0.0](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v2.0.0...open-telemetry-hook-v3.0.0) (2022-09-20)
### ⚠ BREAKING CHANGES
* update to js-sdk (#108)
### Features
* update to js-sdk ([#108](https://github.com/open-feature/js-sdk-contrib/issues/108)) ([60d6146](https://github.com/open-feature/js-sdk-contrib/commit/60d6146e30d3ca547e940c3ba441d80fd75d886d))
## [2.0.0](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v1.2.3...open-telemetry-hook-v2.0.0) (2022-08-15)
### ⚠ BREAKING CHANGES
* set openfeature sdk min version to 0.2.0 (#93)
### Features
* Update OTel hook to latest semantic convention ([#65](https://github.com/open-feature/js-sdk-contrib/issues/65)) ([0dd7802](https://github.com/open-feature/js-sdk-contrib/commit/0dd780271fabd7aa7c503a48bff75bebb63b46b9))
### Bug Fixes
* add test ([#71](https://github.com/open-feature/js-sdk-contrib/issues/71)) ([080fc4b](https://github.com/open-feature/js-sdk-contrib/commit/080fc4b3c926728361ad34d6763df7bc2d5ab023))
* change test name ([#75](https://github.com/open-feature/js-sdk-contrib/issues/75)) ([abac20d](https://github.com/open-feature/js-sdk-contrib/commit/abac20d29f54865a18662baacaeb60fb5d8c8175))
* set openfeature sdk min version to 0.2.0 ([#93](https://github.com/open-feature/js-sdk-contrib/issues/93)) ([a733102](https://github.com/open-feature/js-sdk-contrib/commit/a733102f523f9289fdce356a342828cc2e020f48))
* shell scripts in templates ([#73](https://github.com/open-feature/js-sdk-contrib/issues/73)) ([89c8cfe](https://github.com/open-feature/js-sdk-contrib/commit/89c8cfe981348376995f50ca757299077249544e))
## [1.2.3-alpha](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v1.2.2-alpha...open-telemetry-hook-v1.2.3-alpha) (2022-07-21)
### Bug Fixes
* change test name ([#75](https://github.com/open-feature/js-sdk-contrib/issues/75)) ([abac20d](https://github.com/open-feature/js-sdk-contrib/commit/abac20d29f54865a18662baacaeb60fb5d8c8175))
## [1.2.2-alpha](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v1.2.1-alpha...open-telemetry-hook-v1.2.2-alpha) (2022-07-21)
### Bug Fixes
* shell scripts in templates ([#73](https://github.com/open-feature/js-sdk-contrib/issues/73)) ([89c8cfe](https://github.com/open-feature/js-sdk-contrib/commit/89c8cfe981348376995f50ca757299077249544e))
## [1.2.1-alpha](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v1.2.0-alpha...open-telemetry-hook-v1.2.1-alpha) (2022-07-21)
### Bug Fixes
* add test ([#71](https://github.com/open-feature/js-sdk-contrib/issues/71)) ([080fc4b](https://github.com/open-feature/js-sdk-contrib/commit/080fc4b3c926728361ad34d6763df7bc2d5ab023))
## [1.2.0-alpha](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v1.1.0-alpha...open-telemetry-hook-v1.2.0-alpha) (2022-07-21)
# [1.0.0-alpha.7](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.6...open-telemetry-v1.0.0-alpha.7) (2022-06-17)
### Features
* Update OTel hook to latest semantic convention ([#65](https://github.com/open-feature/js-sdk-contrib/issues/65)) ([0dd7802](https://github.com/open-feature/js-sdk-contrib/commit/0dd780271fabd7aa7c503a48bff75bebb63b46b9))
* Touch otel ([#19](https://github.com/open-feature/node-sdk-contrib/issues/19)) ([d2fe992](https://github.com/open-feature/node-sdk-contrib/commit/d2fe99287152d4a1bb12fcb0d9d7e82fafc087f5))
## [1.1.0-alpha](https://github.com/open-feature/js-sdk-contrib/compare/open-telemetry-hook-v1.0.6-alpha...open-telemetry-hook-v1.1.0-alpha) (2022-07-21)
# [1.0.0-alpha.6](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.5...open-telemetry-v1.0.0-alpha.6) (2022-06-17)
### Bug Fixes
* Comment for clarity ([#18](https://github.com/open-feature/node-sdk-contrib/issues/18)) ([cb25a0e](https://github.com/open-feature/node-sdk-contrib/commit/cb25a0e57155a382891821d40b21b046b5e9a81f))
# [1.0.0-alpha.5](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.4...open-telemetry-v1.0.0-alpha.5) (2022-06-17)
### Bug Fixes
* Flagd tag fix ([#17](https://github.com/open-feature/node-sdk-contrib/issues/17)) ([2c0b892](https://github.com/open-feature/node-sdk-contrib/commit/2c0b8920359efd6d04e9300e1550808d5e09e5e4))
# [1.0.0-alpha.4](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.3...open-telemetry-v1.0.0-alpha.4) (2022-06-17)
### Features
* Update OTel hook to latest semantic convention ([#65](https://github.com/open-feature/js-sdk-contrib/issues/65)) ([0dd7802](https://github.com/open-feature/js-sdk-contrib/commit/0dd780271fabd7aa7c503a48bff75bebb63b46b9))
* Flagd http provider ([#15](https://github.com/open-feature/node-sdk-contrib/issues/15)) ([ea6e51c](https://github.com/open-feature/node-sdk-contrib/commit/ea6e51c9975224ab0238430d407af7b9d6d501ec))
# [1.0.0-alpha.3](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.2...open-telemetry-v1.0.0-alpha.3) (2022-06-15)
### Features
* set license to apache 2 ([#13](https://github.com/open-feature/node-sdk-contrib/issues/13)) ([56cb146](https://github.com/open-feature/node-sdk-contrib/commit/56cb14630d49cc8311049bb96edbfed93e6260d1))
# [1.0.0-alpha.2](https://github.com/open-feature/node-sdk-contrib/compare/open-telemetry-v1.0.0-alpha.1...open-telemetry-v1.0.0-alpha.2) (2022-06-15)
### Features
* set publish config access to public ([#12](https://github.com/open-feature/node-sdk-contrib/issues/12)) ([30308f6](https://github.com/open-feature/node-sdk-contrib/commit/30308f69ae0780019cf024fb504a07d09976b77f))
# 1.0.0-alpha.1 (2022-06-15)
### Features
* remove trace name and version ([#10](https://github.com/open-feature/node-sdk-contrib/issues/10)) ([a8f3ef1](https://github.com/open-feature/node-sdk-contrib/commit/a8f3ef119c2d141de49db5857c607b6b0b4776a6))

View File

@ -1,98 +1,23 @@
# OpenTelemetry Hooks
# NodeJS OpenTelemetry Hook for OpenFeature
The OpenTelemetry hooks for OpenFeature provide a [spec compliant][otel-spec] way to automatically add feature flag evaluation information to traces and metrics.
Since feature flags are dynamic and affect runtime behavior, its important to collect relevant feature flag telemetry signals.
These can be used to determine the impact a feature has on application behavior, enabling enhanced observability use cases, such as A/B testing or progressive feature releases.
![Experimental](https://img.shields.io/badge/experimental-breaking%20changes%20allowed-yellow)
## Installation
```
$ npm install @openfeature/open-telemetry-hooks
$ npm install @openfeature/open-telemetry-hook
```
### Peer dependencies
Confirm that the following peer dependencies are installed.
### Required peer dependencies
```
$ npm install @openfeature/server-sdk @opentelemetry/api
$ npm install @openfeature/nodejs-sdk @opentelemetry/api
```
## Hooks
## Building
### TracingHook
Run `npx nx package hooks-open-telemetry` to build the library.
This hook adds a [span event](https://opentelemetry.io/docs/concepts/signals/traces/#span-events) for each feature flag evaluation.
## Running unit tests
### MetricsHook
This hook performs metric collection by tapping into various hook stages. Below are the metrics are extracted by this hook:
- `feature_flag.evaluation_requests_total`
- `feature_flag.evaluation_success_total`
- `feature_flag.evaluation_error_total`
- `feature_flag.evaluation_active_count`
## Usage
OpenFeature provides various ways to register hooks. The location that a hook is registered affects when the hook is run.
It's recommended to register both the `TracingHook` and `MetricsHook` globally in most situations, but it's possible to only enable the hook on specific clients.
You should **never** register these hooks both globally and on a client.
More information on hooks can be found in the [OpenFeature documentation][hook-concept].
### Register Globally
The `TracingHook` and `MetricsHook` can both be set on the OpenFeature singleton.
This will ensure that every flag evaluation will always generate the applicable telemetry signals.
```typescript
import { OpenFeature } from '@openfeature/server-sdk';
import { TracingHook } from '@openfeature/open-telemetry-hooks';
OpenFeature.addHooks(new TracingHook());
```
### Register Per Client
The `TracingHook` and `MetricsHook` can both be set on an individual client. This should only be done if it wasn't set globally and other clients shouldn't use this hook.
Setting the hook on the client will ensure that every flag evaluation performed by this client will always generate the applicable telemetry signals.
```typescript
import { OpenFeature } from '@openfeature/server-sdk';
import { MetricsHook } from '@openfeature/open-telemetry-hooks';
const client = OpenFeature.getClient('my-app');
client.addHooks(new MetricsHook());
```
### Custom Attributes
Custom attributes can be extracted from [flag metadata](https://openfeature.dev/specification/types#flag-metadata) by supplying a `attributeMapper` in the `MetricsHookOptions` or `TracingHookOptions`.
In the case of the `MetricsHook`, these will be added to the `feature_flag.evaluation_success_total` metric.
The `TracingHook` adds them as [span event attributes](https://opentelemetry.io/docs/instrumentation/js/manual/#span-events).
```typescript
// configure an attributeMapper function for a custom property
const attributeMapper: AttributeMapper = (flagMetadata) => {
return {
myCustomAttribute: flagMetadata.someFlagMetadataField,
};
};
const metricsHook = new MetricsHook({ attributeMapper });
const tracingHook = new TracingHook({ attributeMapper });
```
## Development
### Building
Run `nx package hooks-open-telemetry` to build the library.
### Running unit tests
Run `nx test hooks-open-telemetry` to execute the unit tests via [Jest](https://jestjs.io).
[otel-spec]: https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/feature-flags/
[hook-concept]: https://openfeature.dev/docs/reference/concepts/hooks
Run `npx nx test hooks-open-telemetry` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -1,3 +1,3 @@
{
"presets": [["minify", { "builtIns": false }]]
}
"presets": ["minify"]
}

View File

@ -2,13 +2,14 @@
export default {
displayName: 'hooks-open-telemetry',
preset: '../../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
'^.+\\.[tj]s$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/hooks/open-telemetry',
};

View File

@ -1,21 +1,14 @@
{
"name": "@openfeature/open-telemetry-hooks",
"version": "0.4.0",
"license": "Apache-2.0",
"name": "@openfeature/open-telemetry-hook",
"version": "0.0.0-semantic-release",
"type": "commonjs",
"repository": {
"type": "git",
"url": "https://github.com/open-feature/js-sdk-contrib.git",
"url": "https://github.com/open-feature/node-sdk-contrib.git",
"directory": "libs/hooks/open-telemetry"
},
"publishConfig": {
"access": "public"
},
"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",
"current-version": "echo $npm_package_version"
},
"peerDependencies": {
"@openfeature/server-sdk": "^1.13.0",
"@opentelemetry/api": ">=1.3.0"
}
"license": "Apache-2.0"
}

View File

@ -1,28 +1,21 @@
{
"name": "hooks-open-telemetry",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/hooks/open-telemetry/src",
"projectType": "library",
"targets": {
"package": {
"executor": "@nx/rollup:rollup",
"executor": "@nrwl/web:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "libs/hooks/open-telemetry/package.json",
"outputPath": "dist/libs/hooks/open-telemetry",
"entryFile": "libs/hooks/open-telemetry/src/index.ts",
"tsConfig": "libs/hooks/open-telemetry/tsconfig.lib.json",
"compiler": "tsc",
"generateExportsField": true,
"compiler": "babel",
"umdName": "OpenTelemetry",
"external": "all",
"external": ["typescript"],
"format": ["cjs", "esm"],
"assets": [
{
"glob": "package.json",
"input": "./assets",
"output": "./src/"
},
{
"glob": "LICENSE",
"input": "./",
@ -37,7 +30,7 @@
}
},
"build": {
"executor": "@nx/js:tsc",
"executor": "@nrwl/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/hooks/open-telemetry",
@ -47,29 +40,45 @@
}
},
"publish": {
"executor": "nx:run-commands",
"executor": "@nrwl/workspace:run-commands",
"options": {
"command": "npm run publish-if-not-exists",
"cwd": "dist/libs/hooks/open-telemetry"
"command": "node tools/scripts/publish.mjs hooks-open-telemetry {args.ver} {args.tag}"
},
"dependsOn": [
{
"projects": "self",
"target": "package"
}
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/hooks/open-telemetry/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/libs/hooks/open-telemetry"],
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/libs/hooks/open-telemetry"],
"options": {
"jestConfig": "libs/hooks/open-telemetry/jest.config.ts",
"codeCoverage": true,
"coverageDirectory": "coverage/libs/hooks/open-telemetry"
"passWithNoTests": true
}
},
"release": {
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "npx semantic-release --extends ./libs/hooks/open-telemetry/release.config.js",
"parallel": false
},
"dependsOn": [
{
"projects": "self",
"target": "package"
}
]
}
},
"tags": []

View File

@ -0,0 +1,28 @@
const name = 'open-telemetry';
const srcRoot = `libs/hooks/${name}`;
module.exports = {
pkgRoot: `dist/${srcRoot}`,
tagFormat: name + '-v${version}',
commitPaths: [`${srcRoot}/*`],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
[
'@semantic-release/changelog',
{
changelogFile: `${srcRoot}/CHANGELOG.md`,
},
],
'@semantic-release/npm',
[
'@semantic-release/git',
{
assets: [`${srcRoot}/package.json`, `${srcRoot}/CHANGELOG.md`],
message:
`release(version): Release ${name} ` +
'${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
},
],
],
};

View File

@ -1,2 +1 @@
export * from './lib/traces';
export * from './lib/metrics';
export * from './lib/open-telemetry-hook';

View File

@ -1,16 +0,0 @@
// see: https://opentelemetry.io/docs/specs/otel/logs/semantic_conventions/feature-flags/
export const FEATURE_FLAG = 'feature_flag';
export const EXCEPTION_ATTR = 'exception';
export const ACTIVE_COUNT_NAME = `${FEATURE_FLAG}.evaluation_active_count`;
export const REQUESTS_TOTAL_NAME = `${FEATURE_FLAG}.evaluation_requests_total`;
export const SUCCESS_TOTAL_NAME = `${FEATURE_FLAG}.evaluation_success_total`;
export const ERROR_TOTAL_NAME = `${FEATURE_FLAG}.evaluation_error_total`;
export type EvaluationAttributes = { [key: `${typeof FEATURE_FLAG}.${string}`]: string | undefined };
export type ExceptionAttributes = { [EXCEPTION_ATTR]: string };
export const KEY_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.key`;
export const PROVIDER_NAME_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.provider_name`;
export const VARIANT_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.variant`;
export const REASON_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.reason`;

View File

@ -1 +0,0 @@
export * from './metrics-hook';

View File

@ -1,331 +0,0 @@
import type { BeforeHookContext, EvaluationDetails, HookContext } from '@openfeature/server-sdk';
import { StandardResolutionReasons } from '@openfeature/server-sdk';
import opentelemetry from '@opentelemetry/api';
import type { DataPoint, ScopeMetrics } from '@opentelemetry/sdk-metrics';
import { MeterProvider, MetricReader } from '@opentelemetry/sdk-metrics';
import {
ACTIVE_COUNT_NAME,
ERROR_TOTAL_NAME,
KEY_ATTR,
PROVIDER_NAME_ATTR,
REASON_ATTR,
REQUESTS_TOTAL_NAME,
SUCCESS_TOTAL_NAME,
VARIANT_ATTR,
} from '../conventions';
import { MetricsHook } from './metrics-hook';
import type { AttributeMapper } from '../otel-hook';
// no-op "in-memory" reader
class InMemoryMetricReader extends MetricReader {
protected onShutdown(): Promise<void> {
return Promise.resolve();
}
protected onForceFlush(): Promise<void> {
return Promise.resolve();
}
}
describe(MetricsHook.name, () => {
let reader: MetricReader;
beforeAll(() => {
reader = new InMemoryMetricReader();
const provider = new MeterProvider();
provider.addMetricReader(reader);
// Set this MeterProvider to be global to the app being instrumented.
const successful = opentelemetry.metrics.setGlobalMeterProvider(provider);
expect(successful).toBeTruthy();
});
describe('before stage', () => {
it('should increment evaluation_active_count and evaluation_requests_total and set attrs', async () => {
const FLAG_KEY = 'before-test-key';
const PROVIDER_NAME = 'before-provider-name';
const mockHookContext: BeforeHookContext = {
flagKey: FLAG_KEY,
providerMetadata: {
name: PROVIDER_NAME,
},
} as BeforeHookContext;
const hook = new MetricsHook();
hook.before(mockHookContext);
const result = await reader.collect();
expect(
hasDataPointMatching(
result.resourceMetrics.scopeMetrics,
ACTIVE_COUNT_NAME,
0,
(point) =>
point.value === 1 &&
point.attributes[KEY_ATTR] === FLAG_KEY &&
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME,
),
).toBeTruthy();
expect(
hasDataPointMatching(
result.resourceMetrics.scopeMetrics,
REQUESTS_TOTAL_NAME,
0,
(point) =>
point.value === 1 &&
point.attributes[KEY_ATTR] === FLAG_KEY &&
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME,
),
).toBeTruthy();
});
});
describe('after stage', () => {
describe('variant set', () => {
it('should increment evaluation_success_total and set attrs with variant = variant', async () => {
const FLAG_KEY = 'after-test-key';
const PROVIDER_NAME = 'after-provider-name';
const VARIANT = 'one';
const VALUE = 1;
const mockHookContext: HookContext = {
flagKey: FLAG_KEY,
providerMetadata: {
name: PROVIDER_NAME,
},
} as HookContext;
const evaluationDetails: EvaluationDetails<number> = {
variant: VARIANT,
value: VALUE,
reason: StandardResolutionReasons.STATIC,
} as EvaluationDetails<number>;
const hook = new MetricsHook();
hook.after(mockHookContext, evaluationDetails);
const result = await reader.collect();
expect(
hasDataPointMatching(
result.resourceMetrics.scopeMetrics,
SUCCESS_TOTAL_NAME,
0,
(point) =>
point.value === 1 &&
point.attributes[KEY_ATTR] === FLAG_KEY &&
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME &&
point.attributes[VARIANT_ATTR] === VARIANT &&
point.attributes[REASON_ATTR] === StandardResolutionReasons.STATIC,
),
).toBeTruthy();
});
it('should increment evaluation_success_total and set attrs with variant = value', async () => {
const FLAG_KEY = 'after-test-key';
const PROVIDER_NAME = 'after-provider-name';
const VALUE = 1;
const mockHookContext: HookContext = {
flagKey: FLAG_KEY,
providerMetadata: {
name: PROVIDER_NAME,
},
} as HookContext;
const evaluationDetails: EvaluationDetails<number> = {
value: VALUE,
reason: StandardResolutionReasons.STATIC,
} as EvaluationDetails<number>;
const hook = new MetricsHook();
hook.after(mockHookContext, evaluationDetails);
const result = await reader.collect();
expect(
hasDataPointMatching(
result.resourceMetrics.scopeMetrics,
SUCCESS_TOTAL_NAME,
1,
(point) =>
point.value === 1 &&
point.attributes[KEY_ATTR] === FLAG_KEY &&
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME &&
point.attributes[VARIANT_ATTR] === VALUE.toString() &&
point.attributes[REASON_ATTR] === StandardResolutionReasons.STATIC,
),
).toBeTruthy();
});
});
describe('attributeMapper defined', () => {
it('should run attribute mapper', async () => {
const FLAG_KEY = 'after-test-key';
const PROVIDER_NAME = 'after-provider-name';
const VARIANT = 'two';
const VALUE = 2;
const CUSTOM_ATTR_KEY_1 = 'custom1';
const CUSTOM_ATTR_KEY_2 = 'custom2';
const CUSTOM_ATTR_VALUE_1 = 'value1';
const CUSTOM_ATTR_VALUE_2 = 500;
const mockHookContext: HookContext = {
flagKey: FLAG_KEY,
providerMetadata: {
name: PROVIDER_NAME,
},
} as HookContext;
const evaluationDetails: EvaluationDetails<number> = {
flagKey: FLAG_KEY,
variant: VARIANT,
value: VALUE,
reason: StandardResolutionReasons.STATIC,
flagMetadata: {
[CUSTOM_ATTR_KEY_1]: CUSTOM_ATTR_VALUE_1,
[CUSTOM_ATTR_KEY_2]: CUSTOM_ATTR_VALUE_2,
},
} as EvaluationDetails<number>;
// configure a mapper for our custom properties
const attributeMapper: AttributeMapper = (flagMetadata) => {
return {
[CUSTOM_ATTR_KEY_1]: flagMetadata[CUSTOM_ATTR_KEY_1],
[CUSTOM_ATTR_KEY_2]: flagMetadata[CUSTOM_ATTR_KEY_2],
};
};
const hook = new MetricsHook({ attributeMapper });
hook.after(mockHookContext, evaluationDetails);
const result = await reader.collect();
expect(
hasDataPointMatching(
result.resourceMetrics.scopeMetrics,
SUCCESS_TOTAL_NAME,
2,
(point) =>
point.value === 1 &&
point.attributes[KEY_ATTR] === FLAG_KEY &&
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME &&
point.attributes[VARIANT_ATTR] === VARIANT &&
point.attributes[REASON_ATTR] === StandardResolutionReasons.STATIC &&
// custom attributes should be present
point.attributes[CUSTOM_ATTR_KEY_1] === CUSTOM_ATTR_VALUE_1 &&
point.attributes[CUSTOM_ATTR_KEY_2] === CUSTOM_ATTR_VALUE_2,
),
).toBeTruthy();
});
});
describe('attributeMapper throws', () => {
it('should no-op', async () => {
const FLAG_KEY = 'after-test-key';
const PROVIDER_NAME = 'after-provider-name';
const VARIANT = 'three';
const VALUE = 3;
const mockHookContext: HookContext = {
flagKey: FLAG_KEY,
providerMetadata: {
name: PROVIDER_NAME,
},
} as HookContext;
const evaluationDetails: EvaluationDetails<number> = {
flagKey: FLAG_KEY,
variant: VARIANT,
value: VALUE,
reason: StandardResolutionReasons.STATIC,
} as EvaluationDetails<number>;
// configure a mapper that throws
const attributeMapper: AttributeMapper = () => {
throw new Error('fake error');
};
const hook = new MetricsHook({ attributeMapper });
hook.after(mockHookContext, evaluationDetails);
const result = await reader.collect();
expect(
hasDataPointMatching(
result.resourceMetrics.scopeMetrics,
SUCCESS_TOTAL_NAME,
3,
(point) =>
point.value === 1 &&
point.attributes[KEY_ATTR] === FLAG_KEY &&
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME &&
point.attributes[VARIANT_ATTR] === VARIANT &&
point.attributes[REASON_ATTR] === StandardResolutionReasons.STATIC,
),
).toBeTruthy();
});
});
});
describe('finally stage', () => {
it('should decrement evaluation_success_total and set attrs', async () => {
const FLAG_KEY = 'finally-test-key';
const PROVIDER_NAME = 'finally-provider-name';
const mockHookContext: HookContext = {
flagKey: FLAG_KEY,
providerMetadata: {
name: PROVIDER_NAME,
},
} as HookContext;
const hook = new MetricsHook();
hook.finally(mockHookContext);
const result = await reader.collect();
expect(
hasDataPointMatching(
result.resourceMetrics.scopeMetrics,
ACTIVE_COUNT_NAME,
1,
(point) =>
point.value === -1 &&
point.attributes[KEY_ATTR] === FLAG_KEY &&
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME,
),
).toBeTruthy();
});
});
describe('error stage', () => {
it('should decrement evaluation_success_total and set attrs', async () => {
const FLAG_KEY = 'error-test-key';
const PROVIDER_NAME = 'error-provider-name';
const ERROR_MESSAGE = 'error message';
const error = new Error(ERROR_MESSAGE);
const mockHookContext: HookContext = {
flagKey: FLAG_KEY,
providerMetadata: {
name: PROVIDER_NAME,
},
} as HookContext;
const hook = new MetricsHook();
hook.error(mockHookContext, error);
const result = await reader.collect();
expect(
hasDataPointMatching(
result.resourceMetrics.scopeMetrics,
ERROR_TOTAL_NAME,
0,
(point) =>
point.value === 1 &&
point.attributes[KEY_ATTR] === FLAG_KEY &&
point.attributes[PROVIDER_NAME_ATTR] === PROVIDER_NAME,
),
).toBeTruthy();
});
});
});
const hasDataPointMatching = (
scopeMetrics: ScopeMetrics[],
metricName: string,
dataPointIndex: number,
dataPointMatcher: (dataPoint: DataPoint<number>) => boolean,
) => {
const found = scopeMetrics.find((sm) =>
sm.metrics.find((m) => {
const point = m.dataPoints[dataPointIndex] as DataPoint<number>;
if (point) {
return m.descriptor.name === metricName && dataPointMatcher(point);
}
}),
);
if (!found) {
throw Error('Unable to find matching datapoint');
}
return found;
};

View File

@ -1,106 +0,0 @@
import type { BeforeHookContext, Logger } from '@openfeature/server-sdk';
import {
StandardResolutionReasons,
type EvaluationDetails,
type FlagValue,
type Hook,
type HookContext,
} from '@openfeature/server-sdk';
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,
KEY_ATTR,
PROVIDER_NAME_ATTR,
REASON_ATTR,
REQUESTS_TOTAL_NAME,
SUCCESS_TOTAL_NAME,
VARIANT_ATTR,
} from '../conventions';
import type { OpenTelemetryHookOptions } from '../otel-hook';
import { OpenTelemetryHook } from '../otel-hook';
type ErrorEvaluationAttributes = EvaluationAttributes & ExceptionAttributes;
export type MetricsHookOptions = OpenTelemetryHookOptions;
const METER_NAME = 'js.openfeature.dev';
const ACTIVE_DESCRIPTION = 'active flag evaluations counter';
const REQUESTS_DESCRIPTION = 'feature flag evaluation request counter';
const SUCCESS_DESCRIPTION = 'feature flag evaluation success counter';
const ERROR_DESCRIPTION = 'feature flag evaluation error counter';
/**
* A hook that adds conventionally-compliant metrics to feature flag evaluations.
*
* See {@link https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/feature-flags/}
*/
export class MetricsHook extends OpenTelemetryHook implements Hook {
protected name = MetricsHook.name;
private readonly evaluationActiveUpDownCounter: UpDownCounter<EvaluationAttributes>;
private readonly evaluationRequestCounter: Counter<EvaluationAttributes>;
private readonly evaluationSuccessCounter: Counter<EvaluationAttributes | Attributes>;
private readonly evaluationErrorCounter: Counter<ErrorEvaluationAttributes>;
constructor(
options?: MetricsHookOptions,
private readonly logger?: Logger,
) {
super(options, logger);
const meter = metrics.getMeter(METER_NAME);
this.evaluationActiveUpDownCounter = meter.createUpDownCounter(ACTIVE_COUNT_NAME, {
description: ACTIVE_DESCRIPTION,
valueType: ValueType.INT,
});
this.evaluationRequestCounter = meter.createCounter(REQUESTS_TOTAL_NAME, {
description: REQUESTS_DESCRIPTION,
valueType: ValueType.INT,
});
this.evaluationSuccessCounter = meter.createCounter(SUCCESS_TOTAL_NAME, {
description: SUCCESS_DESCRIPTION,
valueType: ValueType.INT,
});
this.evaluationErrorCounter = meter.createCounter(ERROR_TOTAL_NAME, {
description: ERROR_DESCRIPTION,
valueType: ValueType.INT,
});
}
before(hookContext: BeforeHookContext) {
const attributes: EvaluationAttributes = {
[KEY_ATTR]: hookContext.flagKey,
[PROVIDER_NAME_ATTR]: hookContext.providerMetadata.name,
};
this.evaluationActiveUpDownCounter.add(1, attributes);
this.evaluationRequestCounter.add(1, attributes);
}
after(hookContext: Readonly<HookContext<FlagValue>>, evaluationDetails: EvaluationDetails<FlagValue>) {
this.evaluationSuccessCounter.add(1, {
[KEY_ATTR]: hookContext.flagKey,
[PROVIDER_NAME_ATTR]: hookContext.providerMetadata.name,
[VARIANT_ATTR]: evaluationDetails.variant ?? evaluationDetails.value?.toString(),
[REASON_ATTR]: evaluationDetails.reason ?? StandardResolutionReasons.UNKNOWN,
...this.safeAttributeMapper(evaluationDetails?.flagMetadata || {}),
});
}
error(hookContext: Readonly<HookContext<FlagValue>>, error: unknown) {
this.evaluationErrorCounter.add(1, {
[KEY_ATTR]: hookContext.flagKey,
[PROVIDER_NAME_ATTR]: hookContext.providerMetadata.name,
[EXCEPTION_ATTR]: (error as Error)?.message || 'Unknown error',
});
}
finally(hookContext: Readonly<HookContext<FlagValue>>) {
this.evaluationActiveUpDownCounter.add(-1, {
[KEY_ATTR]: hookContext.flagKey,
[PROVIDER_NAME_ATTR]: hookContext.providerMetadata.name,
});
}
}

View File

@ -0,0 +1,150 @@
import { EvaluationDetails, HookContext } from '@openfeature/nodejs-sdk';
const setAttributes = jest.fn();
const setAttribute = jest.fn();
const recordException = jest.fn();
const end = jest.fn();
const startSpan = jest.fn(() => ({
setAttributes,
setAttribute,
recordException,
end,
}));
const getTracer = jest.fn(() => ({ startSpan }));
jest.mock('@opentelemetry/api', () => ({
trace: {
getTracer,
},
}));
// Import must be after the mocks
import { OpenTelemetryHook } from './open-telemetry-hook';
describe('OpenTelemetry Hooks', () => {
const hookContext: HookContext = {
flagKey: 'testFlagKey',
clientMetadata: {
name: 'testClient',
},
providerMetadata: {
name: 'testProvider',
},
context: {},
defaultValue: true,
flagValueType: 'boolean',
};
let otelHook: OpenTelemetryHook;
beforeEach(() => {
otelHook = new OpenTelemetryHook();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should use the same span with all the hooks', () => {
const evaluationDetails: EvaluationDetails<boolean> = {
flagKey: hookContext.flagKey,
value: true,
};
const setSpanMapSpy = jest.spyOn(otelHook['spanMap'], 'set');
const testError = new Error();
otelHook.before(hookContext);
expect(setSpanMapSpy).toBeCalled();
otelHook.after(hookContext, evaluationDetails);
expect(setAttribute).toBeCalledWith('feature_flag.evaluated.value', 'true');
otelHook.error(hookContext, testError);
expect(recordException).toBeCalledWith(testError);
otelHook.finally(hookContext);
expect(end).toBeCalled();
});
describe('before hook', () => {
it('should start a new span', () => {
expect(otelHook.before(hookContext)).toBeUndefined();
expect(getTracer).toBeCalled();
expect(startSpan).toBeCalledWith('feature flag - boolean');
expect(setAttributes).toBeCalledWith({
'feature_flag.client.name': 'testClient',
'feature_flag.client.version': undefined,
'feature_flag.flag_key': 'testFlagKey',
'feature_flag.provider.name': 'testProvider',
});
expect(otelHook['spanMap'].has(hookContext)).toBeTruthy;
});
});
describe('after hook', () => {
it('should set the variant as a span attribute', () => {
const evaluationDetails: EvaluationDetails<boolean> = {
flagKey: hookContext.flagKey,
value: true,
variant: 'enabled',
}; // The before hook should
otelHook.before(hookContext);
otelHook.after(hookContext, evaluationDetails);
expect(setAttribute).toBeCalledWith(
'feature_flag.evaluated.variant',
'enabled'
);
});
it('should set the value as a span attribute', () => {
const evaluationDetails: EvaluationDetails<boolean> = {
flagKey: hookContext.flagKey,
value: true,
}; // The before hook should
otelHook.before(hookContext);
otelHook.after(hookContext, evaluationDetails);
expect(setAttribute).toBeCalledWith(
'feature_flag.evaluated.value',
'true'
);
});
});
describe('error hook', () => {
const testError = new Error();
it('should not call recordException because the span is undefined', () => {
otelHook.error(hookContext, testError);
expect(otelHook['spanMap'].has(hookContext)).toBeFalsy;
expect(recordException).not.toBeCalledWith(testError);
});
it('should call recordException with a test error', () => {
otelHook.before(hookContext);
otelHook.error(hookContext, testError);
expect(otelHook['spanMap'].has(hookContext)).toBeTruthy;
expect(recordException).toBeCalledWith(testError);
});
});
describe('finally hook', () => {
it('should not call end because the span is undefined', () => {
otelHook.finally(hookContext);
expect(otelHook['spanMap'].has(hookContext)).toBeFalsy;
expect(end).not.toBeCalled();
});
it('should call end to finish the span', () => {
otelHook.before(hookContext);
otelHook.finally(hookContext);
expect(otelHook['spanMap'].has(hookContext)).toBeTruthy;
expect(end).toBeCalled();
});
});
});

View File

@ -0,0 +1,66 @@
import {
Hook,
HookContext,
EvaluationDetails,
FlagValue,
} from '@openfeature/nodejs-sdk';
import { Span, Tracer, trace } from '@opentelemetry/api';
const SpanProperties = Object.freeze({
FLAG_KEY: 'feature_flag.flag_key',
CLIENT_NAME: 'feature_flag.client.name',
CLIENT_VERSION: 'feature_flag.client.version',
PROVIDER_NAME: 'feature_flag.provider.name',
VARIANT: 'feature_flag.evaluated.variant',
VALUE: 'feature_flag.evaluated.value',
});
export class OpenTelemetryHook implements Hook {
private spanMap = new WeakMap<HookContext, Span>();
private tracer: Tracer;
constructor() {
this.tracer = trace.getTracer('@openfeature/open-telemetry-hook');
}
before(hookContext: HookContext) {
const span = this.tracer.startSpan(
`feature flag - ${hookContext.flagValueType}`
);
span.setAttributes({
[SpanProperties.FLAG_KEY]: hookContext.flagKey,
[SpanProperties.CLIENT_NAME]: hookContext.clientMetadata.name,
[SpanProperties.CLIENT_VERSION]: hookContext.clientMetadata.version,
[SpanProperties.PROVIDER_NAME]: hookContext.providerMetadata.name,
});
this.spanMap.set(hookContext, span);
}
after(
hookContext: HookContext,
evaluationDetails: EvaluationDetails<FlagValue>
) {
if (evaluationDetails.variant) {
this.spanMap
.get(hookContext)
?.setAttribute(SpanProperties.VARIANT, evaluationDetails.variant);
} else {
this.spanMap
.get(hookContext)
?.setAttribute(
SpanProperties.VALUE,
JSON.stringify(evaluationDetails.value)
);
}
}
error(hookContext: HookContext, err: Error) {
this.spanMap.get(hookContext)?.recordException(err);
}
finally(hookContext: HookContext) {
this.spanMap.get(hookContext)?.end();
}
}

View File

@ -1,30 +0,0 @@
import type { FlagMetadata, Logger } from '@openfeature/server-sdk';
import type { Attributes } from '@opentelemetry/api';
export type AttributeMapper = (flagMetadata: FlagMetadata) => Attributes;
export type OpenTelemetryHookOptions = {
/**
* A function that maps OpenFeature flag metadata values to OpenTelemetry attributes.
*/
attributeMapper?: AttributeMapper;
};
/**
* Base class that does some logging and safely wraps the AttributeMapper.
*/
export abstract class OpenTelemetryHook {
protected safeAttributeMapper: AttributeMapper;
protected abstract name: string;
constructor(options?: OpenTelemetryHookOptions, logger?: Logger) {
this.safeAttributeMapper = (flagMetadata: FlagMetadata) => {
try {
return options?.attributeMapper?.(flagMetadata) || {};
} catch (err) {
logger?.debug(`${this.name}: error in attributeMapper, ${err.message}, ${err.stack}`);
return {};
}
};
}
}

View File

@ -1 +0,0 @@
export * from './tracing-hook';

View File

@ -1,195 +0,0 @@
import type { EvaluationDetails, HookContext } from '@openfeature/server-sdk';
const addEvent = jest.fn();
const recordException = jest.fn();
const getActiveSpan = jest.fn<unknown, unknown[]>(() => ({ addEvent, recordException }));
jest.mock('@opentelemetry/api', () => ({
trace: {
getActiveSpan,
},
}));
// Import must be after the mocks
import { TracingHook } from './tracing-hook';
describe('OpenTelemetry Hooks', () => {
const hookContext: HookContext = {
flagKey: 'testFlagKey',
clientMetadata: {
providerMetadata: {
name: 'fake',
},
name: 'testClient',
},
providerMetadata: {
name: 'testProvider',
},
context: {},
defaultValue: true,
flagValueType: 'boolean',
logger: console,
};
let tracingHook: TracingHook;
afterEach(() => {
jest.clearAllMocks();
});
describe('after stage', () => {
describe('no attribute mapper', () => {
beforeEach(() => {
tracingHook = new TracingHook();
});
it('should use the variant value on the span event', () => {
const evaluationDetails: EvaluationDetails<boolean> = {
flagKey: hookContext.flagKey,
value: true,
variant: 'enabled',
flagMetadata: {},
};
tracingHook.after(hookContext, evaluationDetails);
expect(addEvent).toBeCalledWith('feature_flag', {
'feature_flag.key': 'testFlagKey',
'feature_flag.provider_name': 'testProvider',
'feature_flag.variant': 'enabled',
});
});
it('should use a stringified value as the variant value on the span event', () => {
const evaluationDetails: EvaluationDetails<boolean> = {
flagKey: hookContext.flagKey,
value: true,
flagMetadata: {},
};
tracingHook.after(hookContext, evaluationDetails);
expect(addEvent).toBeCalledWith('feature_flag', {
'feature_flag.key': 'testFlagKey',
'feature_flag.provider_name': 'testProvider',
'feature_flag.variant': 'true',
});
});
it('should set the value without extra quotes if value is already a string', () => {
const evaluationDetails: EvaluationDetails<string> = {
flagKey: hookContext.flagKey,
value: 'already-string',
flagMetadata: {},
};
tracingHook.after(hookContext, evaluationDetails);
expect(addEvent).toBeCalledWith('feature_flag', {
'feature_flag.key': 'testFlagKey',
'feature_flag.provider_name': 'testProvider',
'feature_flag.variant': 'already-string',
});
});
it('should not call addEvent because there is no active span', () => {
getActiveSpan.mockReturnValueOnce(undefined);
const evaluationDetails: EvaluationDetails<boolean> = {
flagKey: hookContext.flagKey,
value: true,
variant: 'enabled',
flagMetadata: {},
};
tracingHook.after(hookContext, evaluationDetails);
expect(addEvent).not.toBeCalled();
});
});
describe('attribute mapper configured', () => {
describe('no error in mapper', () => {
beforeEach(() => {
tracingHook = new TracingHook({
attributeMapper: (flagMetadata) => {
return {
customAttr1: flagMetadata.metadata1,
customAttr2: flagMetadata.metadata2,
customAttr3: flagMetadata.metadata3,
};
},
});
});
it('should run the attribute mapper to add custom attributes, if set', () => {
const evaluationDetails: EvaluationDetails<boolean> = {
flagKey: hookContext.flagKey,
value: true,
variant: 'enabled',
flagMetadata: {
metadata1: 'one',
metadata2: 2,
metadata3: true,
},
};
tracingHook.after(hookContext, evaluationDetails);
expect(addEvent).toBeCalledWith('feature_flag', {
'feature_flag.key': 'testFlagKey',
'feature_flag.provider_name': 'testProvider',
'feature_flag.variant': 'enabled',
customAttr1: 'one',
customAttr2: 2,
customAttr3: true,
});
});
});
describe('error in mapper', () => {
beforeEach(() => {
tracingHook = new TracingHook({
attributeMapper: () => {
throw new Error('fake error');
},
});
});
it('should no-op', () => {
const evaluationDetails: EvaluationDetails<boolean> = {
flagKey: hookContext.flagKey,
value: true,
variant: 'enabled',
flagMetadata: {
metadata1: 'one',
metadata2: 2,
metadata3: true,
},
};
tracingHook.after(hookContext, evaluationDetails);
expect(addEvent).toBeCalledWith('feature_flag', {
'feature_flag.key': 'testFlagKey',
'feature_flag.provider_name': 'testProvider',
'feature_flag.variant': 'enabled',
});
});
});
});
});
describe('error stage', () => {
const testError = new Error();
it('should call recordException with a test error', () => {
tracingHook.error(hookContext, testError);
expect(recordException).toBeCalledWith(testError);
});
it('should not call recordException because there is no active span', () => {
getActiveSpan.mockReturnValueOnce(undefined);
tracingHook.error(hookContext, testError);
expect(recordException).not.toBeCalled();
});
});
});

View File

@ -1,46 +0,0 @@
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 type { OpenTelemetryHookOptions } from '../otel-hook';
import { OpenTelemetryHook } from '../otel-hook';
export type TracingHookOptions = OpenTelemetryHookOptions;
/**
* A hook that adds conventionally-compliant span events to feature flag evaluations.
*
* See {@link https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/feature-flags/}
*/
export class TracingHook extends OpenTelemetryHook implements Hook {
protected name = TracingHook.name;
constructor(options?: TracingHookOptions, logger?: Logger) {
super(options, logger);
}
after(hookContext: HookContext, evaluationDetails: EvaluationDetails<FlagValue>) {
const currentTrace = trace.getActiveSpan();
if (currentTrace) {
let variant = evaluationDetails.variant;
if (!variant) {
if (typeof evaluationDetails.value === 'string') {
variant = evaluationDetails.value;
} else {
variant = JSON.stringify(evaluationDetails.value);
}
}
currentTrace.addEvent(FEATURE_FLAG, {
[KEY_ATTR]: hookContext.flagKey,
[PROVIDER_NAME_ATTR]: hookContext.providerMetadata.name,
[VARIANT_ATTR]: variant,
...this.safeAttributeMapper(evaluationDetails.flagMetadata),
});
}
}
error(_: HookContext, err: Error) {
trace.getActiveSpan()?.recordException(err);
}
}

View File

@ -1,6 +1,6 @@
# OpenFeature JavaScript Providers
# OpenFeature NodeJS Providers
Providers are responsible for performing flag evaluation. They provide an abstraction between the underlying flag management system and OpenFeature itself. This allows providers to be changed without requiring a major code refactor. Please see the [spec](https://openfeature.dev/docs/specification/sections/providers) for more details.
Providers are responsible for performing flag evaluation. They provide an abstraction between the underlying flag management system and OpenFeature itself. This allows providers to be changed without requiring a major code refactor. Please see the [spec](https://github.com/open-feature/spec/blob/main/specification/provider/providers.md) for more details.
## Add a new provider

View File

@ -1,30 +0,0 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"]
}
]
}
}
]
}

View File

@ -1,23 +0,0 @@
# 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)
### ✨ New Features
* **aws-ssm:** implement AWS SSM provider ([#1221](https://github.com/open-feature/js-sdk-contrib/issues/1221)) ([819a247](https://github.com/open-feature/js-sdk-contrib/commit/819a247c41112c2873aa025ac0abd3c62eb53aca))

View File

@ -1,73 +0,0 @@
# AWS SSM Provider
## What is AWS SSM?
AWS Systems Manager (SSM) is a service provided by Amazon Web Services (AWS) that enables users to manage and automate operational tasks across their AWS infrastructure. One of its key components is AWS Systems Manager Parameter Store, which allows users to store, retrieve, and manage configuration data and secrets securely.
SSM Parameter Store can be used to manage application configuration settings, database connection strings, API keys, and other sensitive information. It provides integration with AWS Identity and Access Management (IAM) to control access and encryption through AWS Key Management Service (KMS).
The aws-ssm provider for OpenFeature allows applications to fetch feature flag configurations from AWS SSM Parameter Store, enabling centralized and dynamic configuration management.
## Installation
```
$ npm install @openfeature/aws-ssm-provider
```
## Set AWS Provider
```
OpenFeature.setProvider(
new AwsSsmProvider({
ssmClientConfig: {
region: 'eu-west-1', // Change this to your desired AWS region
// You can setup your aws credentials here or it will be automatically retrieved from env vars
// See https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html
},
// Use an LRUCache for improve performance and optimize AWS SDK Calls to SSM (cost awareness)
cacheOpts: {
enabled: true, // Enable caching
size: 1, // Cache size
ttl: 10, // Time-to-live in seconds
},
})
);
```
# 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!
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();
const flagValue = await featureFlags.getBooleanValue('my-feature-flag', false);
console.log(`Feature flag value: ${flagValue}`);
```

View File

@ -1,3 +0,0 @@
{
"presets": [["minify", { "builtIns": false }]]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
{
"name": "@openfeature/aws-ssm-provider",
"version": "0.1.3",
"dependencies": {
"@aws-sdk/client-ssm": "^3.759.0",
"lru-cache": "^11.0.2",
"tslib": "^2.3.0"
},
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"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",
"current-version": "echo $npm_package_version"
},
"license": "Apache-2.0",
"peerDependencies": {
"@openfeature/server-sdk": "^1.17.0"
},
"devDependencies": {
"@smithy/types": "^4.1.0",
"aws-sdk-client-mock": "^4.1.0"
}
}

View File

@ -1,64 +0,0 @@
{
"name": "providers-aws-ssm",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/providers/aws-ssm/src",
"projectType": "library",
"targets": {
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "npm run publish-if-not-exists",
"cwd": "dist/libs/providers/aws-ssm"
},
"dependsOn": [
{
"target": "package"
}
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/providers/aws-ssm/jest.config.ts"
}
},
"package": {
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "libs/providers/aws-ssm/package.json",
"outputPath": "dist/libs/providers/aws-ssm",
"entryFile": "libs/providers/aws-ssm/src/index.ts",
"tsConfig": "libs/providers/aws-ssm/tsconfig.lib.json",
"compiler": "tsc",
"generateExportsField": true,
"umdName": "aws-ssm",
"external": "all",
"format": ["cjs", "esm"],
"assets": [
{
"glob": "package.json",
"input": "./assets",
"output": "./src/"
},
{
"glob": "LICENSE",
"input": "./",
"output": "./"
},
{
"glob": "README.md",
"input": "./libs/providers/aws-ssm",
"output": "./"
}
]
}
}
},
"tags": []
}

View File

@ -1 +0,0 @@
export * from './lib/aws-ssm-provider';

View File

@ -1,86 +0,0 @@
import { OpenFeature } from '@openfeature/server-sdk';
import { AwsSsmProvider } from '../lib/aws-ssm-provider';
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);
describe('AWS SSM Provider E2E', () => {
const featureFlags = OpenFeature.getClient();
OpenFeature.setProvider(
new AwsSsmProvider({
ssmClientConfig: {
region: 'eu-west-1',
},
cacheOpts: {
enabled: true,
size: 1,
ttl: 10,
},
}),
);
describe('when using OpenFeature with AWS SSM Provider to retrieve a boolean', () => {
it('should use AWS SSM in order to retrieve the value', async () => {
const res: GetParameterCommandOutput = {
Parameter: {
Name: '/lambda/loggingEnabled',
Value: 'true',
},
$metadata: {},
};
ssmMock.on(GetParameterCommand).resolves(res);
const flagValue = await featureFlags.getBooleanValue('/lambda/loggingEnabled', false);
expect(flagValue).toBe(true);
});
});
describe('when using OpenFeature with AWS SSM Provider to retrieve a string', () => {
it('should use AWS SSM in order to retrieve the value', async () => {
const res: GetParameterCommandOutput = {
Parameter: {
Name: '/lambda/logLevel',
Value: 'ERROR',
},
$metadata: {},
};
ssmMock.on(GetParameterCommand).resolves(res);
const flagValue = await featureFlags.getStringValue('/lambda/logLevel', 'INFO');
expect(flagValue).toBe('ERROR');
});
});
describe('when using OpenFeature with AWS SSM Provider to retrieve a number', () => {
it('should use AWS SSM in order to retrieve the value', async () => {
const res: GetParameterCommandOutput = {
Parameter: {
Name: '/lambda/logRetentionInDays',
Value: '3',
},
$metadata: {},
};
ssmMock.on(GetParameterCommand).resolves(res);
const flagValue = await featureFlags.getNumberValue('/lambda/logRetentionInDays', 14);
expect(flagValue).toBe(3);
});
});
describe('when using OpenFeature with AWS SSM Provider to retrieve an object', () => {
it('should use AWS SSM in order to retrieve the value', async () => {
const res: GetParameterCommandOutput = {
Parameter: {
Name: '/lambda/env',
Value: JSON.stringify({
PROCESS_NUMBER: 3,
SOME_ENV_VAR: 4,
}),
},
$metadata: {},
};
ssmMock.on(GetParameterCommand).resolves(res);
const flagValue = await featureFlags.getObjectValue('/lambda/env', {});
expect(flagValue).toStrictEqual({
PROCESS_NUMBER: 3,
SOME_ENV_VAR: 4,
});
});
});
});

View File

@ -1,203 +0,0 @@
import type { SSMClientConfig } from '@aws-sdk/client-ssm';
import { AwsSsmProvider } from './aws-ssm-provider';
import { ErrorCode, StandardResolutionReasons } from '@openfeature/core';
const MOCK_SSM_CLIENT_CONFIG: SSMClientConfig = {
region: 'us-east-1',
credentials: {
accessKeyId: 'accessKeyId',
secretAccessKey: 'secretAccessKey',
},
};
const provider: AwsSsmProvider = new AwsSsmProvider({
ssmClientConfig: MOCK_SSM_CLIENT_CONFIG,
cacheOpts: {
enabled: true,
ttl: 1000,
size: 100,
},
});
describe(AwsSsmProvider.name, () => {
describe(AwsSsmProvider.prototype.resolveBooleanEvaluation.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when flag is cached', () => {
afterAll(() => {
provider.cache.clear();
});
it('should return cached value', async () => {
provider.cache.set('test', {
value: true,
reason: StandardResolutionReasons.STATIC,
});
await expect(provider.resolveBooleanEvaluation('test', false, {})).resolves.toEqual({
value: true,
reason: StandardResolutionReasons.CACHED,
});
});
});
describe('when flag is not cached', () => {
describe('when getBooleanValue rejects', () => {
it('should return default value', async () => {
jest.spyOn(provider.service, 'getBooleanValue').mockRejectedValue(new Error());
await expect(provider.resolveBooleanEvaluation('test', false, {})).resolves.toEqual({
value: false,
reason: StandardResolutionReasons.ERROR,
errorMessage: 'An unknown error occurred',
errorCode: ErrorCode.GENERAL,
});
});
});
describe('when getBooleanValue resolves', () => {
it('should resolve with expected value', async () => {
jest.spyOn(provider.service, 'getBooleanValue').mockResolvedValue({
value: true,
reason: StandardResolutionReasons.STATIC,
});
await expect(provider.resolveBooleanEvaluation('test', false, {})).resolves.toEqual({
value: true,
reason: StandardResolutionReasons.STATIC,
});
});
});
});
});
describe(AwsSsmProvider.prototype.resolveStringEvaluation.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when flag is cached', () => {
afterAll(() => {
provider.cache.clear();
});
it('should return cached value', async () => {
provider.cache.set('test', {
value: 'somestring',
reason: StandardResolutionReasons.STATIC,
});
await expect(provider.resolveStringEvaluation('test', 'default', {})).resolves.toEqual({
value: 'somestring',
reason: StandardResolutionReasons.CACHED,
});
});
});
describe('when flag is not cached', () => {
describe('when getStringValue rejects', () => {
it('should return default value', async () => {
jest.spyOn(provider.service, 'getStringValue').mockRejectedValue(new Error());
await expect(provider.resolveStringEvaluation('test', 'default', {})).resolves.toEqual({
value: 'default',
reason: StandardResolutionReasons.ERROR,
errorMessage: 'An unknown error occurred',
errorCode: ErrorCode.GENERAL,
});
});
});
describe('when getStringValue resolves', () => {
it('should resolve with expected value', async () => {
jest.spyOn(provider.service, 'getStringValue').mockResolvedValue({
value: 'somestring',
reason: StandardResolutionReasons.STATIC,
});
await expect(provider.resolveStringEvaluation('test', 'default', {})).resolves.toEqual({
value: 'somestring',
reason: StandardResolutionReasons.STATIC,
});
});
});
});
});
describe(AwsSsmProvider.prototype.resolveNumberEvaluation.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when flag is cached', () => {
afterAll(() => {
provider.cache.clear();
});
it('should return cached value', async () => {
provider.cache.set('test', {
value: 489,
reason: StandardResolutionReasons.STATIC,
});
await expect(provider.resolveNumberEvaluation('test', -1, {})).resolves.toEqual({
value: 489,
reason: StandardResolutionReasons.CACHED,
});
});
});
describe('when flag is not cached', () => {
describe('when getNumberValue rejects', () => {
it('should return default value', async () => {
jest.spyOn(provider.service, 'getNumberValue').mockRejectedValue(new Error());
await expect(provider.resolveNumberEvaluation('test', -1, {})).resolves.toEqual({
value: -1,
reason: StandardResolutionReasons.ERROR,
errorMessage: 'An unknown error occurred',
errorCode: ErrorCode.GENERAL,
});
});
});
describe('when getNumberValue resolves', () => {
it('should resolve with expected value', async () => {
jest.spyOn(provider.service, 'getNumberValue').mockResolvedValue({
value: 489,
reason: StandardResolutionReasons.STATIC,
});
await expect(provider.resolveNumberEvaluation('test', -1, {})).resolves.toEqual({
value: 489,
reason: StandardResolutionReasons.STATIC,
});
});
});
});
});
describe(AwsSsmProvider.prototype.resolveObjectEvaluation.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when flag is cached', () => {
afterAll(() => {
provider.cache.clear();
});
it('should return cached value', async () => {
provider.cache.set('test', {
value: { default: false },
reason: StandardResolutionReasons.STATIC,
});
await expect(provider.resolveObjectEvaluation('test', { default: true }, {})).resolves.toEqual({
value: { default: false },
reason: StandardResolutionReasons.CACHED,
});
});
});
describe('when flag is not cached', () => {
describe('when getObjectValue rejects', () => {
it('should return default value', async () => {
jest.spyOn(provider.service, 'getObjectValue').mockRejectedValue(new Error());
await expect(provider.resolveObjectEvaluation('test', { default: true }, {})).resolves.toEqual({
value: { default: true },
reason: StandardResolutionReasons.ERROR,
errorMessage: 'An unknown error occurred',
errorCode: ErrorCode.GENERAL,
});
});
});
describe('when getObjectValue resolves', () => {
it('should resolve with expected value', async () => {
jest.spyOn(provider.service, 'getObjectValue').mockResolvedValue({
value: { default: true },
reason: StandardResolutionReasons.STATIC,
});
await expect(provider.resolveObjectEvaluation('test', -1, {})).resolves.toEqual({
value: { default: true },
reason: StandardResolutionReasons.STATIC,
});
});
});
});
});
});

View File

@ -1,146 +0,0 @@
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 type { AwsSsmProviderConfig } from './types';
import { SSMService } from './ssm-service';
import { Cache } from './cache';
export class AwsSsmProvider implements Provider {
metadata = {
name: AwsSsmProvider.name,
};
readonly runsOn = 'server';
readonly service: SSMService;
hooks = [];
cache: Cache;
constructor(config: AwsSsmProviderConfig) {
this.service = new SSMService(config.ssmClientConfig, config.enableDecryption);
this.cache = new Cache(config.cacheOpts);
}
async resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext,
): Promise<ResolutionDetails<boolean>> {
const cachedValue = this.cache.get(flagKey);
if (cachedValue) {
return {
value: cachedValue.value,
reason: StandardResolutionReasons.CACHED,
};
}
try {
const res = await this.service.getBooleanValue(flagKey);
this.cache.set(flagKey, res);
return res;
} catch (err) {
let errMsg = 'An unknown error occurred';
if (err instanceof InternalServerError) {
errMsg = err.message;
}
return {
value: defaultValue,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.GENERAL,
errorMessage: errMsg,
};
}
}
async resolveStringEvaluation(
flagKey: string,
defaultValue: string,
context: EvaluationContext,
): Promise<ResolutionDetails<string>> {
const cachedValue = this.cache.get(flagKey);
if (cachedValue) {
return {
value: cachedValue.value,
reason: StandardResolutionReasons.CACHED,
};
}
try {
const res = await this.service.getStringValue(flagKey);
this.cache.set(flagKey, res);
return res;
} catch (err) {
let errMsg = 'An unknown error occurred';
if (err instanceof InternalServerError) {
errMsg = err.message;
}
return {
value: defaultValue,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.GENERAL,
errorMessage: errMsg,
};
}
}
async resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
context: EvaluationContext,
): Promise<ResolutionDetails<number>> {
const cachedValue = this.cache.get(flagKey);
if (cachedValue) {
return {
value: cachedValue.value,
reason: StandardResolutionReasons.CACHED,
};
}
try {
return await this.service.getNumberValue(flagKey);
} catch (err) {
let errMsg = 'An unknown error occurred';
if (err instanceof InternalServerError) {
errMsg = err.message;
}
return {
value: defaultValue,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.GENERAL,
errorMessage: errMsg,
};
}
}
async resolveObjectEvaluation<U extends JsonValue>(
flagKey: string,
defaultValue: U,
context: EvaluationContext,
): Promise<ResolutionDetails<U>> {
const cachedValue = this.cache.get(flagKey);
if (cachedValue) {
return {
value: cachedValue.value,
reason: StandardResolutionReasons.CACHED,
};
}
try {
return await this.service.getObjectValue(flagKey);
} catch (err) {
let errMsg = 'An unknown error occurred';
if (err instanceof InternalServerError) {
errMsg = err.message;
}
return {
value: defaultValue,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.GENERAL,
errorMessage: errMsg,
};
}
}
}

View File

@ -1,59 +0,0 @@
import { Cache } from './cache';
describe(Cache.name, () => {
describe(Cache.prototype.get.name, () => {
describe('when cache is disabled', () => {
it('should return undefined', () => {
const cache = new Cache({ enabled: false, size: 1, ttl: 1 });
expect(cache.get('test')).toBeUndefined();
});
});
describe('when cache is enabled', () => {
describe('when key is not in cache', () => {
it('should return undefined', () => {
const cache = new Cache({ enabled: true, size: 1, ttl: 1 });
expect(cache.get('test')).toBeUndefined();
});
});
describe('when key is in cache', () => {
it('should return the value', () => {
const cache = new Cache({ enabled: true, size: 1, ttl: 1 });
cache.set('test', { value: true, reason: 'test' });
expect(cache.get('test')).toEqual({ value: true, reason: 'test' });
});
});
});
});
describe(Cache.prototype.set.name, () => {
describe('when cache is disabled', () => {
it('should not set the value', () => {
const spy = jest.spyOn(Cache.prototype, 'set');
expect(spy).not.toHaveBeenCalled();
});
});
describe('when cache is enabled', () => {
it('should set the value', () => {
const cache = new Cache({ enabled: true, size: 1, ttl: 1 });
cache.set('test', { value: true, reason: 'test' });
expect(cache.get('test')).toEqual({ value: true, reason: 'test' });
});
});
});
describe(Cache.prototype.clear.name, () => {
describe('when cache is disabled', () => {
it('should not clear the cache', () => {
const spy = jest.spyOn(Cache.prototype, 'clear');
expect(spy).not.toHaveBeenCalled();
});
});
describe('when cache is enabled', () => {
it('should clear the cache', () => {
const cache = new Cache({ enabled: true, size: 1, ttl: 1 });
cache.set('test', { value: true, reason: 'test' });
cache.clear();
expect(cache.get('test')).toBeUndefined();
});
});
});
});

View File

@ -1,38 +0,0 @@
import type { ResolutionDetails } from '@openfeature/core';
import type { LRUCacheConfig } from './types';
import { LRUCache } from 'lru-cache';
export class Cache {
private cache: LRUCache<string, ResolutionDetails<any>>;
private ttl: number;
private enabled: boolean;
constructor(opts: LRUCacheConfig) {
this.cache = new LRUCache({
maxSize: opts.size ?? 1000,
sizeCalculation: () => 1,
});
this.ttl = opts.ttl ?? 300000;
this.enabled = opts.enabled;
}
get(key: string): ResolutionDetails<any> | undefined {
if (!this.enabled) {
return undefined;
}
return this.cache.get(key);
}
set(key: string, value: ResolutionDetails<any>): void {
if (!this.enabled) {
return;
}
this.cache.set(key, value, { ttl: this.ttl });
}
clear() {
if (!this.enabled) {
return;
}
this.cache.clear();
}
}

View File

@ -1,122 +0,0 @@
import { ParseError, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
import { SSMService } from './ssm-service';
describe(SSMService.name, () => {
describe(SSMService.prototype.getBooleanValue.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe(`when _getParamFromSSM returns "true"`, () => {
it(`should return a ResolutionDetails with value true`, async () => {
jest
.spyOn(SSMService.prototype, '_getValueFromSSM')
.mockResolvedValue({ val: 'true', metadata: { httpStatusCode: 200 } });
const service = new SSMService({});
const result = await service.getBooleanValue('test');
expect(result).toEqual({
value: true,
reason: StandardResolutionReasons.STATIC,
flagMetadata: { httpStatusCode: 200 },
});
});
});
describe(`when _getParamFromSSM returns "false"`, () => {
it(`should return a ResolutionDetails with value true`, async () => {
jest
.spyOn(SSMService.prototype, '_getValueFromSSM')
.mockResolvedValue({ val: 'false', metadata: { httpStatusCode: 200 } });
const service = new SSMService({});
const result = await service.getBooleanValue('test');
expect(result).toEqual({
value: false,
reason: StandardResolutionReasons.STATIC,
flagMetadata: { httpStatusCode: 200 },
});
});
});
describe(`when _getParamFromSSM returns an invalid value`, () => {
it('should throw a TypeMismatchError', () => {
jest
.spyOn(SSMService.prototype, '_getValueFromSSM')
.mockResolvedValue({ val: 'invalid boolean', metadata: { httpStatusCode: 400 } });
const service = new SSMService({});
expect(() => service.getBooleanValue('test')).rejects.toThrow(TypeMismatchError);
});
});
});
describe(SSMService.prototype.getStringValue.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe(`when _getParamFromSSM returns a valid value`, () => {
it(`should return a ResolutionDetails with that value`, async () => {
jest
.spyOn(SSMService.prototype, '_getValueFromSSM')
.mockResolvedValue({ val: 'example', metadata: { httpStatusCode: 200 } });
const service = new SSMService({});
const result = await service.getStringValue('example');
expect(result).toEqual({
value: 'example',
reason: StandardResolutionReasons.STATIC,
flagMetadata: { httpStatusCode: 200 },
});
});
});
});
describe(SSMService.prototype.getNumberValue.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe(`when _getParamFromSSM returns a valid number`, () => {
it(`should return a ResolutionDetails with value true`, async () => {
jest
.spyOn(SSMService.prototype, '_getValueFromSSM')
.mockResolvedValue({ val: '1478', metadata: { httpStatusCode: 200 } });
const service = new SSMService({});
const result = await service.getNumberValue('test');
expect(result).toEqual({
value: 1478,
reason: StandardResolutionReasons.STATIC,
flagMetadata: { httpStatusCode: 200 },
});
});
});
describe(`when _getParamFromSSM returns a value that is not a number`, () => {
it(`should return a TypeMismatchError`, async () => {
jest
.spyOn(SSMService.prototype, '_getValueFromSSM')
.mockResolvedValue({ val: 'invalid number', metadata: { httpStatusCode: 400 } });
const service = new SSMService({});
expect(() => service.getNumberValue('test')).rejects.toThrow(TypeMismatchError);
});
});
});
describe(SSMService.prototype.getObjectValue.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe(`when _getParamFromSSM returns a valid object`, () => {
it(`should return a ResolutionDetails with that object`, async () => {
jest
.spyOn(SSMService.prototype, '_getValueFromSSM')
.mockResolvedValue({ val: JSON.stringify({ test: true }), metadata: { httpStatusCode: 400 } });
const service = new SSMService({});
const result = await service.getObjectValue('test');
expect(result).toEqual({
value: { test: true },
reason: StandardResolutionReasons.STATIC,
flagMetadata: { httpStatusCode: 400 },
});
});
});
describe(`when _getParamFromSSM returns an invalid object`, () => {
it(`should return a ParseError`, async () => {
jest
.spyOn(SSMService.prototype, '_getValueFromSSM')
.mockResolvedValue({ val: 'invalid object', metadata: { httpStatusCode: 400 } });
const service = new SSMService({});
expect(() => service.getObjectValue('test')).rejects.toThrow(ParseError);
});
});
});
});

View File

@ -1,118 +0,0 @@
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;
enableDecryption: boolean;
constructor(config: SSMClientConfig, enableDecryption?: boolean) {
this.client = new SSMClient(config);
this.enableDecryption = enableDecryption ?? false;
}
async getBooleanValue(name: string): Promise<ResolutionDetails<boolean>> {
const res = await this._getValueFromSSM(name);
const { val, metadata } = res;
let result: boolean;
switch (val) {
case 'true':
result = true;
break;
case 'false':
result = false;
break;
default:
throw new TypeMismatchError(`${val} is not a valid boolean value`);
}
return {
value: result,
reason: StandardResolutionReasons.STATIC,
flagMetadata: { ...metadata },
};
}
async getStringValue(name: string): Promise<ResolutionDetails<string>> {
const res = await this._getValueFromSSM(name);
const { val, metadata } = res;
return {
value: val,
reason: StandardResolutionReasons.STATIC,
flagMetadata: { ...metadata },
};
}
async getNumberValue(name: string): Promise<ResolutionDetails<number>> {
const res = await this._getValueFromSSM(name);
const { val, metadata } = res;
if (Number.isNaN(Number(val))) {
throw new TypeMismatchError(`${val} is not a number`);
}
return {
value: Number(val),
reason: StandardResolutionReasons.STATIC,
flagMetadata: { ...metadata },
};
}
async getObjectValue<U extends JsonValue>(name: string): Promise<ResolutionDetails<U>> {
const res = await this._getValueFromSSM(name);
const { val, metadata } = res;
try {
return {
value: JSON.parse(val),
reason: StandardResolutionReasons.STATIC,
flagMetadata: { ...metadata },
};
} catch (e) {
throw new ParseError(`Unable to parse value as JSON: ${e}`);
}
}
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 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);
if (!res.Parameter) {
throw new FlagNotFoundError(`Unable to find an SSM Parameter with key ${name}`);
}
if (!res.Parameter.Value) {
throw new ParseError(`Value is empty`);
}
return { val: res.Parameter.Value, metadata: res.$metadata };
}
}

View File

@ -1,13 +0,0 @@
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;
};

View File

@ -1,22 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "ES6",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

View File

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@ -1,60 +0,0 @@
# 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)
### 🧹 Chore
* bump the required core version ([1408397](https://github.com/open-feature/js-sdk-contrib/commit/140839777b5cff8e624b23fc9eb2f8d2f4a977cb))
## [0.1.4](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-web-provider-v0.1.3...config-cat-web-provider-v0.1.4) (2025-03-04)
### 🐛 Bug Fixes
* **config-cat:** Forward default value to underlying client ([#1214](https://github.com/open-feature/js-sdk-contrib/issues/1214)) ([9d14173](https://github.com/open-feature/js-sdk-contrib/commit/9d14173cf08da3030fc58fea8786b24bafd80403))
### 🧹 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.3](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-web-provider-v0.1.2...config-cat-web-provider-v0.1.3) (2024-09-20)
### 🐛 Bug Fixes
* **config-cat-web:** Fix code examples in README.md ([#1050](https://github.com/open-feature/js-sdk-contrib/issues/1050)) ([0b6179b](https://github.com/open-feature/js-sdk-contrib/commit/0b6179b9cb16cce592be6c2fbe86dbacce5adc1f))
* **config-cat:** Revise readme ([#1054](https://github.com/open-feature/js-sdk-contrib/issues/1054)) ([7e1dd72](https://github.com/open-feature/js-sdk-contrib/commit/7e1dd72a1450a9982b340afda62d34379d1b3f16))
## [0.1.2](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-web-provider-v0.1.1...config-cat-web-provider-v0.1.2) (2024-08-22)
### 🐛 Bug Fixes
* **config-cat-web:** Update dependency configcat-js-ssr to v8.4.2 ([#1041](https://github.com/open-feature/js-sdk-contrib/issues/1041)) ([55e554d](https://github.com/open-feature/js-sdk-contrib/commit/55e554d9fc9966d7d2b364da4776c478a2ba9bb1))
## [0.1.1](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-web-provider-v0.1.0...config-cat-web-provider-v0.1.1) (2024-07-28)
### 📚 Documentation
* A few corrections to ConfigCat providers' README.md ([#1014](https://github.com/open-feature/js-sdk-contrib/issues/1014)) ([3b24653](https://github.com/open-feature/js-sdk-contrib/commit/3b24653854643c827bddccb12aeb59e61204202d))
## 0.1.0 (2024-07-21)
### ⚠ BREAKING CHANGES
* implement config cat web provider ([#918](https://github.com/open-feature/js-sdk-contrib/issues/918))
### ✨ New Features
* implement config cat web provider ([#918](https://github.com/open-feature/js-sdk-contrib/issues/918)) ([e280014](https://github.com/open-feature/js-sdk-contrib/commit/e280014f8998dd2e5f2b7700f0d24842eeafab5f))

View File

@ -1,160 +0,0 @@
# ConfigCat Web Provider
This is an OpenFeature provider implementation for using [ConfigCat](https://configcat.com), a managed feature flag service in JavaScript frontend applications.
## Installation
```
$ npm install @openfeature/config-cat-web-provider
```
#### Required peer dependencies
The OpenFeature SDK is required as peer dependency.
The minimum required version of `@openfeature/web-sdk` currently is `1.0.0`.
The minimum required version of `configcat-js-ssr` currently is `8.4.3`.
```
$ npm install @openfeature/web-sdk configcat-js-ssr
```
## Usage
The ConfigCat provider uses the [ConfigCat JavaScript SSR SDK](https://configcat.com/docs/sdk-reference/js-ssr/).
It can be created by passing the ConfigCat SDK options to ```ConfigCatWebProvider.create```.
The available options can be found in the [ConfigCat JavaScript SSR SDK](https://configcat.com/docs/sdk-reference/js-ssr/#creating-the-configcat-client).
The ConfigCat Web Provider only supports the `AutoPolling` mode because it caches all evaluation data to support synchronous evaluation of feature flags.
### Example using the default configuration
```javascript
import { OpenFeature } from "@openfeature/web-sdk";
import { ConfigCatWebProvider } from '@openfeature/config-cat-web-provider';
// Create and set the provider.
const provider = ConfigCatWebProvider.create('<sdk_key>');
await OpenFeature.setProviderAndWait(provider);
// Create a client instance to evaluate feature flags.
const client = OpenFeature.getClient();
const value = await client.getBooleanValue('isAwesomeFeatureEnabled', false);
console.log(`isAwesomeFeatureEnabled: ${value}`);
// On application shutdown, clean up the OpenFeature provider and the underlying ConfigCat client.
await OpenFeature.clearProviders();
```
### Example using custom configuration
```javascript
import { OpenFeature } from "@openfeature/web-sdk";
import { ConfigCatWebProvider } from '@openfeature/config-cat-web-provider';
import { createConsoleLogger, LogLevel } from 'configcat-js-ssr';
// Create and set the provider.
const provider = ConfigCatWebProvider.create('<sdk_key>', {
logger: createConsoleLogger(LogLevel.Info),
setupHooks: (hooks) => hooks.on('clientReady', () => console.log('Client is ready!')),
});
await OpenFeature.setProviderAndWait(provider);
// ...
```
## Evaluation Context
The OpenFeature Evaluation Context is mapped to the [ConfigCat User Object](https://configcat.com/docs/advanced/user-object/).
The [ConfigCat User Object](https://configcat.com/docs/advanced/user-object/) has three predefined attributes,
and allows for additional attributes.
The following shows how the attributes are mapped:
| OpenFeature EvaluationContext Field | ConfigCat User Field | Required |
|-------------------------------------|----------------------|----------|
| targetingKey | identifier | yes |
| email | email | no |
| country | country | no |
| _Any Other_ | custom | no |
The custom types are mapped the following way:
| OpenFeature EvaluationContext Field Type | ConfigCat User Field Type |
|------------------------------------------|---------------------------|
| string | string |
| number | number |
| boolean | string |
| Array<string> | Array<string> |
| Array | Array |
| object | string |
The following example shows the conversion between an OpenFeature Evaluation Context and the corresponding ConfigCat
User:
#### OpenFeature
```json
{
"targetingKey": "test",
"email": "email",
"country": "country",
"customString": "customString",
"customNumber": 1,
"customBoolean": true,
"customObject": {
"prop1": "1",
"prop2": 2
},
"customStringArray": [
"one",
"two"
],
"customArray": [
1,
"2",
false
]
}
```
#### ConfigCat
```json
{
"identifier": "test",
"email": "email",
"country": "country",
"custom": {
"customString": "customString",
"customBoolean": "true",
"customNumber": 1,
"customObject": "{\"prop1\":\"1\",\"prop2\":2}",
"customStringArray": [
"one",
"two"
],
"customArray": "[1,\"2\",false]"
}
}
```
## Events
The ConfigCat provider emits the
following [OpenFeature events](https://openfeature.dev/specification/types#provider-events):
- PROVIDER_READY
- PROVIDER_ERROR
- PROVIDER_CONFIGURATION_CHANGED
## Building
Run `nx package providers-config-cat-web` to build the library.
## Running unit tests
Run `nx test providers-config-cat-web` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -1,3 +0,0 @@
{
"presets": [["minify", { "builtIns": false }]]
}

View File

@ -1,10 +0,0 @@
/* eslint-disable */
export default {
displayName: 'providers-config-cat-web',
preset: '../../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/providers/config-cat',
};

View File

@ -1,156 +0,0 @@
{
"name": "@openfeature/config-cat-web-provider",
"version": "0.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openfeature/config-cat-web-provider",
"version": "0.1.6",
"peerDependencies": {
"@openfeature/web-sdk": "^1.0.0",
"configcat-js-ssr": "^8.4.3"
}
},
"node_modules/@openfeature/core": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@openfeature/core/-/core-1.2.0.tgz",
"integrity": "sha512-JyIiije5f+8Big1xz7UAmxqVmHBuFUI9Dh8DEFG2D1ocgjMm1tEzYXJDr3urCQGNnX9M/cYtNhEcGfyontIgJw==",
"peer": true
},
"node_modules/@openfeature/web-sdk": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@openfeature/web-sdk/-/web-sdk-1.1.0.tgz",
"integrity": "sha512-qfJBWVN0AzYGoZZUE4w4LrQc3Oq3MWaUys+bkBjkgyFFDJM4TrgRz+wz/f3TwRVKj2Bc0EZ0ouyfupdWjR7bsQ==",
"peer": true,
"peerDependencies": {
"@openfeature/core": "1.2.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"peer": true
},
"node_modules/axios": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/configcat-common": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-9.3.1.tgz",
"integrity": "sha512-yVkIbluksD/kZfVyKjLIOpwLrq3/ZRM7Lwrsz89JmbpQ6VtbnelrTQynSPElTtKjrPRZx56v3IZYk3nWTnnM6A==",
"peer": true,
"dependencies": {
"tslib": "^2.4.1"
}
},
"node_modules/configcat-js-ssr": {
"version": "8.4.3",
"resolved": "https://registry.npmjs.org/configcat-js-ssr/-/configcat-js-ssr-8.4.3.tgz",
"integrity": "sha512-9tNM61cgJOE9C1MO8wBK1QglrnlT8VpiAW/KgGdFdOuIPs3ky62EThgAE+HYSRYEv4JrRNB4i7G0v1Qgbf18Hw==",
"peer": true,
"dependencies": {
"axios": "^1.7.4",
"configcat-common": "9.3.1",
"tslib": "^2.4.1"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"peer": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"peer": true,
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"peer": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"peer": true
},
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"peer": true
}
}
}

View File

@ -1,14 +0,0 @@
{
"name": "@openfeature/config-cat-web-provider",
"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",
"current-version": "echo $npm_package_version"
},
"peerDependencies": {
"@openfeature/web-sdk": "^1.0.0",
"configcat-js-ssr": "^8.4.3",
"@openfeature/config-cat-core": "0.1.1"
}
}

View File

@ -1,64 +0,0 @@
{
"name": "providers-config-cat-web",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/providers/config-cat-web/src",
"projectType": "library",
"targets": {
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "npm run publish-if-not-exists",
"cwd": "dist/libs/providers/config-cat-web"
},
"dependsOn": [
{
"target": "package"
}
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/providers/config-cat-web/jest.config.ts"
}
},
"package": {
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "libs/providers/config-cat-web/package.json",
"outputPath": "dist/libs/providers/config-cat-web",
"entryFile": "libs/providers/config-cat-web/src/index.ts",
"tsConfig": "libs/providers/config-cat-web/tsconfig.lib.json",
"compiler": "tsc",
"generateExportsField": true,
"umdName": "config-cat",
"external": "all",
"format": ["cjs", "esm"],
"assets": [
{
"glob": "package.json",
"input": "./assets",
"output": "./src/"
},
{
"glob": "LICENSE",
"input": "./",
"output": "./"
},
{
"glob": "README.md",
"input": "./libs/providers/config-cat-web",
"output": "./"
}
]
}
}
},
"tags": []
}

View File

@ -1 +0,0 @@
export * from './lib/config-cat-web-provider';

View File

@ -1,191 +0,0 @@
import { ConfigCatWebProvider } from './config-cat-web-provider';
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', () => {
const targetingKey = 'abc';
let provider: ConfigCatWebProvider;
let configCatEmitter: EventEmitter<HookEvents>;
const values = {
booleanFalse: false,
booleanTrue: true,
number1: 1,
number2: 2,
stringTest: 'Test',
jsonValid: JSON.stringify({ valid: true }),
jsonInvalid: '{test:123',
jsonPrimitive: JSON.stringify(123),
};
beforeAll(async () => {
provider = ConfigCatWebProvider.create('__key__', {
logger: createConsoleLogger(LogLevel.Off),
offline: true,
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
});
await provider.initialize();
// Currently there is no option to get access to the event emitter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
configCatEmitter = (provider.configCatClient as any).options.hooks;
});
afterAll(async () => {
await provider.onClose();
});
it('should be an instance of ConfigCatWebProvider', () => {
expect(provider).toBeInstanceOf(ConfigCatWebProvider);
});
it('should dispose the configcat client on provider closing', async () => {
const newProvider = ConfigCatWebProvider.create('__another_key__', {
logger: createConsoleLogger(LogLevel.Off),
offline: true,
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
});
await newProvider.initialize();
if (!newProvider.configCatClient) {
throw Error('No ConfigCat client');
}
const clientDisposeSpy = jest.spyOn(newProvider.configCatClient, 'dispose');
await newProvider.onClose();
expect(clientDisposeSpy).toHaveBeenCalled();
});
describe('events', () => {
it('should emit PROVIDER_CONFIGURATION_CHANGED event', () => {
const handler = jest.fn();
const eventData = { settings: { myFlag: {} as ISettingUnion }, salt: undefined, segments: [] };
provider.events.addHandler(ProviderEvents.ConfigurationChanged, handler);
configCatEmitter.emit('configChanged', eventData);
expect(handler).toHaveBeenCalledWith({
flagsChanged: ['myFlag'],
});
});
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}}}}';
const fakeSharedCache = new (class implements IConfigCatCache {
private _value?: string;
get(key: string) {
return this._value;
}
set(key: string, value: string) {
this._value = value;
}
})();
const provider = ConfigCatWebProvider.create('configcat-sdk-1/1234567890123456789012/1234567890123456789012', {
cache: fakeSharedCache,
logger: createConsoleLogger(LogLevel.Off),
offline: true,
maxInitWaitTimeSeconds: 1,
});
const readyHandler = jest.fn();
provider.events.addHandler(ProviderEvents.Ready, readyHandler);
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();
});
});
describe('method resolveBooleanEvaluation', () => {
it('should throw FlagNotFoundError if type is different than expected', () => {
expect(() => provider.resolveBooleanEvaluation('nonExistent', false, { targetingKey })).toThrow(
FlagNotFoundError,
);
});
it('should return right value if key exists', () => {
const value = provider.resolveBooleanEvaluation('booleanTrue', false, { targetingKey });
expect(value).toHaveProperty('value', values.booleanTrue);
});
it('should throw TypeMismatchError if type is different than expected', () => {
expect(() => provider.resolveBooleanEvaluation('number1', false, { targetingKey })).toThrow(TypeMismatchError);
});
});
describe('method resolveStringEvaluation', () => {
it('should throw FlagNotFoundError if type is different than expected', async () => {
expect(() => provider.resolveStringEvaluation('nonExistent', 'nonExistent', { targetingKey })).toThrow(
FlagNotFoundError,
);
});
it('should return right value if key exists', () => {
const value = provider.resolveStringEvaluation('stringTest', 'default', { targetingKey });
expect(value).toHaveProperty('value', values.stringTest);
});
it('should throw TypeMismatchError if type is different than expected', async () => {
expect(() => provider.resolveStringEvaluation('number1', 'default', { targetingKey })).toThrow(TypeMismatchError);
});
});
describe('method resolveNumberEvaluation', () => {
it('should throw FlagNotFoundError if type is different than expected', async () => {
expect(() => provider.resolveNumberEvaluation('nonExistent', 0, { targetingKey })).toThrow(FlagNotFoundError);
});
it('should return right value if key exists', () => {
const value = provider.resolveNumberEvaluation('number1', 0, { targetingKey });
expect(value).toHaveProperty('value', values.number1);
});
it('should throw TypeMismatchError if type is different than expected', () => {
expect(() => provider.resolveNumberEvaluation('stringTest', 0, { targetingKey })).toThrow(TypeMismatchError);
});
});
describe('method resolveObjectEvaluation', () => {
it('should throw FlagNotFoundError if type is different than expected', () => {
expect(() => provider.resolveObjectEvaluation('nonExistent', false, { targetingKey })).toThrow(FlagNotFoundError);
});
it('should return right value if key exists', () => {
const value = provider.resolveObjectEvaluation('jsonValid', {}, { targetingKey });
expect(value).toHaveProperty('value', JSON.parse(values.jsonValid));
});
it('should throw ParseError if string is not valid JSON', () => {
expect(() => provider.resolveObjectEvaluation('jsonInvalid', {}, { targetingKey })).toThrow(ParseError);
});
it('should return right value if key exists and value is only a JSON primitive', () => {
const value = provider.resolveObjectEvaluation('jsonPrimitive', {}, { targetingKey });
expect(value).toHaveProperty('value', JSON.parse(values.jsonPrimitive));
});
});
});

View File

@ -1,167 +0,0 @@
import type { EvaluationContext, JsonValue, Paradigm, Provider, ResolutionDetails } from '@openfeature/web-sdk';
import {
OpenFeatureEventEmitter,
ParseError,
ProviderEvents,
ProviderNotReadyError,
TypeMismatchError,
} from '@openfeature/web-sdk';
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 _isProviderReady = false;
private _client?: IConfigCatClient;
public runsOn: Paradigm = 'client';
public metadata = {
name: ConfigCatWebProvider.name,
};
protected constructor(clientFactory: (provider: ConfigCatWebProvider) => IConfigCatClient) {
this._clientFactory = clientFactory;
}
public static create(sdkKey: string, options?: OptionsForPollingMode<PollingMode.AutoPoll>) {
// Let's create a shallow copy to not mess up caller's options object.
options = options ? { ...options } : {};
return new ConfigCatWebProvider((provider) => {
const oldSetupHooks = options?.setupHooks;
options.setupHooks = (hooks) => {
oldSetupHooks?.(hooks);
hooks.on('configChanged', (config: IConfig) =>
provider.events.emit(ProviderEvents.ConfigurationChanged, {
flagsChanged: Object.keys(config.settings),
}),
);
};
return getClient(sdkKey, PollingMode.AutoPoll, options);
});
}
public async initialize(): Promise<void> {
const client = this._clientFactory(this);
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() {
return this._client;
}
public async onClose(): Promise<void> {
this._client?.dispose();
}
public resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext,
): ResolutionDetails<boolean> {
return this.evaluate(flagKey, 'boolean', defaultValue, context);
}
public resolveStringEvaluation(
flagKey: string,
defaultValue: string,
context: EvaluationContext,
): ResolutionDetails<string> {
return this.evaluate(flagKey, 'string', defaultValue, context);
}
public resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
context: EvaluationContext,
): ResolutionDetails<number> {
return this.evaluate(flagKey, 'number', defaultValue, context);
}
public resolveObjectEvaluation<U extends JsonValue>(
flagKey: string,
defaultValue: U,
context: EvaluationContext,
): ResolutionDetails<U> {
const objectValue = this.evaluate(flagKey, 'object', defaultValue, context);
return objectValue as ResolutionDetails<U>;
}
protected evaluate<T extends PrimitiveTypeName>(
flagKey: string,
flagType: T,
defaultValue: PrimitiveType<T>,
context: EvaluationContext,
): ResolutionDetails<PrimitiveType<T>> {
if (!this._client) {
throw new ProviderNotReadyError('Provider is not initialized');
}
// Make sure that the user-provided `defaultValue` is compatible with `flagType` as there is
// no guarantee that it actually is. (User may bypass type checking or may not use TypeScript at all.)
if (!isType(flagType, defaultValue)) {
throw new TypeMismatchError();
}
const configCatDefaultValue = flagType !== 'object' ? (defaultValue as SettingValue) : JSON.stringify(defaultValue);
const snapshot = this._client.snapshot();
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) {
throw parseError(evaluationData.errorMessage);
}
if (flagType !== 'object') {
// When `flagType` (more precisely, `configCatDefaultValue`) is boolean, string or number,
// ConfigCat SDK guarantees that the returned `value` is compatible with `PrimitiveType<T>`.
// See also: https://configcat.com/docs/sdk-reference/js-ssr/#setting-type-mapping
return toResolutionDetails(value as PrimitiveType<T>, evaluationData);
}
let json: JsonValue;
try {
// In this case we can be sure that `value` is string since `configCatDefaultValue` is string,
// which means that ConfigCat SDK is guaranteed to return a string value.
json = JSON.parse(value as string);
} catch (e) {
throw new ParseError(`Unable to parse "${value}" as JSON`);
}
return toResolutionDetails(json as PrimitiveType<T>, evaluationData);
}
}

View File

@ -1,21 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "ES6",
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

View File

@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@ -1,18 +0,0 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -1,158 +0,0 @@
# 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)
### 🧹 Chore
* bump the required core version ([d8fc42f](https://github.com/open-feature/js-sdk-contrib/commit/d8fc42f5d23f30f011a697610e65d83144c19fca))
## [0.7.3](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.7.2...config-cat-provider-v0.7.3) (2025-03-04)
### 🐛 Bug Fixes
* **config-cat:** Forward default value to underlying client ([#1214](https://github.com/open-feature/js-sdk-contrib/issues/1214)) ([9d14173](https://github.com/open-feature/js-sdk-contrib/commit/9d14173cf08da3030fc58fea8786b24bafd80403))
### 🧹 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.7.2](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.7.1...config-cat-provider-v0.7.2) (2024-09-20)
### 🐛 Bug Fixes
* **config-cat:** Revise readme ([#1054](https://github.com/open-feature/js-sdk-contrib/issues/1054)) ([7e1dd72](https://github.com/open-feature/js-sdk-contrib/commit/7e1dd72a1450a9982b340afda62d34379d1b3f16))
## [0.7.1](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.7.0...config-cat-provider-v0.7.1) (2024-07-23)
### 📚 Documentation
* A few corrections to ConfigCat providers' README.md ([#1014](https://github.com/open-feature/js-sdk-contrib/issues/1014)) ([3b24653](https://github.com/open-feature/js-sdk-contrib/commit/3b24653854643c827bddccb12aeb59e61204202d))
## [0.7.0](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.6.1...config-cat-provider-v0.7.0) (2024-07-21)
### ⚠ BREAKING CHANGES
* make interface similar to config-cat-web ([#918](https://github.com/open-feature/js-sdk-contrib/issues/918))
### ✨ New Features
* make interface similar to config-cat-web ([#918](https://github.com/open-feature/js-sdk-contrib/issues/918)) ([e280014](https://github.com/open-feature/js-sdk-contrib/commit/e280014f8998dd2e5f2b7700f0d24842eeafab5f))
## [0.6.1](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.6.0...config-cat-provider-v0.6.1) (2024-04-05)
### 🐛 Bug Fixes
* **config-cat:** remove event emitter type import from config-cat ([#830](https://github.com/open-feature/js-sdk-contrib/issues/830)) ([1c76f63](https://github.com/open-feature/js-sdk-contrib/commit/1c76f63db0c8992325ac645d308e90337529e049))
### 🧹 Chore
* Lint Fix Providers ([#837](https://github.com/open-feature/js-sdk-contrib/issues/837)) ([8c6c46b](https://github.com/open-feature/js-sdk-contrib/commit/8c6c46b5f8f72c5a292af7e5ff8ad8d710982554))
* lock configcat dep ([#827](https://github.com/open-feature/js-sdk-contrib/issues/827)) ([28f25a2](https://github.com/open-feature/js-sdk-contrib/commit/28f25a25cfc6ba3262472c7bad061ae3b256aba3))
## [0.6.0](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.5.0...config-cat-provider-v0.6.0) (2024-03-16)
### ⚠ BREAKING CHANGES
* update OpenFeature SDK peer ([#798](https://github.com/open-feature/js-sdk-contrib/issues/798))
### ✨ New Features
* update OpenFeature SDK peer ([#798](https://github.com/open-feature/js-sdk-contrib/issues/798)) ([ebd16b9](https://github.com/open-feature/js-sdk-contrib/commit/ebd16b9630bcc6b253a7061a144e8d476cd8b586))
## [0.5.0](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.4.0...config-cat-provider-v0.5.0) (2024-03-14)
### ⚠ BREAKING CHANGES
* Allow ConfigCat provider to be used in server applications ([#796](https://github.com/open-feature/js-sdk-contrib/issues/796))
### 🐛 Bug Fixes
* Allow ConfigCat provider to be used in server applications ([#796](https://github.com/open-feature/js-sdk-contrib/issues/796)) ([190946f](https://github.com/open-feature/js-sdk-contrib/commit/190946ff83ede64f513d43a1791cc4dc274b0d37))
* **deps:** update dependency configcat-js to v9 ([#664](https://github.com/open-feature/js-sdk-contrib/issues/664)) ([6fdf552](https://github.com/open-feature/js-sdk-contrib/commit/6fdf55256cc3238fdeb9bd2bf0cde0bf494a78f9))
### 🧹 Chore
* address lint issues ([#642](https://github.com/open-feature/js-sdk-contrib/issues/642)) ([bbd9aee](https://github.com/open-feature/js-sdk-contrib/commit/bbd9aee896dc4a0817f379b799a1b8d331ee76c6))
* fix lint issues and bump server sdk version ([#715](https://github.com/open-feature/js-sdk-contrib/issues/715)) ([bd57177](https://github.com/open-feature/js-sdk-contrib/commit/bd571770f3a1a01bd62663dc3473273449f96c5c))
## [0.4.0](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.3.0...config-cat-provider-v0.4.0) (2023-10-11)
### ⚠ BREAKING CHANGES
* use @openfeature/server-sdk peer ([#608](https://github.com/open-feature/js-sdk-contrib/issues/608))
### 🐛 Bug Fixes
* packaging issues impacting babel/react ([#596](https://github.com/open-feature/js-sdk-contrib/issues/596)) ([0446eab](https://github.com/open-feature/js-sdk-contrib/commit/0446eab5cf9b45ce7de251b4f5feb8df1d499b9d))
### 🧹 Chore
* update nx, run migrations ([#552](https://github.com/open-feature/js-sdk-contrib/issues/552)) ([a88d8fc](https://github.com/open-feature/js-sdk-contrib/commit/a88d8fc097789fd7f56011e6ebb66070f52c6e56))
* use @openfeature/server-sdk peer ([#608](https://github.com/open-feature/js-sdk-contrib/issues/608)) ([ae3732a](https://github.com/open-feature/js-sdk-contrib/commit/ae3732a9068f684517db28ea1ae27b29a35e6b16))
## [0.3.0](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.2.0...config-cat-provider-v0.3.0) (2023-07-27)
### ⚠ BREAKING CHANGES
* remove configcat 7 peer dependency ([#435](https://github.com/open-feature/js-sdk-contrib/issues/435))
### 🐛 Bug Fixes
* **config-cat:** add status flag to provider ([#491](https://github.com/open-feature/js-sdk-contrib/issues/491)) ([f599c31](https://github.com/open-feature/js-sdk-contrib/commit/f599c3145881b81107c9a65b2c4cfe2a8b4111f1))
* remove configcat 7 peer dependency ([#435](https://github.com/open-feature/js-sdk-contrib/issues/435)) ([da5d212](https://github.com/open-feature/js-sdk-contrib/commit/da5d21208e8929f7cdfc805e256cb892968bcd95))
## [0.2.0](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.1.1...config-cat-provider-v0.2.0) (2023-06-27)
### ⚠ BREAKING CHANGES
* implement events and shutdown for spec 0.6.0 ([#422](https://github.com/open-feature/js-sdk-contrib/issues/422))
### 🧹 Chore
* migrate to nx 16 ([#366](https://github.com/open-feature/js-sdk-contrib/issues/366)) ([7a9c201](https://github.com/open-feature/js-sdk-contrib/commit/7a9c201d16fd7f070a1bcd2e359487ba6e7b78d7))
### 🐛 Bug Fixes
* **deps:** update dependency configcat-js to v8 ([#415](https://github.com/open-feature/js-sdk-contrib/issues/415)) ([64c7d3a](https://github.com/open-feature/js-sdk-contrib/commit/64c7d3a6c89cc4bce2e445869a4ee421e93f990b))
### ✨ New Features
* implement events and shutdown for spec 0.6.0 ([#422](https://github.com/open-feature/js-sdk-contrib/issues/422)) ([3db6927](https://github.com/open-feature/js-sdk-contrib/commit/3db6927416f1841ff836452935fc6f89634239e3))
## [0.1.1](https://github.com/open-feature/js-sdk-contrib/compare/config-cat-provider-v0.1.0...config-cat-provider-v0.1.1) (2023-04-25)
### ✨ New Features
* add ConfigCat provider [#327](https://github.com/open-feature/js-sdk-contrib/issues/327) ([#334](https://github.com/open-feature/js-sdk-contrib/issues/334)) ([495bf69](https://github.com/open-feature/js-sdk-contrib/commit/495bf690b7d83d429622cfcc554ece2b6fb9a34e))

View File

@ -1,153 +0,0 @@
# ConfigCat Provider
This is an OpenFeature provider implementation for using [ConfigCat](https://configcat.com), a managed feature flag service in Node.js applications.
## Installation
```
$ npm install @openfeature/config-cat-provider
```
#### Required peer dependencies
The OpenFeature SDK is required as peer dependency.
The minimum required version of `@openfeature/server-sdk` currently is `1.13.5`.
The minimum required version of `configcat-node` currently is `11.3.1`.
```
$ npm install @openfeature/server-sdk configcat-node
```
## Usage
The ConfigCat provider uses the [ConfigCat Node.js SDK](https://configcat.com/docs/sdk-reference/node/).
It can be created by passing the ConfigCat SDK options to ```ConfigCatProvider.create```.
The available options can be found in the [ConfigCat Node.js SDK](https://configcat.com/docs/sdk-reference/node/#creating-the-configcat-client).
### Example using the default configuration
```javascript
import { OpenFeature } from "@openfeature/server-sdk";
import { ConfigCatProvider } from '@openfeature/config-cat-provider';
// Create and set the provider.
const provider = ConfigCatProvider.create('<sdk_key>');
await OpenFeature.setProviderAndWait(provider);
// Obtain a client instance and evaluate feature flags.
const client = OpenFeature.getClient();
const value = await client.getBooleanValue('isAwesomeFeatureEnabled', false);
console.log(`isAwesomeFeatureEnabled: ${value}`);
// On application shutdown, clean up the OpenFeature provider and the underlying ConfigCat client.
await OpenFeature.clearProviders();
```
### Example using a different polling mode and custom configuration
```javascript
import { OpenFeature } from "@openfeature/server-sdk";
import { ConfigCatProvider } from '@openfeature/config-cat-provider';
import { createConsoleLogger, LogLevel, PollingMode } from 'configcat-node';
// Create and set the provider.
const provider = ConfigCatProvider.create('<sdk_key>', PollingMode.LazyLoad, {
logger: createConsoleLogger(LogLevel.Info),
setupHooks: (hooks) => hooks.on('clientReady', () => console.log('Client is ready!')),
});
await OpenFeature.setProviderAndWait(provider);
// ...
```
## Evaluation Context
The OpenFeature Evaluation Context is mapped to the [ConfigCat User Object](https://configcat.com/docs/advanced/user-object/).
The [ConfigCat User Object](https://configcat.com/docs/advanced/user-object/) has three predefined attributes,
and allows for additional attributes.
The following shows how the attributes are mapped:
| OpenFeature EvaluationContext Field | ConfigCat User Field | Required |
|-------------------------------------|----------------------|----------|
| targetingKey | identifier | yes |
| email | email | no |
| country | country | no |
| _Any Other_ | custom | no |
The custom types are mapped the following way:
| OpenFeature EvaluationContext Field Type | ConfigCat User Field Type |
|-----------------------------------------------|---------------------------|
| string | string |
| number | number |
| boolean | string |
| Array<string> | Array<string> |
| Array | Array |
| object | string |
The following example shows the conversion between an OpenFeature Evaluation Context and the corresponding ConfigCat
User:
#### OpenFeature
```json
{
"targetingKey": "test",
"email": "email",
"country": "country",
"customString": "customString",
"customNumber": 1,
"customBoolean": true,
"customObject": {
"prop1": "1",
"prop2": 2
},
"customStringArray": ["one", "two"],
"customArray": [
1,
"2",
false
]
}
```
#### ConfigCat
```json
{
"identifier": "test",
"email": "email",
"country": "country",
"custom": {
"customString": "customString",
"customBoolean": "true",
"customNumber": 1,
"customObject": "{\"prop1\":\"1\",\"prop2\":2}",
"customStringArray": ["one", "two"],
"customArray": "[1,\"2\",false]"
}
}
```
## Events
The ConfigCat provider emits the
following [OpenFeature events](https://openfeature.dev/specification/types#provider-events):
- PROVIDER_READY
- PROVIDER_ERROR
- PROVIDER_CONFIGURATION_CHANGED
## Building
Run `nx package providers-config-cat` to build the library.
## Running unit tests
Run `nx test providers-config-cat` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -1,3 +0,0 @@
{
"presets": [["minify", { "builtIns": false }]]
}

View File

@ -1,10 +0,0 @@
/* eslint-disable */
export default {
displayName: 'providers-config-cat',
preset: '../../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/providers/config-cat',
};

View File

@ -1,72 +0,0 @@
{
"name": "@openfeature/config-cat-provider",
"version": "0.7.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openfeature/config-cat-provider",
"version": "0.7.6",
"peerDependencies": {
"@openfeature/server-sdk": "^1.13.5",
"configcat-node": "^11.3.1"
}
},
"node_modules/@openfeature/core": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@openfeature/core/-/core-1.2.0.tgz",
"integrity": "sha512-JyIiije5f+8Big1xz7UAmxqVmHBuFUI9Dh8DEFG2D1ocgjMm1tEzYXJDr3urCQGNnX9M/cYtNhEcGfyontIgJw==",
"peer": true
},
"node_modules/@openfeature/server-sdk": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@openfeature/server-sdk/-/server-sdk-1.14.0.tgz",
"integrity": "sha512-PGPI6OZdyAy2FZVUiH1suw/WuWZJsIlK2xd1KbRl5rlMLawYk2bKGBGgZYX9rcozsGKOZM6/vaFjCSB6QCjCfw==",
"peer": true,
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@openfeature/core": "1.2.0"
}
},
"node_modules/configcat-common": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-9.3.1.tgz",
"integrity": "sha512-yVkIbluksD/kZfVyKjLIOpwLrq3/ZRM7Lwrsz89JmbpQ6VtbnelrTQynSPElTtKjrPRZx56v3IZYk3nWTnnM6A==",
"peer": true,
"dependencies": {
"tslib": "^2.4.1"
}
},
"node_modules/configcat-node": {
"version": "11.3.1",
"resolved": "https://registry.npmjs.org/configcat-node/-/configcat-node-11.3.1.tgz",
"integrity": "sha512-7XJbgBpcxlwzlRLmvCtHTkO247Ban2ZkBqlmk+T0wVEt5tXfltgd53SYLYpw7RBWX0ma/QyP5E+/k/UDdMrOCw==",
"peer": true,
"dependencies": {
"configcat-common": "9.3.1",
"tslib": "^2.4.1",
"tunnel": "0.0.6"
},
"engines": {
"node": ">=14"
}
},
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"peer": true
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"peer": true,
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
}
}
}

View File

@ -1,15 +0,0 @@
{
"name": "@openfeature/config-cat-provider",
"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",
"current-version": "echo $npm_package_version"
},
"peerDependencies": {
"@openfeature/server-sdk": "^1.13.5",
"configcat-node": "^11.3.1",
"@openfeature/config-cat-core": "0.1.1",
"configcat-common": "9.4.0"
}
}

View File

@ -1,64 +0,0 @@
{
"name": "providers-config-cat",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/providers/config-cat/src",
"projectType": "library",
"targets": {
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "npm run publish-if-not-exists",
"cwd": "dist/libs/providers/config-cat"
},
"dependsOn": [
{
"target": "package"
}
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/providers/config-cat/jest.config.ts"
}
},
"package": {
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "libs/providers/config-cat/package.json",
"outputPath": "dist/libs/providers/config-cat",
"entryFile": "libs/providers/config-cat/src/index.ts",
"tsConfig": "libs/providers/config-cat/tsconfig.lib.json",
"compiler": "tsc",
"generateExportsField": true,
"umdName": "config-cat",
"external": "all",
"format": ["cjs", "esm"],
"assets": [
{
"glob": "package.json",
"input": "./assets",
"output": "./src/"
},
{
"glob": "LICENSE",
"input": "./",
"output": "./"
},
{
"glob": "README.md",
"input": "./libs/providers/config-cat",
"output": "./"
}
]
}
}
},
"tags": []
}

View File

@ -1 +0,0 @@
export * from './lib/config-cat-provider';

View File

@ -1,211 +0,0 @@
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,
LogLevel,
OverrideBehaviour,
PollingMode,
} from 'configcat-js-ssr';
import type { EventEmitter } from 'events';
describe('ConfigCatProvider', () => {
const targetingKey = 'abc';
let provider: ConfigCatProvider;
let configCatEmitter: EventEmitter<HookEvents>;
const values = {
booleanFalse: false,
booleanTrue: true,
number1: 1,
number2: 2,
stringTest: 'Test',
jsonValid: JSON.stringify({ valid: true }),
jsonInvalid: '{test:123',
jsonPrimitive: JSON.stringify(123),
};
beforeAll(async () => {
provider = ConfigCatProvider.create('__key__', PollingMode.ManualPoll, {
logger: createConsoleLogger(LogLevel.Off),
offline: true,
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
});
await provider.initialize();
// Currently there is no option to get access to the event emitter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
configCatEmitter = (provider.configCatClient as any).options.hooks;
});
afterAll(async () => {
await provider.onClose();
});
it('should be an instance of ConfigCatProvider', () => {
expect(provider).toBeInstanceOf(ConfigCatProvider);
});
it('should dispose the configcat client on provider closing', async () => {
const newProvider = ConfigCatProvider.create('__another_key__', PollingMode.AutoPoll, {
logger: createConsoleLogger(LogLevel.Off),
offline: true,
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
});
await newProvider.initialize();
if (!newProvider.configCatClient) {
throw Error('No ConfigCat client');
}
const clientDisposeSpy = jest.spyOn(newProvider.configCatClient, 'dispose');
await newProvider.onClose();
expect(clientDisposeSpy).toHaveBeenCalled();
});
describe('events', () => {
it('should emit PROVIDER_CONFIGURATION_CHANGED event', () => {
const handler = jest.fn();
const eventData = { settings: { myFlag: {} as ISettingUnion }, salt: undefined, segments: [] };
provider.events.addHandler(ProviderEvents.ConfigurationChanged, handler);
configCatEmitter.emit('configChanged', eventData);
expect(handler).toHaveBeenCalledWith({
flagsChanged: ['myFlag'],
});
});
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}}}}';
const fakeSharedCache = new (class implements IConfigCatCache {
private _value?: string;
get(key: string) {
return this._value;
}
set(key: string, value: string) {
this._value = value;
}
})();
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);
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();
});
});
describe('method resolveBooleanEvaluation', () => {
it('should throw FlagNotFoundError if type is different than expected', async () => {
await expect(provider.resolveBooleanEvaluation('nonExistent', false, { targetingKey })).rejects.toThrow(
FlagNotFoundError,
);
});
it('should return right value if key exists', async () => {
const value = await provider.resolveBooleanEvaluation('booleanTrue', false, { targetingKey });
expect(value).toHaveProperty('value', values.booleanTrue);
});
it('should throw TypeMismatchError if type is different than expected', async () => {
await expect(provider.resolveBooleanEvaluation('number1', false, { targetingKey })).rejects.toThrow(
TypeMismatchError,
);
});
});
describe('method resolveStringEvaluation', () => {
it('should throw FlagNotFoundError if type is different than expected', async () => {
await expect(provider.resolveStringEvaluation('nonExistent', 'nonExistent', { targetingKey })).rejects.toThrow(
FlagNotFoundError,
);
});
it('should return right value if key exists', async () => {
const value = await provider.resolveStringEvaluation('stringTest', 'default', { targetingKey });
expect(value).toHaveProperty('value', values.stringTest);
});
it('should throw TypeMismatchError if type is different than expected', async () => {
await expect(provider.resolveStringEvaluation('number1', 'default', { targetingKey })).rejects.toThrow(
TypeMismatchError,
);
});
});
describe('method resolveNumberEvaluation', () => {
it('should throw FlagNotFoundError if type is different than expected', async () => {
await expect(provider.resolveNumberEvaluation('nonExistent', 0, { targetingKey })).rejects.toThrow(
FlagNotFoundError,
);
});
it('should return right value if key exists', async () => {
const value = await provider.resolveNumberEvaluation('number1', 0, { targetingKey });
expect(value).toHaveProperty('value', values.number1);
});
it('should throw TypeMismatchError if type is different than expected', async () => {
await expect(provider.resolveNumberEvaluation('stringTest', 0, { targetingKey })).rejects.toThrow(
TypeMismatchError,
);
});
});
describe('method resolveObjectEvaluation', () => {
it('should throw FlagNotFoundError if type is different than expected', async () => {
await expect(provider.resolveObjectEvaluation('nonExistent', false, { targetingKey })).rejects.toThrow(
FlagNotFoundError,
);
});
it('should return right value if key exists', async () => {
const value = await provider.resolveObjectEvaluation('jsonValid', {}, { targetingKey });
expect(value).toHaveProperty('value', JSON.parse(values.jsonValid));
});
it('should throw ParseError if string is not valid JSON', async () => {
await expect(provider.resolveObjectEvaluation('jsonInvalid', {}, { targetingKey })).rejects.toThrow(ParseError);
});
it('should return right value if key exists and value is only a JSON primitive', async () => {
const value = await provider.resolveObjectEvaluation('jsonPrimitive', {}, { targetingKey });
expect(value).toHaveProperty('value', JSON.parse(values.jsonPrimitive));
});
});
});

View File

@ -1,172 +0,0 @@
import type { EvaluationContext, JsonValue, Provider, ResolutionDetails, Paradigm } from '@openfeature/server-sdk';
import {
OpenFeatureEventEmitter,
ProviderEvents,
ProviderNotReadyError,
TypeMismatchError,
ParseError,
} from '@openfeature/server-sdk';
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 readonly _pollingMode: PollingMode;
private _isProviderReady = false;
private _client?: IConfigCatClient;
public runsOn: Paradigm = 'server';
public metadata = {
name: ConfigCatProvider.name,
};
protected constructor(clientFactory: (provider: ConfigCatProvider) => IConfigCatClient, pollingMode: PollingMode) {
this._clientFactory = clientFactory;
this._pollingMode = pollingMode;
}
public static create<TMode extends PollingMode>(
sdkKey: string,
pollingMode?: TMode,
options?: OptionsForPollingMode<TMode>,
): ConfigCatProvider {
// Let's create a shallow copy to not mess up caller's options object.
options = options ? { ...options } : ({} as OptionsForPollingMode<TMode>);
return new ConfigCatProvider((provider) => {
const oldSetupHooks = options?.setupHooks;
options.setupHooks = (hooks) => {
oldSetupHooks?.(hooks);
hooks.on('configChanged', (config: IConfig) =>
provider.events.emit(ProviderEvents.ConfigurationChanged, {
flagsChanged: Object.keys(config.settings),
}),
);
};
return getClient(sdkKey, pollingMode, options);
}, pollingMode ?? PollingMode.AutoPoll);
}
public async initialize(): Promise<void> {
const client = this._clientFactory(this);
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() {
return this._client;
}
public async onClose(): Promise<void> {
this._client?.dispose();
}
async resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext,
): Promise<ResolutionDetails<boolean>> {
return this.evaluate(flagKey, 'boolean', defaultValue, context);
}
public async resolveStringEvaluation(
flagKey: string,
defaultValue: string,
context: EvaluationContext,
): Promise<ResolutionDetails<string>> {
return this.evaluate(flagKey, 'string', defaultValue, context);
}
public async resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
context: EvaluationContext,
): Promise<ResolutionDetails<number>> {
return this.evaluate(flagKey, 'number', defaultValue, context);
}
public async resolveObjectEvaluation<U extends JsonValue>(
flagKey: string,
defaultValue: U,
context: EvaluationContext,
): Promise<ResolutionDetails<U>> {
const objectValue = await this.evaluate(flagKey, 'object', defaultValue, context);
return objectValue as ResolutionDetails<U>;
}
protected async evaluate<T extends PrimitiveTypeName>(
flagKey: string,
flagType: T,
defaultValue: PrimitiveType<T>,
context: EvaluationContext,
): Promise<ResolutionDetails<PrimitiveType<T>>> {
if (!this._client) {
throw new ProviderNotReadyError('Provider is not initialized');
}
// Make sure that the user-provided `defaultValue` is compatible with `flagType` as there is
// no guarantee that it actually is. (User may bypass type checking or may not use TypeScript at all.)
if (!isType(flagType, defaultValue)) {
throw new TypeMismatchError();
}
const configCatDefaultValue = flagType !== 'object' ? (defaultValue as SettingValue) : JSON.stringify(defaultValue);
const { value, ...evaluationData } = await this._client.getValueDetailsAsync(
flagKey,
configCatDefaultValue,
transformContext(context),
);
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) {
throw parseError(evaluationData.errorMessage);
}
if (flagType !== 'object') {
// When `flagType` (more precisely, `configCatDefaultValue`) is boolean, string or number,
// ConfigCat SDK guarantees that the returned `value` is compatible with `PrimitiveType<T>`.
// See also: https://configcat.com/docs/sdk-reference/node/#setting-type-mapping
return toResolutionDetails(value as PrimitiveType<T>, evaluationData);
}
let json: JsonValue;
try {
// In this case we can be sure that `value` is string since `configCatDefaultValue` is string,
// which means that ConfigCat SDK is guaranteed to return a string value.
json = JSON.parse(value as string);
} catch (e) {
throw new ParseError(`Unable to parse "${value}" as JSON`);
}
return toResolutionDetails(json as PrimitiveType<T>, evaluationData);
}
}

View File

@ -1,21 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "ES6",
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

View File

@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@ -1,18 +0,0 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -1,51 +0,0 @@
# Changelog
## [0.3.1](https://github.com/open-feature/js-sdk-contrib/compare/env-var-provider-v0.3.0...env-var-provider-v0.3.1) (2024-07-10)
### 🐛 Bug Fixes
* **env-var:** set runs on property to server ([#981](https://github.com/open-feature/js-sdk-contrib/issues/981)) ([919761d](https://github.com/open-feature/js-sdk-contrib/commit/919761d8926fc102c84b11288d4c6d1ff3e3fc05))
## [0.3.0](https://github.com/open-feature/js-sdk-contrib/compare/env-var-provider-v0.2.0...env-var-provider-v0.3.0) (2024-03-25)
### ⚠ BREAKING CHANGES
* update OpenFeature SDK peer ([#798](https://github.com/open-feature/js-sdk-contrib/issues/798))
### ✨ New Features
* update OpenFeature SDK peer ([#798](https://github.com/open-feature/js-sdk-contrib/issues/798)) ([ebd16b9](https://github.com/open-feature/js-sdk-contrib/commit/ebd16b9630bcc6b253a7061a144e8d476cd8b586))
### 🧹 Chore
* address lint issues ([#642](https://github.com/open-feature/js-sdk-contrib/issues/642)) ([bbd9aee](https://github.com/open-feature/js-sdk-contrib/commit/bbd9aee896dc4a0817f379b799a1b8d331ee76c6))
* fix lint issues and bump server sdk version ([#715](https://github.com/open-feature/js-sdk-contrib/issues/715)) ([bd57177](https://github.com/open-feature/js-sdk-contrib/commit/bd571770f3a1a01bd62663dc3473273449f96c5c))
## [0.2.0](https://github.com/open-feature/js-sdk-contrib/compare/env-var-provider-v0.1.1...env-var-provider-v0.2.0) (2023-10-11)
### ⚠ BREAKING CHANGES
* use @openfeature/server-sdk peer ([#608](https://github.com/open-feature/js-sdk-contrib/issues/608))
### 🐛 Bug Fixes
* packaging issues impacting babel/react ([#596](https://github.com/open-feature/js-sdk-contrib/issues/596)) ([0446eab](https://github.com/open-feature/js-sdk-contrib/commit/0446eab5cf9b45ce7de251b4f5feb8df1d499b9d))
### 🧹 Chore
* correct publish executor ([#378](https://github.com/open-feature/js-sdk-contrib/issues/378)) ([395ed18](https://github.com/open-feature/js-sdk-contrib/commit/395ed186de8811ae249f087821fdbdf8899c19f2))
* migrate to nx 16 ([#366](https://github.com/open-feature/js-sdk-contrib/issues/366)) ([7a9c201](https://github.com/open-feature/js-sdk-contrib/commit/7a9c201d16fd7f070a1bcd2e359487ba6e7b78d7))
* update nx, run migrations ([#552](https://github.com/open-feature/js-sdk-contrib/issues/552)) ([a88d8fc](https://github.com/open-feature/js-sdk-contrib/commit/a88d8fc097789fd7f56011e6ebb66070f52c6e56))
* use @openfeature/server-sdk peer ([#608](https://github.com/open-feature/js-sdk-contrib/issues/608)) ([ae3732a](https://github.com/open-feature/js-sdk-contrib/commit/ae3732a9068f684517db28ea1ae27b29a35e6b16))
## [0.1.1](https://github.com/open-feature/js-sdk-contrib/compare/env-var-provider-v0.1.0...env-var-provider-v0.1.1) (2023-03-02)
### Features
* initial environment variable provider ([#239](https://github.com/open-feature/js-sdk-contrib/issues/239)) ([4c5e06f](https://github.com/open-feature/js-sdk-contrib/commit/4c5e06f1b6b13b85096c424e0d52bc182f28bc33))

View File

@ -1,96 +0,0 @@
# Environment Variable Provider
The environment variable provider is a great way to start using OpenFeature.
It doesn't require any infrastructure to setup or manage, and provides a simple way to gain experience with the core concepts of feature flagging.
However, it doesn't support features such as dynamic updates at run-time or contextual flag evaluation.
That's where feature flags become extremely powerful!
Thankfully, the OpenFeature SDK supports basic providers such at this one, while making it simple to switch to a more powerful system when the time is right.
## Installation
```
$ npm install @openfeature/env-var-provider
```
Required peer dependencies
```
$ npm install @openfeature/server-sdk
```
## Usage
The environment variable provider uses environment variables to determine the value of a feature flag.
It supports `booleans`, `strings`, `numbers` and `objects` by attempting to interpret the value of an environment variable to the requested type.
The default value will be returned if the environment variable doesn't exist or the value can't be cast to the desired type.
```typescript
// Register the environment variable provider globally
OpenFeature.setProvider(new EnvVarProvider());
```
### Available options
| Option name | Type | Default |
| ------------------- | ------- | ------- |
| disableConstantCase | boolean | false |
## Examples
### Boolean example
```sh
# Start a hypothetical application with the ENABLE_NEW_FEATURE environment variable
ENABLE_NEW_FEATURE=true node my-app.js
```
```typescript
const client = OpenFeature.getClient();
client.getBooleanValue('enable-new-feature', false);
```
### Number example
```sh
# Start a hypothetical application with the DIFFICULTY_MULTIPLIER environment variable
DIFFICULTY_MULTIPLIER=5 node my-app.js
```
```typescript
const client = OpenFeature.getClient();
client.getNumberValue('difficulty-multiplier', 0);
```
### String example
```sh
# Start a hypothetical application with the WELCOME_MESSAGE environment variable
WELCOME_MESSAGE=yo node my-app.js
```
```typescript
const client = OpenFeature.getClient();
client.getStringValue('welcome-message', 'hi');
```
### Object example
```sh
# Start a hypothetical application with the PREFERRED_SDK environment variable
PREFERRED_SDK='{"name": "openfeature"}' node my-app.js
```
```typescript
const client = OpenFeature.getClient();
client.getObjectValue('preferred-sdk', { name: 'OpenFeature' });
```
## Development
### Building
Run `nx package providers-env-var` to build the library.
### Running unit tests
Run `nx test providers-env-var` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -1,3 +0,0 @@
{
"presets": [["minify", { "builtIns": false }]]
}

View File

@ -1,15 +0,0 @@
/* eslint-disable */
export default {
displayName: 'providers-env-var',
preset: '../../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/providers/env-var',
};

View File

@ -1,12 +0,0 @@
{
"name": "@openfeature/env-var-provider",
"version": "0.3.1",
"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",
"current-version": "echo $npm_package_version"
},
"peerDependencies": {
"@openfeature/server-sdk": "^1.13.0"
}
}

View File

@ -1,64 +0,0 @@
{
"name": "providers-env-var",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/providers/env-var/src",
"projectType": "library",
"targets": {
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "npm run publish-if-not-exists",
"cwd": "dist/libs/providers/env-var"
},
"dependsOn": [
{
"target": "package"
}
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/providers/env-var/jest.config.ts"
}
},
"package": {
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "libs/providers/env-var/package.json",
"outputPath": "dist/libs/providers/env-var",
"entryFile": "libs/providers/env-var/src/index.ts",
"tsConfig": "libs/providers/env-var/tsconfig.lib.json",
"compiler": "tsc",
"generateExportsField": true,
"umdName": "Env Var",
"external": "all",
"format": ["cjs", "esm"],
"assets": [
{
"glob": "package.json",
"input": "./assets",
"output": "./src/"
},
{
"glob": "LICENSE",
"input": "./",
"output": "./"
},
{
"glob": "README.md",
"input": "./libs/providers/env-var",
"output": "./"
}
]
}
}
},
"tags": []
}

View File

@ -1 +0,0 @@
export * from './lib/env-var-provider';

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