Compare commits
73 Commits
config-cat
...
main
Author | SHA1 | Date |
---|---|---|
|
91ba360d36 | |
|
0ff5c88135 | |
|
601e7de199 | |
|
fca83c925c | |
|
e2404a480f | |
|
6776e9735a | |
|
0a6e44302d | |
|
d6c4817948 | |
|
1a913b8dba | |
|
1b5ecc0959 | |
|
18268f8a21 | |
|
ddbde614e9 | |
|
e501475536 | |
|
ea14c71e4d | |
|
6a6d23ba32 | |
|
82fbf1e042 | |
|
375193081a | |
|
fbc5023097 | |
|
e6766c07e5 | |
|
9c0f895af5 | |
|
cf9fe09cc1 | |
|
4e1202982e | |
|
6ab7f1abfc | |
|
1ffe54af81 | |
|
ec5309a0a9 | |
|
02a671ca7c | |
|
4f30cda7a3 | |
|
141a37df58 | |
|
d3ad93e3a9 | |
|
d495ee346f | |
|
b9ff7e1448 | |
|
70518ac4c1 | |
|
6edf52e7c2 | |
|
61b46c9687 | |
|
9e8496a384 | |
|
6dd71ac73a | |
|
8cdd5b4524 | |
|
4fe9c7e232 | |
|
e83fa88790 | |
|
771dcd522b | |
|
182077cf32 | |
|
fbd9f9155d | |
|
24a7e0e3bb | |
|
c1b00f69ee | |
|
3045e0cac8 | |
|
4dbcbf1870 | |
|
8d2fd484db | |
|
a0cdef59fd | |
|
d9ffcec165 | |
|
136b1c9858 | |
|
7083655c78 | |
|
df1ec47c3f | |
|
de161ad178 | |
|
a80f5ce3d7 | |
|
afae82c1a1 | |
|
b9231018c4 | |
|
4a48caa78a | |
|
92c67281c8 | |
|
358a04f3f0 | |
|
ce6a8e1a80 | |
|
ee676d8495 | |
|
18a0cb906a | |
|
d7f8746434 | |
|
fbe9adc832 | |
|
29f8b84685 | |
|
ca98f7c6dc | |
|
241e36edf6 | |
|
e6d3e9902f | |
|
7be62ae45f | |
|
a4fa64ec24 | |
|
9323b14968 | |
|
dba2a28001 | |
|
4180281715 |
|
@ -7,6 +7,14 @@
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"disallowTypeAnnotations": true,
|
||||||
|
"fixStyle": "separate-type-imports",
|
||||||
|
"prefer": "type-imports"
|
||||||
|
}
|
||||||
|
],
|
||||||
"@nx/enforce-module-boundaries": [
|
"@nx/enforce-module-boundaries": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
@ -56,7 +64,7 @@
|
||||||
"checkObsoleteDependencies": true,
|
"checkObsoleteDependencies": true,
|
||||||
"checkVersionMismatches": true,
|
"checkVersionMismatches": true,
|
||||||
"ignoredDependencies": ["jest-cucumber", "jest"],
|
"ignoredDependencies": ["jest-cucumber", "jest"],
|
||||||
"ignoredFiles": ["**/test/**", "**/spec/**", "**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js"]
|
"ignoredFiles": ["**/test/**", "**/tests/*", "**/spec/**", "**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/jest.*"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,14 +14,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x, 20.x, 22.x]
|
node-version: [20.x, 22.x, 24.x]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
@ -43,10 +43,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
# we need 'fetch' for this test, which is only in 18+
|
node-version: 20
|
||||||
node-version: 18
|
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
|
|
|
@ -5,6 +5,7 @@ on:
|
||||||
name: Run Release Please
|
name: Run Release Please
|
||||||
jobs:
|
jobs:
|
||||||
release-please:
|
release-please:
|
||||||
|
environment: publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
# Release-please creates a PR that tracks all changes
|
# Release-please creates a PR that tracks all changes
|
||||||
|
@ -28,10 +29,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
github_token: ${{ github.token }}
|
github_token: ${{ github.token }}
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
if: ${{ steps.release.outputs.releases_created }}
|
if: ${{ steps.release.outputs.releases_created }}
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
- name: Build Packages
|
- name: Build Packages
|
||||||
if: ${{ steps.release.outputs.releases_created }}
|
if: ${{ steps.release.outputs.releases_created }}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
[submodule "libs/shared/flagd-core/test-harness"]
|
[submodule "libs/shared/flagd-core/test-harness"]
|
||||||
path = libs/shared/flagd-core/test-harness
|
path = libs/shared/flagd-core/test-harness
|
||||||
url = https://github.com/open-feature/flagd-testbed
|
url = https://github.com/open-feature/flagd-testbed
|
||||||
branch = v0.5.21
|
branch = v2.8.0
|
||||||
[submodule "libs/shared/flagd-core/spec"]
|
[submodule "libs/shared/flagd-core/spec"]
|
||||||
path = libs/shared/flagd-core/spec
|
path = libs/shared/flagd-core/spec
|
||||||
url = https://github.com/open-feature/spec
|
url = https://github.com/open-feature/spec
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
{
|
{
|
||||||
"libs/hooks/open-telemetry": "0.4.0",
|
"libs/hooks/open-telemetry": "0.4.0",
|
||||||
"libs/providers/go-feature-flag": "0.7.6",
|
"libs/providers/go-feature-flag": "0.7.8",
|
||||||
"libs/providers/flagd": "0.13.3",
|
"libs/providers/flagd": "0.13.3",
|
||||||
"libs/providers/flagd-web": "0.7.3",
|
"libs/providers/flagd-web": "0.7.3",
|
||||||
"libs/providers/env-var": "0.3.1",
|
"libs/providers/env-var": "0.3.1",
|
||||||
"libs/providers/config-cat": "0.7.5",
|
"libs/providers/config-cat": "0.7.6",
|
||||||
"libs/providers/launchdarkly-client": "0.3.2",
|
"libs/providers/launchdarkly-client": "0.3.2",
|
||||||
"libs/providers/go-feature-flag-web": "0.2.5",
|
"libs/providers/go-feature-flag-web": "0.2.6",
|
||||||
"libs/shared/flagd-core": "1.0.0",
|
"libs/shared/flagd-core": "1.1.0",
|
||||||
"libs/shared/ofrep-core": "1.0.1",
|
"libs/shared/ofrep-core": "1.0.1",
|
||||||
"libs/providers/ofrep": "0.2.1",
|
"libs/providers/ofrep": "0.2.1",
|
||||||
"libs/providers/ofrep-web": "0.3.2",
|
"libs/providers/ofrep-web": "0.3.3",
|
||||||
"libs/providers/flipt": "0.1.2",
|
"libs/providers/flipt": "0.1.3",
|
||||||
"libs/providers/flagsmith-client": "0.1.3",
|
"libs/providers/flagsmith-client": "0.1.3",
|
||||||
"libs/providers/flipt-web": "0.1.2",
|
"libs/providers/flipt-web": "0.1.5",
|
||||||
"libs/providers/multi-provider": "0.1.2",
|
"libs/providers/multi-provider": "0.1.2",
|
||||||
"libs/providers/multi-provider-web": "0.0.3",
|
"libs/providers/multi-provider-web": "0.0.3",
|
||||||
"libs/providers/growthbook-client": "0.1.2",
|
"libs/providers/growthbook-client": "0.1.2",
|
||||||
|
@ -21,5 +21,5 @@
|
||||||
"libs/shared/config-cat-core": "0.1.1",
|
"libs/shared/config-cat-core": "0.1.1",
|
||||||
"libs/providers/unleash-web": "0.1.1",
|
"libs/providers/unleash-web": "0.1.1",
|
||||||
"libs/providers/growthbook": "0.1.2",
|
"libs/providers/growthbook": "0.1.2",
|
||||||
"libs/providers/aws-ssm": "0.1.2"
|
"libs/providers/aws-ssm": "0.1.3"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@
|
||||||
#
|
#
|
||||||
# Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-javascript/workgroup.yaml
|
# Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-javascript/workgroup.yaml
|
||||||
#
|
#
|
||||||
* @open-feature/sdk-javascript-maintainers
|
* @open-feature/sdk-javascript-maintainers @open-feature/maintainers
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { BeforeHookContext, EvaluationDetails, HookContext, StandardResolutionReasons } from '@openfeature/server-sdk';
|
import type { BeforeHookContext, EvaluationDetails, HookContext } from '@openfeature/server-sdk';
|
||||||
|
import { StandardResolutionReasons } from '@openfeature/server-sdk';
|
||||||
import opentelemetry from '@opentelemetry/api';
|
import opentelemetry from '@opentelemetry/api';
|
||||||
import { DataPoint, MeterProvider, MetricReader, ScopeMetrics } from '@opentelemetry/sdk-metrics';
|
import type { DataPoint, ScopeMetrics } from '@opentelemetry/sdk-metrics';
|
||||||
|
import { MeterProvider, MetricReader } from '@opentelemetry/sdk-metrics';
|
||||||
import {
|
import {
|
||||||
ACTIVE_COUNT_NAME,
|
ACTIVE_COUNT_NAME,
|
||||||
ERROR_TOTAL_NAME,
|
ERROR_TOTAL_NAME,
|
||||||
|
@ -12,7 +14,7 @@ import {
|
||||||
VARIANT_ATTR,
|
VARIANT_ATTR,
|
||||||
} from '../conventions';
|
} from '../conventions';
|
||||||
import { MetricsHook } from './metrics-hook';
|
import { MetricsHook } from './metrics-hook';
|
||||||
import { AttributeMapper } from '../otel-hook';
|
import type { AttributeMapper } from '../otel-hook';
|
||||||
|
|
||||||
// no-op "in-memory" reader
|
// no-op "in-memory" reader
|
||||||
class InMemoryMetricReader extends MetricReader {
|
class InMemoryMetricReader extends MetricReader {
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
|
import type { BeforeHookContext, Logger } from '@openfeature/server-sdk';
|
||||||
import {
|
import {
|
||||||
BeforeHookContext,
|
|
||||||
Logger,
|
|
||||||
StandardResolutionReasons,
|
StandardResolutionReasons,
|
||||||
type EvaluationDetails,
|
type EvaluationDetails,
|
||||||
type FlagValue,
|
type FlagValue,
|
||||||
type Hook,
|
type Hook,
|
||||||
type HookContext,
|
type HookContext,
|
||||||
} from '@openfeature/server-sdk';
|
} from '@openfeature/server-sdk';
|
||||||
import { Attributes, Counter, UpDownCounter, ValueType, metrics } from '@opentelemetry/api';
|
import type { Attributes, Counter, UpDownCounter } from '@opentelemetry/api';
|
||||||
|
import { ValueType, metrics } from '@opentelemetry/api';
|
||||||
|
import type { EvaluationAttributes, ExceptionAttributes } from '../conventions';
|
||||||
import {
|
import {
|
||||||
ACTIVE_COUNT_NAME,
|
ACTIVE_COUNT_NAME,
|
||||||
ERROR_TOTAL_NAME,
|
ERROR_TOTAL_NAME,
|
||||||
EXCEPTION_ATTR,
|
EXCEPTION_ATTR,
|
||||||
EvaluationAttributes,
|
|
||||||
ExceptionAttributes,
|
|
||||||
KEY_ATTR,
|
KEY_ATTR,
|
||||||
PROVIDER_NAME_ATTR,
|
PROVIDER_NAME_ATTR,
|
||||||
REASON_ATTR,
|
REASON_ATTR,
|
||||||
|
@ -21,7 +20,8 @@ import {
|
||||||
SUCCESS_TOTAL_NAME,
|
SUCCESS_TOTAL_NAME,
|
||||||
VARIANT_ATTR,
|
VARIANT_ATTR,
|
||||||
} from '../conventions';
|
} from '../conventions';
|
||||||
import { OpenTelemetryHook, OpenTelemetryHookOptions } from '../otel-hook';
|
import type { OpenTelemetryHookOptions } from '../otel-hook';
|
||||||
|
import { OpenTelemetryHook } from '../otel-hook';
|
||||||
|
|
||||||
type ErrorEvaluationAttributes = EvaluationAttributes & ExceptionAttributes;
|
type ErrorEvaluationAttributes = EvaluationAttributes & ExceptionAttributes;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FlagMetadata, Logger } from '@openfeature/server-sdk';
|
import type { FlagMetadata, Logger } from '@openfeature/server-sdk';
|
||||||
import { Attributes } from '@opentelemetry/api';
|
import type { Attributes } from '@opentelemetry/api';
|
||||||
|
|
||||||
export type AttributeMapper = (flagMetadata: FlagMetadata) => Attributes;
|
export type AttributeMapper = (flagMetadata: FlagMetadata) => Attributes;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EvaluationDetails, HookContext } from '@openfeature/server-sdk';
|
import type { EvaluationDetails, HookContext } from '@openfeature/server-sdk';
|
||||||
|
|
||||||
const addEvent = jest.fn();
|
const addEvent = jest.fn();
|
||||||
const recordException = jest.fn();
|
const recordException = jest.fn();
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Hook, HookContext, EvaluationDetails, FlagValue, Logger } from '@openfeature/server-sdk';
|
import type { Hook, HookContext, EvaluationDetails, FlagValue, Logger } from '@openfeature/server-sdk';
|
||||||
import { trace } from '@opentelemetry/api';
|
import { trace } from '@opentelemetry/api';
|
||||||
import { FEATURE_FLAG, KEY_ATTR, PROVIDER_NAME_ATTR, VARIANT_ATTR } from '../conventions';
|
import { FEATURE_FLAG, KEY_ATTR, PROVIDER_NAME_ATTR, VARIANT_ATTR } from '../conventions';
|
||||||
import { OpenTelemetryHook, OpenTelemetryHookOptions } from '../otel-hook';
|
import type { OpenTelemetryHookOptions } from '../otel-hook';
|
||||||
|
import { OpenTelemetryHook } from '../otel-hook';
|
||||||
|
|
||||||
export type TracingHookOptions = OpenTelemetryHookOptions;
|
export type TracingHookOptions = OpenTelemetryHookOptions;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
# Changelog
|
# 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)
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/aws-ssm-provider",
|
"name": "@openfeature/aws-ssm-provider",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-ssm": "^3.759.0",
|
"@aws-sdk/client-ssm": "^3.759.0",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { OpenFeature } from '@openfeature/server-sdk';
|
import { OpenFeature } from '@openfeature/server-sdk';
|
||||||
import { AwsSsmProvider } from '../lib/aws-ssm-provider';
|
import { AwsSsmProvider } from '../lib/aws-ssm-provider';
|
||||||
import { GetParameterCommand, GetParameterCommandOutput, SSMClient } from '@aws-sdk/client-ssm';
|
import type { GetParameterCommandOutput } from '@aws-sdk/client-ssm';
|
||||||
|
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
|
||||||
import { mockClient } from 'aws-sdk-client-mock';
|
import { mockClient } from 'aws-sdk-client-mock';
|
||||||
|
|
||||||
const ssmMock = mockClient(SSMClient);
|
const ssmMock = mockClient(SSMClient);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SSMClientConfig } from '@aws-sdk/client-ssm';
|
import type { SSMClientConfig } from '@aws-sdk/client-ssm';
|
||||||
import { AwsSsmProvider } from './aws-ssm-provider';
|
import { AwsSsmProvider } from './aws-ssm-provider';
|
||||||
import { ErrorCode, StandardResolutionReasons } from '@openfeature/core';
|
import { ErrorCode, StandardResolutionReasons } from '@openfeature/core';
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import {
|
import type { EvaluationContext, Provider, JsonValue, ResolutionDetails } from '@openfeature/server-sdk';
|
||||||
EvaluationContext,
|
import { StandardResolutionReasons, ErrorCode } from '@openfeature/server-sdk';
|
||||||
Provider,
|
|
||||||
JsonValue,
|
|
||||||
ResolutionDetails,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
ErrorCode,
|
|
||||||
} from '@openfeature/server-sdk';
|
|
||||||
import { InternalServerError } from '@aws-sdk/client-ssm';
|
import { InternalServerError } from '@aws-sdk/client-ssm';
|
||||||
import { AwsSsmProviderConfig } from './types';
|
import type { AwsSsmProviderConfig } from './types';
|
||||||
import { SSMService } from './ssm-service';
|
import { SSMService } from './ssm-service';
|
||||||
import { Cache } from './cache';
|
import { Cache } from './cache';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ResolutionDetails } from '@openfeature/core';
|
import type { ResolutionDetails } from '@openfeature/core';
|
||||||
import { LRUCacheConfig } from './types';
|
import type { LRUCacheConfig } from './types';
|
||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
|
|
||||||
export class Cache {
|
export class Cache {
|
||||||
|
|
|
@ -1,19 +1,8 @@
|
||||||
import {
|
import type { SSMClientConfig, GetParameterCommandInput } from '@aws-sdk/client-ssm';
|
||||||
GetParameterCommand,
|
import { GetParameterCommand, SSMClient, DescribeParametersCommand } from '@aws-sdk/client-ssm';
|
||||||
SSMClient,
|
import type { ResponseMetadata } from '@smithy/types';
|
||||||
SSMClientConfig,
|
import type { JsonValue, ResolutionDetails } from '@openfeature/core';
|
||||||
GetParameterCommandInput,
|
import { FlagNotFoundError, TypeMismatchError, ParseError, StandardResolutionReasons } from '@openfeature/core';
|
||||||
DescribeParametersCommand,
|
|
||||||
} from '@aws-sdk/client-ssm';
|
|
||||||
import { ResponseMetadata } from '@smithy/types';
|
|
||||||
import {
|
|
||||||
FlagNotFoundError,
|
|
||||||
TypeMismatchError,
|
|
||||||
JsonValue,
|
|
||||||
ParseError,
|
|
||||||
ResolutionDetails,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
} from '@openfeature/core';
|
|
||||||
|
|
||||||
export class SSMService {
|
export class SSMService {
|
||||||
client: SSMClient;
|
client: SSMClient;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SSMClientConfig } from '@aws-sdk/client-ssm';
|
import type { SSMClientConfig } from '@aws-sdk/client-ssm';
|
||||||
|
|
||||||
export type AwsSsmProviderConfig = {
|
export type AwsSsmProviderConfig = {
|
||||||
ssmClientConfig: SSMClientConfig;
|
ssmClientConfig: SSMClientConfig;
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
import { ConfigCatWebProvider } from './config-cat-web-provider';
|
import { ConfigCatWebProvider } from './config-cat-web-provider';
|
||||||
import {
|
import type { HookEvents, IConfigCatCache, ISettingUnion } from 'configcat-js-ssr';
|
||||||
createConsoleLogger,
|
import { createConsoleLogger, createFlagOverridesFromMap, LogLevel, OverrideBehaviour } from 'configcat-js-ssr';
|
||||||
createFlagOverridesFromMap,
|
import type { EventEmitter } from 'events';
|
||||||
HookEvents,
|
|
||||||
IConfigCatCache,
|
|
||||||
ISettingUnion,
|
|
||||||
LogLevel,
|
|
||||||
OverrideBehaviour,
|
|
||||||
} from 'configcat-js-ssr';
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { ProviderEvents, ParseError, FlagNotFoundError, TypeMismatchError } from '@openfeature/web-sdk';
|
import { ProviderEvents, ParseError, FlagNotFoundError, TypeMismatchError } from '@openfeature/web-sdk';
|
||||||
|
|
||||||
describe('ConfigCatWebProvider', () => {
|
describe('ConfigCatWebProvider', () => {
|
||||||
|
|
|
@ -1,32 +1,15 @@
|
||||||
|
import type { EvaluationContext, JsonValue, Paradigm, Provider, ResolutionDetails } from '@openfeature/web-sdk';
|
||||||
import {
|
import {
|
||||||
EvaluationContext,
|
|
||||||
JsonValue,
|
|
||||||
OpenFeatureEventEmitter,
|
OpenFeatureEventEmitter,
|
||||||
Paradigm,
|
|
||||||
ParseError,
|
ParseError,
|
||||||
Provider,
|
|
||||||
ProviderEvents,
|
ProviderEvents,
|
||||||
ProviderNotReadyError,
|
ProviderNotReadyError,
|
||||||
ResolutionDetails,
|
|
||||||
TypeMismatchError,
|
TypeMismatchError,
|
||||||
} from '@openfeature/web-sdk';
|
} from '@openfeature/web-sdk';
|
||||||
import {
|
import type { PrimitiveType, PrimitiveTypeName } from '@openfeature/config-cat-core';
|
||||||
isType,
|
import { isType, parseError, toResolutionDetails, transformContext } from '@openfeature/config-cat-core';
|
||||||
parseError,
|
import type { IConfig, IConfigCatClient, OptionsForPollingMode, SettingValue } from 'configcat-js-ssr';
|
||||||
PrimitiveType,
|
import { ClientCacheState, getClient, PollingMode } from 'configcat-js-ssr';
|
||||||
PrimitiveTypeName,
|
|
||||||
toResolutionDetails,
|
|
||||||
transformContext,
|
|
||||||
} from '@openfeature/config-cat-core';
|
|
||||||
import {
|
|
||||||
ClientCacheState,
|
|
||||||
getClient,
|
|
||||||
IConfig,
|
|
||||||
IConfigCatClient,
|
|
||||||
OptionsForPollingMode,
|
|
||||||
PollingMode,
|
|
||||||
SettingValue,
|
|
||||||
} from 'configcat-js-ssr';
|
|
||||||
|
|
||||||
export class ConfigCatWebProvider implements Provider {
|
export class ConfigCatWebProvider implements Provider {
|
||||||
public readonly events = new OpenFeatureEventEmitter();
|
public readonly events = new OpenFeatureEventEmitter();
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
# Changelog
|
# 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)
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/config-cat-provider",
|
"name": "@openfeature/config-cat-provider",
|
||||||
"version": "0.7.5",
|
"version": "0.7.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@openfeature/config-cat-provider",
|
"name": "@openfeature/config-cat-provider",
|
||||||
"version": "0.7.5",
|
"version": "0.7.6",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@openfeature/server-sdk": "^1.13.5",
|
"@openfeature/server-sdk": "^1.13.5",
|
||||||
"configcat-node": "^11.3.1"
|
"configcat-node": "^11.3.1"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/config-cat-provider",
|
"name": "@openfeature/config-cat-provider",
|
||||||
"version": "0.7.5",
|
"version": "0.7.6",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"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",
|
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
|
||||||
|
@ -10,6 +10,6 @@
|
||||||
"@openfeature/server-sdk": "^1.13.5",
|
"@openfeature/server-sdk": "^1.13.5",
|
||||||
"configcat-node": "^11.3.1",
|
"configcat-node": "^11.3.1",
|
||||||
"@openfeature/config-cat-core": "0.1.1",
|
"@openfeature/config-cat-core": "0.1.1",
|
||||||
"configcat-common": "9.3.1"
|
"configcat-common": "9.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { ConfigCatProvider } from './config-cat-provider';
|
import { ConfigCatProvider } from './config-cat-provider';
|
||||||
import { ProviderEvents, ParseError, FlagNotFoundError, TypeMismatchError } from '@openfeature/web-sdk';
|
import { ProviderEvents, ParseError, FlagNotFoundError, TypeMismatchError } from '@openfeature/web-sdk';
|
||||||
|
import type { HookEvents, IConfigCatCache, ISettingUnion } from 'configcat-js-ssr';
|
||||||
import {
|
import {
|
||||||
createConsoleLogger,
|
createConsoleLogger,
|
||||||
createFlagOverridesFromMap,
|
createFlagOverridesFromMap,
|
||||||
HookEvents,
|
|
||||||
IConfigCatCache,
|
|
||||||
ISettingUnion,
|
|
||||||
LogLevel,
|
LogLevel,
|
||||||
OverrideBehaviour,
|
OverrideBehaviour,
|
||||||
PollingMode,
|
PollingMode,
|
||||||
} from 'configcat-js-ssr';
|
} from 'configcat-js-ssr';
|
||||||
import { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
|
|
||||||
describe('ConfigCatProvider', () => {
|
describe('ConfigCatProvider', () => {
|
||||||
const targetingKey = 'abc';
|
const targetingKey = 'abc';
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
|
import type { EvaluationContext, JsonValue, Provider, ResolutionDetails, Paradigm } from '@openfeature/server-sdk';
|
||||||
import {
|
import {
|
||||||
EvaluationContext,
|
|
||||||
JsonValue,
|
|
||||||
OpenFeatureEventEmitter,
|
OpenFeatureEventEmitter,
|
||||||
Provider,
|
|
||||||
ProviderEvents,
|
ProviderEvents,
|
||||||
ResolutionDetails,
|
|
||||||
Paradigm,
|
|
||||||
ProviderNotReadyError,
|
ProviderNotReadyError,
|
||||||
TypeMismatchError,
|
TypeMismatchError,
|
||||||
ParseError,
|
ParseError,
|
||||||
} from '@openfeature/server-sdk';
|
} from '@openfeature/server-sdk';
|
||||||
import {
|
import type { PrimitiveType, PrimitiveTypeName } from '@openfeature/config-cat-core';
|
||||||
isType,
|
import { isType, parseError, toResolutionDetails, transformContext } from '@openfeature/config-cat-core';
|
||||||
parseError,
|
import type { SettingValue } from 'configcat-common';
|
||||||
PrimitiveType,
|
import { ClientCacheState, PollingMode } from 'configcat-common';
|
||||||
PrimitiveTypeName,
|
import type { IConfigCatClient, IConfig, OptionsForPollingMode } from 'configcat-node';
|
||||||
toResolutionDetails,
|
import { getClient } from 'configcat-node';
|
||||||
transformContext,
|
|
||||||
} from '@openfeature/config-cat-core';
|
|
||||||
import { ClientCacheState, PollingMode, SettingValue } from 'configcat-common';
|
|
||||||
import { IConfigCatClient, getClient, IConfig, OptionsForPollingMode } from 'configcat-node';
|
|
||||||
|
|
||||||
export class ConfigCatProvider implements Provider {
|
export class ConfigCatProvider implements Provider {
|
||||||
public readonly events = new OpenFeatureEventEmitter();
|
public readonly events = new OpenFeatureEventEmitter();
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import {
|
import type { JsonValue, Provider, ResolutionDetails } from '@openfeature/server-sdk';
|
||||||
FlagNotFoundError,
|
import { FlagNotFoundError, ParseError, StandardResolutionReasons } from '@openfeature/server-sdk';
|
||||||
JsonValue,
|
|
||||||
ParseError,
|
|
||||||
Provider,
|
|
||||||
ResolutionDetails,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
} from '@openfeature/server-sdk';
|
|
||||||
import { constantCase } from './constant-case';
|
import { constantCase } from './constant-case';
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"@openfeature/web-sdk": "^1.0.0"
|
"@openfeature/web-sdk": "^1.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openfeature/flagd-core": "1.0.0",
|
"@openfeature/flagd-core": "1.1.0",
|
||||||
"@connectrpc/connect": "^1.4.0",
|
"@connectrpc/connect": "^1.4.0",
|
||||||
"@connectrpc/connect-web": "^1.4.0",
|
"@connectrpc/connect-web": "^1.4.0",
|
||||||
"@bufbuild/protobuf": "^1.2.0"
|
"@bufbuild/protobuf": "^1.2.0"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit bb763438abc5b0e97dc26a795bc72b7a9f3e4020
|
Subproject commit 2852d7772e6b8674681a6ee6b88db10dbe3f6899
|
|
@ -2,4 +2,7 @@ import { getGherkinTestPath } from '@openfeature/flagd-core';
|
||||||
|
|
||||||
export const FLAGD_NAME = 'flagd';
|
export const FLAGD_NAME = 'flagd';
|
||||||
|
|
||||||
export const GHERKIN_EVALUATION_FEATURE = getGherkinTestPath('flagd.feature');
|
export const GHERKIN_EVALUATION_FEATURE = getGherkinTestPath(
|
||||||
|
'evaluation.feature',
|
||||||
|
'spec/specification/assets/gherkin/',
|
||||||
|
);
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import { StepDefinitions } from 'jest-cucumber';
|
import type { StepDefinitions } from 'jest-cucumber';
|
||||||
import {
|
import type { EvaluationContext, EvaluationDetails, FlagValue, JsonObject } from '@openfeature/web-sdk';
|
||||||
EvaluationDetails,
|
import { OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/web-sdk';
|
||||||
FlagValue,
|
|
||||||
JsonObject,
|
|
||||||
OpenFeature,
|
|
||||||
ProviderEvents,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
} from '@openfeature/web-sdk';
|
|
||||||
import { E2E_CLIENT_NAME } from '@openfeature/flagd-core';
|
import { E2E_CLIENT_NAME } from '@openfeature/flagd-core';
|
||||||
|
|
||||||
export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then }) => {
|
export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then }) => {
|
||||||
|
@ -14,6 +8,7 @@ export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then })
|
||||||
let value: FlagValue;
|
let value: FlagValue;
|
||||||
let details: EvaluationDetails<FlagValue>;
|
let details: EvaluationDetails<FlagValue>;
|
||||||
let fallback: FlagValue;
|
let fallback: FlagValue;
|
||||||
|
let context: EvaluationContext;
|
||||||
|
|
||||||
const client = OpenFeature.getClient(E2E_CLIENT_NAME);
|
const client = OpenFeature.getClient(E2E_CLIENT_NAME);
|
||||||
|
|
||||||
|
@ -23,7 +18,7 @@ export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then })
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
given('a provider is registered', () => undefined);
|
given('a stable provider', () => undefined);
|
||||||
given('a flagd provider is set', () => undefined);
|
given('a flagd provider is set', () => undefined);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
|
@ -85,6 +80,32 @@ export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then })
|
||||||
value = client.getObjectValue(key, defaultValue);
|
value = client.getObjectValue(key, defaultValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
and(/^a flag with key "(.*)" is evaluated with default value "(.*)"$/, async (key, defaultValue) => {
|
||||||
|
await OpenFeature.setContext(context);
|
||||||
|
flagKey = key;
|
||||||
|
fallback = defaultValue;
|
||||||
|
value = client.getStringValue(flagKey, fallback as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
when(
|
||||||
|
/^context contains keys "(.*)", "(.*)", "(.*)", "(.*)" with values "(.*)", "(.*)", (\d+), "(.*)"$/,
|
||||||
|
(key0, key1, key2, key3, stringVal1, stringVal2, intVal, boolVal) => {
|
||||||
|
context = {
|
||||||
|
[key0]: stringVal1,
|
||||||
|
[key1]: stringVal2,
|
||||||
|
[key2]: Number.parseInt(intVal),
|
||||||
|
[key3]: boolVal === true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
and(/^the resolved flag value is "(.*)" when the context is empty$/, async (expectedValue) => {
|
||||||
|
context = {};
|
||||||
|
await OpenFeature.setContext(context);
|
||||||
|
value = client.getStringValue(flagKey, fallback as string);
|
||||||
|
expect(value).toEqual(expectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
then(
|
then(
|
||||||
/^the resolved object value should be contain fields "(.*)", "(.*)", and "(.*)", with values "(.*)", "(.*)" and (\d+), respectively$/,
|
/^the resolved object value should be contain fields "(.*)", "(.*)", and "(.*)", with values "(.*)", "(.*)" and (\d+), respectively$/,
|
||||||
(field1: string, field2: string, field3: string, boolValue: string, stringValue: string, intValue: string) => {
|
(field1: string, field2: string, field3: string, boolValue: string, stringValue: string, intValue: string) => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { OpenFeature } from '@openfeature/web-sdk';
|
import { OpenFeature } from '@openfeature/web-sdk';
|
||||||
import { GenericContainer, StartedTestContainer } from 'testcontainers';
|
import type { StartedTestContainer } from 'testcontainers';
|
||||||
|
import { GenericContainer } from 'testcontainers';
|
||||||
import { FlagdWebProvider } from '../../lib/flagd-web-provider';
|
import { FlagdWebProvider } from '../../lib/flagd-web-provider';
|
||||||
import { autoBindSteps, loadFeature } from 'jest-cucumber';
|
import { autoBindSteps, loadFeature } from 'jest-cucumber';
|
||||||
import { FLAGD_NAME, GHERKIN_EVALUATION_FEATURE } from '../constants';
|
import { FLAGD_NAME, GHERKIN_EVALUATION_FEATURE } from '../constants';
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
import { CallbackClient, Code, ConnectError, PromiseClient } from '@connectrpc/connect';
|
import type { CallbackClient, ConnectError, PromiseClient } from '@connectrpc/connect';
|
||||||
|
import { Code } from '@connectrpc/connect';
|
||||||
import { Struct } from '@bufbuild/protobuf';
|
import { Struct } from '@bufbuild/protobuf';
|
||||||
import {
|
import type { Client, JsonValue } from '@openfeature/web-sdk';
|
||||||
Client,
|
import { ErrorCode, OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/web-sdk';
|
||||||
ErrorCode,
|
|
||||||
JsonValue,
|
|
||||||
OpenFeature,
|
|
||||||
ProviderEvents,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
} from '@openfeature/web-sdk';
|
|
||||||
import fetchMock from 'jest-fetch-mock';
|
import fetchMock from 'jest-fetch-mock';
|
||||||
import { Service } from '../proto/ts/flagd/evaluation/v1/evaluation_connect';
|
import type { Service } from '../proto/ts/flagd/evaluation/v1/evaluation_connect';
|
||||||
import { AnyFlag, EventStreamResponse, ResolveAllResponse } from '../proto/ts/flagd/evaluation/v1/evaluation_pb';
|
import type { AnyFlag, EventStreamResponse, ResolveAllResponse } from '../proto/ts/flagd/evaluation/v1/evaluation_pb';
|
||||||
import { FlagdWebProvider } from './flagd-web-provider';
|
import { FlagdWebProvider } from './flagd-web-provider';
|
||||||
|
|
||||||
const EVENT_CONFIGURATION_CHANGE = 'configuration_change';
|
const EVENT_CONFIGURATION_CHANGE = 'configuration_change';
|
||||||
|
|
|
@ -1,23 +1,27 @@
|
||||||
import { CallbackClient, createCallbackClient, createPromiseClient, PromiseClient } from '@connectrpc/connect';
|
import type { CallbackClient, PromiseClient } from '@connectrpc/connect';
|
||||||
|
import { createCallbackClient, createPromiseClient } from '@connectrpc/connect';
|
||||||
import { createConnectTransport } from '@connectrpc/connect-web';
|
import { createConnectTransport } from '@connectrpc/connect-web';
|
||||||
import { Struct } from '@bufbuild/protobuf';
|
import { Struct } from '@bufbuild/protobuf';
|
||||||
import {
|
import type {
|
||||||
EvaluationContext,
|
EvaluationContext,
|
||||||
FlagNotFoundError,
|
|
||||||
FlagValue,
|
FlagValue,
|
||||||
JsonValue,
|
JsonValue,
|
||||||
Logger,
|
Logger,
|
||||||
|
Provider,
|
||||||
|
ResolutionDetails,
|
||||||
|
} from '@openfeature/web-sdk';
|
||||||
|
import {
|
||||||
|
FlagNotFoundError,
|
||||||
OpenFeature,
|
OpenFeature,
|
||||||
OpenFeatureEventEmitter,
|
OpenFeatureEventEmitter,
|
||||||
Provider,
|
|
||||||
ProviderEvents,
|
ProviderEvents,
|
||||||
ResolutionDetails,
|
|
||||||
StandardResolutionReasons,
|
StandardResolutionReasons,
|
||||||
TypeMismatchError,
|
TypeMismatchError,
|
||||||
} from '@openfeature/web-sdk';
|
} from '@openfeature/web-sdk';
|
||||||
import { Service } from '../proto/ts/flagd/evaluation/v1/evaluation_connect';
|
import { Service } from '../proto/ts/flagd/evaluation/v1/evaluation_connect';
|
||||||
import { AnyFlag } from '../proto/ts/flagd/evaluation/v1/evaluation_pb';
|
import type { AnyFlag } from '../proto/ts/flagd/evaluation/v1/evaluation_pb';
|
||||||
import { FlagdProviderOptions, getOptions } from './options';
|
import type { FlagdProviderOptions } from './options';
|
||||||
|
import { getOptions } from './options';
|
||||||
|
|
||||||
export const ERROR_DISABLED = 'DISABLED';
|
export const ERROR_DISABLED = 'DISABLED';
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit bb763438abc5b0e97dc26a795bc72b7a9f3e4020
|
Subproject commit 2852d7772e6b8674681a6ee6b88db10dbe3f6899
|
|
@ -1 +1 @@
|
||||||
Subproject commit a69f748db2edfec7015ca6bb702ca22fd8c5ef30
|
Subproject commit a3678719bf9f62880375ba835ebe1bb7a77de409
|
|
@ -4,9 +4,15 @@ export const FLAGD_NAME = 'flagd';
|
||||||
export const UNSTABLE_CLIENT_NAME = 'unstable';
|
export const UNSTABLE_CLIENT_NAME = 'unstable';
|
||||||
export const UNAVAILABLE_CLIENT_NAME = 'unavailable';
|
export const UNAVAILABLE_CLIENT_NAME = 'unavailable';
|
||||||
|
|
||||||
export const GHERKIN_FLAGD_FEATURE = getGherkinTestPath('flagd.feature');
|
export const GHERKIN_FLAGD = getGherkinTestPath('*.feature');
|
||||||
export const GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE = getGherkinTestPath('flagd-json-evaluator.feature');
|
export const CONNECTION_FEATURE = getGherkinTestPath('connection.feature');
|
||||||
export const GHERKIN_FLAGD_RECONNECT_FEATURE = getGherkinTestPath('flagd-reconnect.feature');
|
export const CONTEXT_ENRICHMENT_FEATURE = getGherkinTestPath('contextEnrichment.feature');
|
||||||
|
export const EVALUATION_FEATURE = getGherkinTestPath('evaluation.feature');
|
||||||
|
export const EVENTS_FEATURE = getGherkinTestPath('events.feature');
|
||||||
|
export const METADATA_FEATURE = getGherkinTestPath('metadata.feature');
|
||||||
|
export const RPC_CACHING_FEATURE = getGherkinTestPath('rpc-caching.feature');
|
||||||
|
export const SELECTOR_FEATURE = getGherkinTestPath('selector.feature');
|
||||||
|
export const TARGETING_FEATURE = getGherkinTestPath('targeting.feature');
|
||||||
export const GHERKIN_EVALUATION_FEATURE = getGherkinTestPath(
|
export const GHERKIN_EVALUATION_FEATURE = getGherkinTestPath(
|
||||||
'evaluation.feature',
|
'evaluation.feature',
|
||||||
'spec/specification/assets/gherkin/',
|
'spec/specification/assets/gherkin/',
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
export * from './step-definitions';
|
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { StepsDefinitionCallbackOptions } from 'jest-cucumber/dist/src/feature-definition-creation';
|
||||||
|
import type { State, Steps } from './state';
|
||||||
|
import { CacheOption, getConfig, ResolverType } from '../../lib/configuration';
|
||||||
|
import { mapValueToType } from './utils';
|
||||||
|
|
||||||
|
export const configSteps: Steps = (state: State) => {
|
||||||
|
function mapName(name: string): string {
|
||||||
|
switch (name) {
|
||||||
|
case 'resolver':
|
||||||
|
return 'resolverType';
|
||||||
|
default:
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({ given, when, then }: StepsDefinitionCallbackOptions) => {
|
||||||
|
beforeEach(() => {
|
||||||
|
state.options = {};
|
||||||
|
});
|
||||||
|
given(/^an option "(.*)" of type "(.*)" with value "(.*)"$/, (name: string, type: string, value: string) => {
|
||||||
|
state.options[mapName(name)] = mapValueToType(value, type);
|
||||||
|
});
|
||||||
|
given(/^an environment variable "(.*)" with value "(.*)"$/, (name, value) => {
|
||||||
|
process.env[name] = value;
|
||||||
|
});
|
||||||
|
when('a config was initialized', () => {
|
||||||
|
state.config = getConfig(state.options);
|
||||||
|
});
|
||||||
|
|
||||||
|
then(
|
||||||
|
/^the option "(.*)" of type "(.*)" should have the value "(.*)"$/,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
(_name: string, _type: string, _value: string) => {
|
||||||
|
// TODO: implement with configuration unification, see: https://github.com/open-feature/js-sdk-contrib/issues/1096
|
||||||
|
// const expected = mapValueToType(value, type);
|
||||||
|
// const propertyName = mapName(name);
|
||||||
|
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// // @ts-ignore
|
||||||
|
// const configElement = state.config[propertyName];
|
||||||
|
// expect(configElement).toBe(expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
then('we should have an error', () => {});
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { JsonObject } from '@openfeature/server-sdk';
|
||||||
|
import type { State, Steps } from './state';
|
||||||
|
import { mapValueToType } from './utils';
|
||||||
|
|
||||||
|
export const contextSteps: Steps =
|
||||||
|
(state: State) =>
|
||||||
|
({ given, when, then }) => {
|
||||||
|
beforeEach(() => (state.context = undefined));
|
||||||
|
given(
|
||||||
|
/^a context containing a key "(.*)", with type "(.*)" and with value "(.*)"$/,
|
||||||
|
(key: string, type: string, value: string) => {
|
||||||
|
if (state.context == undefined) {
|
||||||
|
state.context = {};
|
||||||
|
}
|
||||||
|
state.context[key] = mapValueToType(value, type);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
given(
|
||||||
|
/^a context containing a nested property with outer key "(.*)" and inner key "(.*)", with value "(.*)"$/,
|
||||||
|
(outer: string, inner: string, value) => {
|
||||||
|
if (state.context == undefined) {
|
||||||
|
state.context = {};
|
||||||
|
}
|
||||||
|
state.context[outer] = { ...(state.context[outer] as JsonObject), [inner]: value };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
given(/^a context containing a targeting key with value "(.*)"$/, (key) => {
|
||||||
|
if (state.context == undefined) {
|
||||||
|
state.context = {};
|
||||||
|
}
|
||||||
|
state.context.targetingKey = key;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { ServerProviderEvents } from '@openfeature/server-sdk';
|
||||||
|
import type { State, Steps } from './state';
|
||||||
|
import { waitFor } from './utils';
|
||||||
|
|
||||||
|
export const eventSteps: Steps =
|
||||||
|
(state: State) =>
|
||||||
|
({ given, when, then }) => {
|
||||||
|
function map(eventType: string): ServerProviderEvents {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'error':
|
||||||
|
return ServerProviderEvents.Error;
|
||||||
|
case 'ready':
|
||||||
|
return ServerProviderEvents.Ready;
|
||||||
|
case 'stale':
|
||||||
|
return ServerProviderEvents.Stale;
|
||||||
|
case 'change':
|
||||||
|
return ServerProviderEvents.ConfigurationChanged;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error('unknown eventtype');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
given(/a (.*) event handler/, async (type: string) => {
|
||||||
|
state.client?.addHandler(map(type), (details) => {
|
||||||
|
state.events.push({ type, details });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
then(/^the (.*) event handler should have been executed$/, async (type: string) => {
|
||||||
|
await waitFor(() => expect(state.events.find((value) => value.type == type)).toBeDefined(), { timeout: 20000 });
|
||||||
|
expect(state.events.find((value) => value.type == type)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
then(/^the (.*) event handler should have been executed within (\d+)ms$/, async (type: string, ms: number) => {
|
||||||
|
await waitFor(() => expect(state.events.find((value) => value.type == type)).toBeDefined(), { timeout: ms });
|
||||||
|
const actual = state.events.find((value) => value.type == type);
|
||||||
|
expect(actual).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
when(/^a (.*) event was fired$/, async (type: string) => {
|
||||||
|
await waitFor(() => expect(state.events.find((value) => value.type == type)), { timeout: 2000 });
|
||||||
|
expect(state.events.find((value) => value.type == type)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
then('the flag should be part of the event payload', async () => {
|
||||||
|
await waitFor(() => expect(state.events.find((value) => value.type == 'change')), { timeout: 2000 });
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,13 +1,6 @@
|
||||||
import { StepDefinitions } from 'jest-cucumber';
|
import type { StepDefinitions } from 'jest-cucumber';
|
||||||
import {
|
import type { EvaluationContext, EvaluationDetails, FlagValue, JsonObject } from '@openfeature/server-sdk';
|
||||||
EvaluationContext,
|
import { OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/server-sdk';
|
||||||
EvaluationDetails,
|
|
||||||
FlagValue,
|
|
||||||
JsonObject,
|
|
||||||
OpenFeature,
|
|
||||||
ProviderEvents,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
} from '@openfeature/server-sdk';
|
|
||||||
import { E2E_CLIENT_NAME } from '@openfeature/flagd-core';
|
import { E2E_CLIENT_NAME } from '@openfeature/flagd-core';
|
||||||
|
|
||||||
export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then }) => {
|
export const flagStepDefinitions: StepDefinitions = ({ given, and, when, then }) => {
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import type { State, Steps } from './state';
|
||||||
|
import { mapValueToType } from './utils';
|
||||||
|
|
||||||
|
export const flagSteps: Steps =
|
||||||
|
(state: State) =>
|
||||||
|
({ given, when, then }) => {
|
||||||
|
beforeEach(() => {
|
||||||
|
state.flag = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
given(
|
||||||
|
/^a (.*)-flag with key "(.*)" and a default value "(.*)"$/,
|
||||||
|
(type: string, name: string, defaultValue: string) => {
|
||||||
|
state.flag = {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
defaultValue: mapValueToType(defaultValue, type),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
when('the flag was evaluated with details', async () => {
|
||||||
|
switch (state.flag?.type) {
|
||||||
|
case 'Boolean':
|
||||||
|
state.details = await state.client?.getBooleanDetails(
|
||||||
|
state.flag?.name,
|
||||||
|
<boolean>state.flag?.defaultValue,
|
||||||
|
state.context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'String':
|
||||||
|
state.details = await state.client?.getStringDetails(
|
||||||
|
state.flag?.name,
|
||||||
|
<string>state.flag?.defaultValue,
|
||||||
|
state.context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'Integer':
|
||||||
|
case 'Float':
|
||||||
|
state.details = await state.client?.getNumberDetails(
|
||||||
|
state.flag?.name,
|
||||||
|
<number>state.flag?.defaultValue,
|
||||||
|
state.context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'Object':
|
||||||
|
state.details = await state.client?.getObjectDetails(
|
||||||
|
state.flag?.name,
|
||||||
|
<boolean>state.flag?.defaultValue,
|
||||||
|
state.context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('unknown type');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
then(/^the resolved details value should be "(.*)"$/, (arg0) => {
|
||||||
|
expect(state.details?.value).toEqual(mapValueToType(arg0, state.flag!.type));
|
||||||
|
});
|
||||||
|
|
||||||
|
then(/^the reason should be "(.*)"$/, (expectedReason) => {
|
||||||
|
expect(state.details?.reason).toBe(expectedReason);
|
||||||
|
});
|
||||||
|
|
||||||
|
then(/^the variant should be "(.*)"$/, (expectedVariant) => {
|
||||||
|
expect(state.details?.variant).toBe(expectedVariant);
|
||||||
|
});
|
||||||
|
|
||||||
|
then('the resolved metadata should contain', (table) => {
|
||||||
|
// TODO: implement metadata tests, https://github.com/open-feature/js-sdk-contrib/issues/1290
|
||||||
|
});
|
||||||
|
|
||||||
|
then('the resolved metadata is empty', () => {
|
||||||
|
// TODO: implement metadata tests, https://github.com/open-feature/js-sdk-contrib/issues/1290
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './flag';
|
|
||||||
export * from './reconnect';
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { OpenFeature } from '@openfeature/server-sdk';
|
||||||
|
import { FlagdContainer } from '../tests/flagdContainer';
|
||||||
|
import type { State, Steps } from './state';
|
||||||
|
import { FlagdProvider } from '../../lib/flagd-provider';
|
||||||
|
import type { FlagdProviderOptions } from '../../lib/configuration';
|
||||||
|
|
||||||
|
export const providerSteps: Steps =
|
||||||
|
(state: State) =>
|
||||||
|
({ given, when, then }) => {
|
||||||
|
const container: FlagdContainer = FlagdContainer.build();
|
||||||
|
beforeAll(async () => {
|
||||||
|
console.log('Setting flagd provider...');
|
||||||
|
|
||||||
|
return await container.start();
|
||||||
|
}, 50000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await OpenFeature.close();
|
||||||
|
await container.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// everything breaks without this
|
||||||
|
await OpenFeature.close();
|
||||||
|
if (state.client) {
|
||||||
|
await fetch('http://' + container.getLaunchpadUrl() + '/stop');
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}, 50000);
|
||||||
|
|
||||||
|
given(/a (.*) flagd provider/, async (providerType: string) => {
|
||||||
|
const flagdOptions: FlagdProviderOptions = {
|
||||||
|
resolverType: state.resolverType,
|
||||||
|
deadlineMs: 2000,
|
||||||
|
};
|
||||||
|
let type = 'default';
|
||||||
|
switch (providerType) {
|
||||||
|
default:
|
||||||
|
flagdOptions['port'] = container.getPort(state.resolverType);
|
||||||
|
if (state?.options?.['selector']) {
|
||||||
|
flagdOptions['selector'] = state?.options?.['selector'] as string;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'unavailable':
|
||||||
|
flagdOptions['port'] = 9999;
|
||||||
|
break;
|
||||||
|
case 'ssl':
|
||||||
|
// TODO: modify this to support ssl
|
||||||
|
flagdOptions['port'] = container.getPort(state.resolverType);
|
||||||
|
if (state?.config?.selector) {
|
||||||
|
flagdOptions['selector'] = state.config.selector;
|
||||||
|
}
|
||||||
|
type = 'ssl';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch('http://' + container.getLaunchpadUrl() + '/start?config=' + type);
|
||||||
|
await new Promise((r) => setTimeout(r, 50));
|
||||||
|
if (providerType == 'unavailable') {
|
||||||
|
OpenFeature.setProvider(providerType, new FlagdProvider(flagdOptions));
|
||||||
|
} else {
|
||||||
|
await OpenFeature.setProviderAndWait(providerType, new FlagdProvider(flagdOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.client = OpenFeature.getClient(providerType);
|
||||||
|
state.providerType = providerType;
|
||||||
|
});
|
||||||
|
|
||||||
|
when(/^the connection is lost for (\d+)s$/, async (time) => {
|
||||||
|
console.log('stopping flagd');
|
||||||
|
await fetch('http://' + container.getLaunchpadUrl() + '/restart?seconds=' + time);
|
||||||
|
});
|
||||||
|
|
||||||
|
when('the flag was modified', async () => {
|
||||||
|
await fetch('http://' + container.getLaunchpadUrl() + '/change');
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { StepDefinitions } from 'jest-cucumber';
|
import type { StepDefinitions } from 'jest-cucumber';
|
||||||
import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk';
|
import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk';
|
||||||
import { UNAVAILABLE_CLIENT_NAME, UNSTABLE_CLIENT_NAME } from '../constants';
|
import { UNAVAILABLE_CLIENT_NAME, UNSTABLE_CLIENT_NAME } from '../constants';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { StepDefinitions } from 'jest-cucumber';
|
||||||
|
import type { CacheOption, ResolverType } from '../../lib/configuration';
|
||||||
|
import type { Client, EvaluationContext, EvaluationDetails, EventDetails, FlagValue } from '@openfeature/server-sdk';
|
||||||
|
|
||||||
|
interface Flag {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
defaultValue: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
type: string;
|
||||||
|
details?: EventDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
flagsChanged?: string[];
|
||||||
|
providerType?: string;
|
||||||
|
details?: EvaluationDetails<FlagValue>;
|
||||||
|
client?: Client;
|
||||||
|
resolverType: ResolverType;
|
||||||
|
context?: EvaluationContext;
|
||||||
|
config?: {
|
||||||
|
cache?: CacheOption;
|
||||||
|
socketPath?: string;
|
||||||
|
port: number;
|
||||||
|
maxCacheSize?: number;
|
||||||
|
resolverType?: ResolverType;
|
||||||
|
host: string;
|
||||||
|
offlineFlagSourcePath?: string;
|
||||||
|
tls: boolean;
|
||||||
|
selector?: string;
|
||||||
|
};
|
||||||
|
options: Record<string, unknown>;
|
||||||
|
events: Event[];
|
||||||
|
flag?: Flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Steps = (state: State) => StepDefinitions;
|
|
@ -0,0 +1,53 @@
|
||||||
|
import type { CacheOption, ResolverType } from '../../lib/configuration';
|
||||||
|
|
||||||
|
export function mapValueToType(value: string, type: string): any {
|
||||||
|
switch (type) {
|
||||||
|
case 'String':
|
||||||
|
if (value == 'null') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
case 'Integer':
|
||||||
|
return Number.parseInt(value);
|
||||||
|
case 'Float':
|
||||||
|
return Number.parseFloat(value);
|
||||||
|
case 'Long':
|
||||||
|
return Number.parseFloat(value);
|
||||||
|
case 'Boolean':
|
||||||
|
return value.toLowerCase() === 'true';
|
||||||
|
case 'ResolverType':
|
||||||
|
return value.toLowerCase() as ResolverType;
|
||||||
|
case 'CacheType':
|
||||||
|
return value as CacheOption;
|
||||||
|
case 'Object':
|
||||||
|
if (value == 'null') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return JSON.parse(value);
|
||||||
|
default:
|
||||||
|
throw new Error('type not supported');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitFor<T>(check: () => T, options: { timeout?: number; interval?: number } = {}): Promise<T> {
|
||||||
|
const { timeout = 5000, interval = 50 } = options; // Default 5s timeout, 50ms polling interval
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const checkCondition = () => {
|
||||||
|
try {
|
||||||
|
const result = check();
|
||||||
|
resolve(result); // If condition passes, resolve the promise
|
||||||
|
} catch (error) {
|
||||||
|
if (Date.now() - startTime > timeout) {
|
||||||
|
reject(new Error('Timeout while waiting for condition'));
|
||||||
|
} else {
|
||||||
|
setTimeout(checkCondition, interval); // Retry after interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkCondition();
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import type { StartedTestContainer, StoppedTestContainer } from 'testcontainers';
|
||||||
|
import { GenericContainer } from 'testcontainers';
|
||||||
|
import fs from 'fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import type { ResolverType } from '../../lib/configuration';
|
||||||
|
|
||||||
|
export class FlagdContainer extends GenericContainer {
|
||||||
|
private static imageBase = 'ghcr.io/open-feature/flagd-testbed';
|
||||||
|
private started: StartedTestContainer | undefined;
|
||||||
|
private stopped: StoppedTestContainer | undefined;
|
||||||
|
|
||||||
|
public static build() {
|
||||||
|
return new FlagdContainer(this.generateImageName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(image: string) {
|
||||||
|
super(image);
|
||||||
|
this.withExposedPorts(8080, 8013, 8014, 8015, 8016);
|
||||||
|
}
|
||||||
|
|
||||||
|
isStarted(): boolean {
|
||||||
|
return this.started !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<StartedTestContainer> {
|
||||||
|
if (this.isStarted()) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
return Promise.resolve(this.started);
|
||||||
|
}
|
||||||
|
const containerPromise = super.start();
|
||||||
|
this.started = await containerPromise;
|
||||||
|
this.stopped = undefined;
|
||||||
|
return containerPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<StoppedTestContainer> {
|
||||||
|
if (!this.started) {
|
||||||
|
throw new Error('container not started');
|
||||||
|
}
|
||||||
|
const containerPromise = this.started.stop();
|
||||||
|
this.stopped = await containerPromise;
|
||||||
|
this.started = undefined;
|
||||||
|
return containerPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLaunchpadUrl() {
|
||||||
|
return this.started?.getHost() + ':' + this.started?.getMappedPort(8080);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static generateImageName(): string {
|
||||||
|
const image = this.imageBase;
|
||||||
|
const file = path.join(__dirname, './../../../../../shared/flagd-core/test-harness/', 'version.txt');
|
||||||
|
const version = fs.readFileSync(file, 'utf8').trim();
|
||||||
|
return `${image}:v${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPort(resolverType: ResolverType) {
|
||||||
|
switch (resolverType) {
|
||||||
|
default:
|
||||||
|
return this.started?.getMappedPort(8013);
|
||||||
|
case 'in-process':
|
||||||
|
return this.started?.getMappedPort(8015);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,69 +0,0 @@
|
||||||
import assert from 'assert';
|
|
||||||
import { OpenFeature } from '@openfeature/server-sdk';
|
|
||||||
import { FlagdProvider } from '../../lib/flagd-provider';
|
|
||||||
import { GenericContainer, StartedTestContainer } from 'testcontainers';
|
|
||||||
import { autoBindSteps, loadFeature } from 'jest-cucumber';
|
|
||||||
import {
|
|
||||||
FLAGD_NAME,
|
|
||||||
GHERKIN_FLAGD_RECONNECT_FEATURE,
|
|
||||||
UNAVAILABLE_CLIENT_NAME,
|
|
||||||
UNSTABLE_CLIENT_NAME,
|
|
||||||
} from '../constants';
|
|
||||||
import { IMAGE_VERSION } from '@openfeature/flagd-core';
|
|
||||||
import { reconnectStepDefinitions } from '../step-definitions';
|
|
||||||
|
|
||||||
// register the flagd provider before the tests.
|
|
||||||
async function setup() {
|
|
||||||
const containers: StartedTestContainer[] = [];
|
|
||||||
|
|
||||||
console.log('Setting flagd provider...');
|
|
||||||
const unstable = await new GenericContainer(`ghcr.io/open-feature/flagd-testbed-unstable:${IMAGE_VERSION}`)
|
|
||||||
.withExposedPorts(8015)
|
|
||||||
.start();
|
|
||||||
containers.push(unstable);
|
|
||||||
await OpenFeature.setProviderAndWait(
|
|
||||||
UNSTABLE_CLIENT_NAME,
|
|
||||||
new FlagdProvider({ resolverType: 'in-process', host: 'localhost', port: unstable.getFirstMappedPort() }),
|
|
||||||
);
|
|
||||||
|
|
||||||
OpenFeature.setProvider(
|
|
||||||
UNAVAILABLE_CLIENT_NAME,
|
|
||||||
new FlagdProvider({ resolverType: 'in-process', host: 'localhost', port: 9092 }),
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
OpenFeature.getProviderMetadata(UNSTABLE_CLIENT_NAME).name === FLAGD_NAME,
|
|
||||||
new Error(
|
|
||||||
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
|
|
||||||
OpenFeature.getProviderMetadata(UNSTABLE_CLIENT_NAME).name
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
OpenFeature.getProviderMetadata(UNAVAILABLE_CLIENT_NAME).name === FLAGD_NAME,
|
|
||||||
new Error(
|
|
||||||
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
|
|
||||||
OpenFeature.getProviderMetadata(UNAVAILABLE_CLIENT_NAME).name
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('flagd provider configured!');
|
|
||||||
return containers;
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.setTimeout(30000);
|
|
||||||
|
|
||||||
describe('in process', () => {
|
|
||||||
let containers: StartedTestContainer[] = [];
|
|
||||||
beforeAll(async () => {
|
|
||||||
containers = await setup();
|
|
||||||
}, 60000);
|
|
||||||
afterAll(async () => {
|
|
||||||
await OpenFeature.close();
|
|
||||||
for (const container of containers) {
|
|
||||||
await container.stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const features = [loadFeature(GHERKIN_FLAGD_RECONNECT_FEATURE)];
|
|
||||||
autoBindSteps(features, [reconnectStepDefinitions]);
|
|
||||||
});
|
|
|
@ -1,60 +1,32 @@
|
||||||
import assert from 'assert';
|
import { autoBindSteps, loadFeatures } from 'jest-cucumber';
|
||||||
import { OpenFeature } from '@openfeature/server-sdk';
|
import { GHERKIN_FLAGD } from '../constants';
|
||||||
import { FlagdProvider } from '../../lib/flagd-provider';
|
import { providerSteps } from '../step-definitions/providerSteps';
|
||||||
import { GenericContainer, StartedTestContainer } from 'testcontainers';
|
import { configSteps } from '../step-definitions/configSteps';
|
||||||
import { autoBindSteps, loadFeature } from 'jest-cucumber';
|
import type { State } from '../step-definitions/state';
|
||||||
import {
|
import { eventSteps } from '../step-definitions/eventSteps';
|
||||||
FLAGD_NAME,
|
import { flagSteps } from '../step-definitions/flagSteps';
|
||||||
GHERKIN_EVALUATION_FEATURE,
|
import { contextSteps } from '../step-definitions/contextSteps';
|
||||||
GHERKIN_FLAGD_FEATURE,
|
|
||||||
GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE,
|
|
||||||
} from '../constants';
|
|
||||||
import { flagStepDefinitions } from '../step-definitions';
|
|
||||||
import { E2E_CLIENT_NAME, IMAGE_VERSION } from '@openfeature/flagd-core';
|
|
||||||
|
|
||||||
// register the flagd provider before the tests.
|
const steps = [providerSteps, configSteps, eventSteps, flagSteps, contextSteps];
|
||||||
async function setup() {
|
|
||||||
const containers: StartedTestContainer[] = [];
|
|
||||||
|
|
||||||
console.log('Setting flagd provider...');
|
jest.setTimeout(50000);
|
||||||
|
describe('in-process', () => {
|
||||||
const stable = await new GenericContainer(`ghcr.io/open-feature/flagd-testbed:${IMAGE_VERSION}`)
|
const state: State = {
|
||||||
.withExposedPorts(8015)
|
resolverType: 'in-process',
|
||||||
.start();
|
options: {},
|
||||||
containers.push(stable);
|
config: undefined,
|
||||||
OpenFeature.setProvider(
|
events: [],
|
||||||
E2E_CLIENT_NAME,
|
};
|
||||||
new FlagdProvider({ resolverType: 'in-process', host: 'localhost', port: stable.getFirstMappedPort() }),
|
autoBindSteps(
|
||||||
|
loadFeatures(GHERKIN_FLAGD, {
|
||||||
|
// remove filters as we add support for features
|
||||||
|
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
|
||||||
|
tagFilter:
|
||||||
|
'@in-process and not @targetURI and not @customCert and not @events and not @sync and not @grace and not @metadata and not @contextEnrichment',
|
||||||
|
scenarioNameTemplate: (vars) => {
|
||||||
|
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
steps.map((step) => step(state)),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
|
||||||
OpenFeature.getProviderMetadata(E2E_CLIENT_NAME).name === FLAGD_NAME,
|
|
||||||
new Error(
|
|
||||||
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
|
|
||||||
OpenFeature.getProviderMetadata(E2E_CLIENT_NAME).name
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('flagd provider configured!');
|
|
||||||
return containers;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('in process', () => {
|
|
||||||
let containers: StartedTestContainer[] = [];
|
|
||||||
beforeAll(async () => {
|
|
||||||
containers = await setup();
|
|
||||||
}, 60000);
|
|
||||||
afterAll(async () => {
|
|
||||||
await OpenFeature.close();
|
|
||||||
for (const container of containers) {
|
|
||||||
await container.stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const features = [
|
|
||||||
loadFeature(GHERKIN_FLAGD_FEATURE),
|
|
||||||
loadFeature(GHERKIN_EVALUATION_FEATURE),
|
|
||||||
loadFeature(GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE),
|
|
||||||
];
|
|
||||||
autoBindSteps(features, [flagStepDefinitions]);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
import assert from 'assert';
|
|
||||||
import { OpenFeature } from '@openfeature/server-sdk';
|
|
||||||
import { FlagdProvider } from '../../lib/flagd-provider';
|
|
||||||
import { GenericContainer, StartedTestContainer } from 'testcontainers';
|
|
||||||
import { autoBindSteps, loadFeature } from 'jest-cucumber';
|
|
||||||
import {
|
|
||||||
FLAGD_NAME,
|
|
||||||
GHERKIN_FLAGD_RECONNECT_FEATURE,
|
|
||||||
UNAVAILABLE_CLIENT_NAME,
|
|
||||||
UNSTABLE_CLIENT_NAME,
|
|
||||||
} from '../constants';
|
|
||||||
import { reconnectStepDefinitions } from '../step-definitions';
|
|
||||||
import { IMAGE_VERSION } from '@openfeature/flagd-core';
|
|
||||||
|
|
||||||
// register the flagd provider before the tests.
|
|
||||||
async function setup() {
|
|
||||||
const containers: StartedTestContainer[] = [];
|
|
||||||
|
|
||||||
console.log('Setting flagd provider...');
|
|
||||||
const unstable = await new GenericContainer(`ghcr.io/open-feature/flagd-testbed-unstable:${IMAGE_VERSION}`)
|
|
||||||
.withExposedPorts(8013)
|
|
||||||
.start();
|
|
||||||
containers.push(unstable);
|
|
||||||
OpenFeature.setProvider(
|
|
||||||
UNSTABLE_CLIENT_NAME,
|
|
||||||
new FlagdProvider({ cache: 'disabled', port: unstable.getFirstMappedPort() }),
|
|
||||||
);
|
|
||||||
OpenFeature.setProvider(UNAVAILABLE_CLIENT_NAME, new FlagdProvider({ cache: 'disabled', port: 8015 }));
|
|
||||||
assert(
|
|
||||||
OpenFeature.getProviderMetadata(UNSTABLE_CLIENT_NAME).name === FLAGD_NAME,
|
|
||||||
new Error(
|
|
||||||
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
|
|
||||||
OpenFeature.getProviderMetadata(UNSTABLE_CLIENT_NAME).name
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
OpenFeature.getProviderMetadata(UNAVAILABLE_CLIENT_NAME).name === FLAGD_NAME,
|
|
||||||
new Error(
|
|
||||||
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
|
|
||||||
OpenFeature.getProviderMetadata(UNAVAILABLE_CLIENT_NAME).name
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('flagd provider configured!');
|
|
||||||
return containers;
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.setTimeout(30000);
|
|
||||||
describe('rpc', () => {
|
|
||||||
let containers: StartedTestContainer[] = [];
|
|
||||||
beforeAll(async () => {
|
|
||||||
containers = await setup();
|
|
||||||
}, 60000);
|
|
||||||
afterAll(async () => {
|
|
||||||
await OpenFeature.close();
|
|
||||||
for (const container of containers) {
|
|
||||||
try {
|
|
||||||
await container.stop();
|
|
||||||
} catch {
|
|
||||||
console.warn(`Failed to stop container ${container.getName()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const features = [loadFeature(GHERKIN_FLAGD_RECONNECT_FEATURE)];
|
|
||||||
autoBindSteps(features, [reconnectStepDefinitions]);
|
|
||||||
});
|
|
|
@ -1,56 +1,33 @@
|
||||||
import assert from 'assert';
|
import { autoBindSteps, loadFeatures } from 'jest-cucumber';
|
||||||
import { OpenFeature } from '@openfeature/server-sdk';
|
import { providerSteps } from '../step-definitions/providerSteps';
|
||||||
import { FlagdProvider } from '../../lib/flagd-provider';
|
import { configSteps } from '../step-definitions/configSteps';
|
||||||
import { GenericContainer, StartedTestContainer } from 'testcontainers';
|
import type { State } from '../step-definitions/state';
|
||||||
import { autoBindSteps, loadFeature } from 'jest-cucumber';
|
import { eventSteps } from '../step-definitions/eventSteps';
|
||||||
import {
|
import { flagSteps } from '../step-definitions/flagSteps';
|
||||||
FLAGD_NAME,
|
import { contextSteps } from '../step-definitions/contextSteps';
|
||||||
GHERKIN_EVALUATION_FEATURE,
|
import { GHERKIN_FLAGD } from '../constants';
|
||||||
GHERKIN_FLAGD_FEATURE,
|
|
||||||
GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE,
|
|
||||||
} from '../constants';
|
|
||||||
import { E2E_CLIENT_NAME, IMAGE_VERSION } from '@openfeature/flagd-core';
|
|
||||||
import { flagStepDefinitions } from '../step-definitions';
|
|
||||||
|
|
||||||
// register the flagd provider before the tests.
|
const steps = [providerSteps, configSteps, eventSteps, flagSteps, contextSteps];
|
||||||
async function setup() {
|
|
||||||
const containers: StartedTestContainer[] = [];
|
|
||||||
|
|
||||||
console.log('Setting flagd provider...');
|
jest.setTimeout(50000);
|
||||||
const stable = await new GenericContainer(`ghcr.io/open-feature/flagd-testbed:${IMAGE_VERSION}`)
|
|
||||||
.withExposedPorts(8013)
|
|
||||||
.start();
|
|
||||||
containers.push(stable);
|
|
||||||
OpenFeature.setProvider(E2E_CLIENT_NAME, new FlagdProvider({ cache: 'disabled', port: stable.getFirstMappedPort() }));
|
|
||||||
|
|
||||||
assert(
|
|
||||||
OpenFeature.getProviderMetadata(E2E_CLIENT_NAME).name === FLAGD_NAME,
|
|
||||||
new Error(
|
|
||||||
`Expected ${FLAGD_NAME} provider to be configured, instead got: ${
|
|
||||||
OpenFeature.getProviderMetadata(E2E_CLIENT_NAME).name
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('flagd provider configured!');
|
|
||||||
return containers;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('rpc', () => {
|
describe('rpc', () => {
|
||||||
let containers: StartedTestContainer[] = [];
|
const state: State = {
|
||||||
beforeAll(async () => {
|
resolverType: 'rpc',
|
||||||
containers = await setup();
|
options: {},
|
||||||
}, 60000);
|
config: undefined,
|
||||||
afterAll(async () => {
|
events: [],
|
||||||
await OpenFeature.close();
|
};
|
||||||
for (const container of containers) {
|
autoBindSteps(
|
||||||
await container.stop();
|
loadFeatures(GHERKIN_FLAGD, {
|
||||||
}
|
tagFilter:
|
||||||
});
|
// remove filters as we add support for features
|
||||||
const features = [
|
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
|
||||||
loadFeature(GHERKIN_FLAGD_FEATURE),
|
'@rpc and not @targetURI and not @customCert and not @events and not @stream and not @grace and not @metadata and not @contextEnrichment and not @caching',
|
||||||
loadFeature(GHERKIN_EVALUATION_FEATURE),
|
scenarioNameTemplate: (vars) => {
|
||||||
loadFeature(GHERKIN_FLAGD_JSON_EVALUATOR_FEATURE),
|
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
|
||||||
];
|
},
|
||||||
autoBindSteps(features, [flagStepDefinitions]);
|
}),
|
||||||
|
steps.map((step) => step(state)),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Config, FlagdProviderOptions, getConfig } from './configuration';
|
import type { Config, FlagdProviderOptions } from './configuration';
|
||||||
|
import { getConfig } from './configuration';
|
||||||
import { DEFAULT_MAX_CACHE_SIZE } from './constants';
|
import { DEFAULT_MAX_CACHE_SIZE } from './constants';
|
||||||
|
|
||||||
describe('Configuration', () => {
|
describe('Configuration', () => {
|
||||||
|
@ -18,6 +19,7 @@ describe('Configuration', () => {
|
||||||
cache: 'lru',
|
cache: 'lru',
|
||||||
resolverType: 'rpc',
|
resolverType: 'rpc',
|
||||||
selector: '',
|
selector: '',
|
||||||
|
deadlineMs: 500,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,6 +57,7 @@ describe('Configuration', () => {
|
||||||
selector,
|
selector,
|
||||||
offlineFlagSourcePath,
|
offlineFlagSourcePath,
|
||||||
defaultAuthority,
|
defaultAuthority,
|
||||||
|
deadlineMs: 500,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,6 +71,7 @@ describe('Configuration', () => {
|
||||||
resolverType: 'rpc',
|
resolverType: 'rpc',
|
||||||
selector: '',
|
selector: '',
|
||||||
defaultAuthority: '',
|
defaultAuthority: '',
|
||||||
|
deadlineMs: 500,
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env['FLAGD_HOST'] = 'override';
|
process.env['FLAGD_HOST'] = 'override';
|
||||||
|
|
|
@ -18,6 +18,13 @@ export interface Config {
|
||||||
*/
|
*/
|
||||||
port: number;
|
port: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The deadline for connections.
|
||||||
|
*
|
||||||
|
* @default 500
|
||||||
|
*/
|
||||||
|
deadlineMs: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if TLS should be used.
|
* Determines if TLS should be used.
|
||||||
*
|
*
|
||||||
|
@ -79,6 +86,7 @@ export interface Config {
|
||||||
export type FlagdProviderOptions = Partial<Config>;
|
export type FlagdProviderOptions = Partial<Config>;
|
||||||
|
|
||||||
const DEFAULT_CONFIG: Omit<Config, 'port' | 'resolverType'> = {
|
const DEFAULT_CONFIG: Omit<Config, 'port' | 'resolverType'> = {
|
||||||
|
deadlineMs: 500,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
tls: false,
|
tls: false,
|
||||||
selector: '',
|
selector: '',
|
||||||
|
@ -93,6 +101,7 @@ const DEFAULT_IN_PROCESS_CONFIG: Config = { ...DEFAULT_CONFIG, resolverType: 'in
|
||||||
enum ENV_VAR {
|
enum ENV_VAR {
|
||||||
FLAGD_HOST = 'FLAGD_HOST',
|
FLAGD_HOST = 'FLAGD_HOST',
|
||||||
FLAGD_PORT = 'FLAGD_PORT',
|
FLAGD_PORT = 'FLAGD_PORT',
|
||||||
|
FLAGD_DEADLINE_MS = 'FLAGD_DEADLINE_MS',
|
||||||
FLAGD_TLS = 'FLAGD_TLS',
|
FLAGD_TLS = 'FLAGD_TLS',
|
||||||
FLAGD_SOCKET_PATH = 'FLAGD_SOCKET_PATH',
|
FLAGD_SOCKET_PATH = 'FLAGD_SOCKET_PATH',
|
||||||
FLAGD_CACHE = 'FLAGD_CACHE',
|
FLAGD_CACHE = 'FLAGD_CACHE',
|
||||||
|
@ -103,38 +112,60 @@ enum ENV_VAR {
|
||||||
FLAGD_DEFAULT_AUTHORITY = 'FLAGD_DEFAULT_AUTHORITY',
|
FLAGD_DEFAULT_AUTHORITY = 'FLAGD_DEFAULT_AUTHORITY',
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEnvVarConfig = (): Partial<Config> => ({
|
function checkEnvVarResolverType() {
|
||||||
...(process.env[ENV_VAR.FLAGD_HOST] && {
|
return (
|
||||||
host: process.env[ENV_VAR.FLAGD_HOST],
|
process.env[ENV_VAR.FLAGD_RESOLVER] &&
|
||||||
}),
|
(process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase() === 'rpc' ||
|
||||||
...(Number(process.env[ENV_VAR.FLAGD_PORT]) && {
|
process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase() === 'in-process')
|
||||||
port: Number(process.env[ENV_VAR.FLAGD_PORT]),
|
);
|
||||||
}),
|
}
|
||||||
...(process.env[ENV_VAR.FLAGD_TLS] && {
|
|
||||||
tls: process.env[ENV_VAR.FLAGD_TLS]?.toLowerCase() === 'true',
|
const getEnvVarConfig = (): Partial<Config> => {
|
||||||
}),
|
let provider = undefined;
|
||||||
...(process.env[ENV_VAR.FLAGD_SOCKET_PATH] && {
|
if (
|
||||||
socketPath: process.env[ENV_VAR.FLAGD_SOCKET_PATH],
|
process.env[ENV_VAR.FLAGD_RESOLVER] &&
|
||||||
}),
|
(process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase() === 'rpc' ||
|
||||||
...((process.env[ENV_VAR.FLAGD_CACHE] === 'lru' || process.env[ENV_VAR.FLAGD_CACHE] === 'disabled') && {
|
process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase() === 'in-process')
|
||||||
cache: process.env[ENV_VAR.FLAGD_CACHE],
|
) {
|
||||||
}),
|
provider = process.env[ENV_VAR.FLAGD_RESOLVER].toLowerCase();
|
||||||
...(process.env[ENV_VAR.FLAGD_MAX_CACHE_SIZE] && {
|
}
|
||||||
maxCacheSize: Number(process.env[ENV_VAR.FLAGD_MAX_CACHE_SIZE]),
|
|
||||||
}),
|
return {
|
||||||
...(process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR] && {
|
...(process.env[ENV_VAR.FLAGD_HOST] && {
|
||||||
selector: process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR],
|
host: process.env[ENV_VAR.FLAGD_HOST],
|
||||||
}),
|
}),
|
||||||
...((process.env[ENV_VAR.FLAGD_RESOLVER] === 'rpc' || process.env[ENV_VAR.FLAGD_RESOLVER] === 'in-process') && {
|
...(Number(process.env[ENV_VAR.FLAGD_PORT]) && {
|
||||||
resolverType: process.env[ENV_VAR.FLAGD_RESOLVER],
|
port: Number(process.env[ENV_VAR.FLAGD_PORT]),
|
||||||
}),
|
}),
|
||||||
...(process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH] && {
|
...(Number(process.env[ENV_VAR.FLAGD_DEADLINE_MS]) && {
|
||||||
offlineFlagSourcePath: process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH],
|
deadlineMs: Number(process.env[ENV_VAR.FLAGD_DEADLINE_MS]),
|
||||||
}),
|
}),
|
||||||
...(process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY] && {
|
...(process.env[ENV_VAR.FLAGD_TLS] && {
|
||||||
defaultAuthority: process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY],
|
tls: process.env[ENV_VAR.FLAGD_TLS]?.toLowerCase() === 'true',
|
||||||
}),
|
}),
|
||||||
});
|
...(process.env[ENV_VAR.FLAGD_SOCKET_PATH] && {
|
||||||
|
socketPath: process.env[ENV_VAR.FLAGD_SOCKET_PATH],
|
||||||
|
}),
|
||||||
|
...((process.env[ENV_VAR.FLAGD_CACHE] === 'lru' || process.env[ENV_VAR.FLAGD_CACHE] === 'disabled') && {
|
||||||
|
cache: process.env[ENV_VAR.FLAGD_CACHE],
|
||||||
|
}),
|
||||||
|
...(process.env[ENV_VAR.FLAGD_MAX_CACHE_SIZE] && {
|
||||||
|
maxCacheSize: Number(process.env[ENV_VAR.FLAGD_MAX_CACHE_SIZE]),
|
||||||
|
}),
|
||||||
|
...(process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR] && {
|
||||||
|
selector: process.env[ENV_VAR.FLAGD_SOURCE_SELECTOR],
|
||||||
|
}),
|
||||||
|
...(provider && {
|
||||||
|
resolverType: provider as ResolverType,
|
||||||
|
}),
|
||||||
|
...(process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH] && {
|
||||||
|
offlineFlagSourcePath: process.env[ENV_VAR.FLAGD_OFFLINE_FLAG_SOURCE_PATH],
|
||||||
|
}),
|
||||||
|
...(process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY] && {
|
||||||
|
defaultAuthority: process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function getConfig(options: FlagdProviderOptions = {}) {
|
export function getConfig(options: FlagdProviderOptions = {}) {
|
||||||
const envVarConfig = getEnvVarConfig();
|
const envVarConfig = getEnvVarConfig();
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { ServiceError, status } from '@grpc/grpc-js';
|
import type { ServiceError } from '@grpc/grpc-js';
|
||||||
import {
|
import { status } from '@grpc/grpc-js';
|
||||||
Client,
|
import type { Client, EvaluationContext, FlagMetadata } from '@openfeature/server-sdk';
|
||||||
ErrorCode,
|
import { ErrorCode, OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/server-sdk';
|
||||||
EvaluationContext,
|
|
||||||
FlagMetadata,
|
|
||||||
OpenFeature,
|
|
||||||
ProviderEvents,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
} from '@openfeature/server-sdk';
|
|
||||||
import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
|
import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
|
||||||
import {
|
import type {
|
||||||
EventStreamResponse,
|
EventStreamResponse,
|
||||||
ResolveBooleanRequest,
|
ResolveBooleanRequest,
|
||||||
ResolveBooleanResponse,
|
ResolveBooleanResponse,
|
||||||
|
@ -22,10 +16,11 @@ import {
|
||||||
ResolveObjectResponse,
|
ResolveObjectResponse,
|
||||||
ResolveStringRequest,
|
ResolveStringRequest,
|
||||||
ResolveStringResponse,
|
ResolveStringResponse,
|
||||||
ServiceClient,
|
|
||||||
} from '../proto/ts/flagd/evaluation/v1/evaluation';
|
} from '../proto/ts/flagd/evaluation/v1/evaluation';
|
||||||
|
import { ServiceClient } from '../proto/ts/flagd/evaluation/v1/evaluation';
|
||||||
import { FlagdProvider } from './flagd-provider';
|
import { FlagdProvider } from './flagd-provider';
|
||||||
import { FlagChangeMessage, GRPCService } from './service/grpc/grpc-service';
|
import type { FlagChangeMessage } from './service/grpc/grpc-service';
|
||||||
|
import { GRPCService } from './service/grpc/grpc-service';
|
||||||
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
|
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
|
||||||
import { EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from './constants';
|
import { EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from './constants';
|
||||||
|
|
||||||
|
@ -149,7 +144,7 @@ describe(FlagdProvider.name, () => {
|
||||||
new FlagdProvider(
|
new FlagdProvider(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
new GRPCService({ host: '', port: 123, tls: false }, basicServiceClientMock),
|
new GRPCService({ deadlineMs: 100, host: '', port: 123, tls: false }, basicServiceClientMock),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
client = OpenFeature.getClient('basic test');
|
client = OpenFeature.getClient('basic test');
|
||||||
|
@ -306,7 +301,10 @@ describe(FlagdProvider.name, () => {
|
||||||
new FlagdProvider(
|
new FlagdProvider(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
|
new GRPCService(
|
||||||
|
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
|
||||||
|
streamingServiceClientMock,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.initialize()
|
.initialize()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -331,7 +329,10 @@ describe(FlagdProvider.name, () => {
|
||||||
new FlagdProvider(
|
new FlagdProvider(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
|
new GRPCService(
|
||||||
|
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
|
||||||
|
streamingServiceClientMock,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// fire message saying provider is ready
|
// fire message saying provider is ready
|
||||||
|
@ -380,7 +381,10 @@ describe(FlagdProvider.name, () => {
|
||||||
new FlagdProvider(
|
new FlagdProvider(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
|
new GRPCService(
|
||||||
|
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
|
||||||
|
streamingServiceClientMock,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// fire message saying provider is ready
|
// fire message saying provider is ready
|
||||||
|
@ -412,7 +416,10 @@ describe(FlagdProvider.name, () => {
|
||||||
new FlagdProvider(
|
new FlagdProvider(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
|
new GRPCService(
|
||||||
|
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
|
||||||
|
streamingServiceClientMock,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// fire message saying provider is ready
|
// fire message saying provider is ready
|
||||||
|
@ -501,7 +508,10 @@ describe(FlagdProvider.name, () => {
|
||||||
const provider = new FlagdProvider(
|
const provider = new FlagdProvider(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
new GRPCService({ host: '', port: 123, tls: false, cache: 'lru' }, streamingServiceClientMock),
|
new GRPCService(
|
||||||
|
{ deadlineMs: 100, host: '', port: 123, tls: false, cache: 'lru' },
|
||||||
|
streamingServiceClientMock,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
provider.initialize().catch(() => {
|
provider.initialize().catch(() => {
|
||||||
// ignore
|
// ignore
|
||||||
|
@ -579,7 +589,7 @@ describe(FlagdProvider.name, () => {
|
||||||
new FlagdProvider(
|
new FlagdProvider(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
new GRPCService({ host: '', port: 123, tls: false }, errServiceClientMock),
|
new GRPCService({ deadlineMs: 100, host: '', port: 123, tls: false }, errServiceClientMock),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
client = OpenFeature.getClient('errors test');
|
client = OpenFeature.getClient('errors test');
|
||||||
|
@ -666,7 +676,7 @@ describe(FlagdProvider.name, () => {
|
||||||
new FlagdProvider(
|
new FlagdProvider(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
new GRPCService({ host: '', port: 123, tls: false }, errServiceClientMock),
|
new GRPCService({ deadlineMs: 100, host: '', port: 123, tls: false }, errServiceClientMock),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
import {
|
import type { EvaluationContext, JsonValue, Logger, Provider, ResolutionDetails } from '@openfeature/server-sdk';
|
||||||
EvaluationContext,
|
import { OpenFeatureEventEmitter, ProviderEvents } from '@openfeature/server-sdk';
|
||||||
JsonValue,
|
import type { FlagdProviderOptions } from './configuration';
|
||||||
Logger,
|
import { getConfig } from './configuration';
|
||||||
OpenFeatureEventEmitter,
|
|
||||||
Provider,
|
|
||||||
ProviderEvents,
|
|
||||||
ResolutionDetails,
|
|
||||||
} from '@openfeature/server-sdk';
|
|
||||||
import { FlagdProviderOptions, getConfig } from './configuration';
|
|
||||||
import { GRPCService } from './service/grpc/grpc-service';
|
import { GRPCService } from './service/grpc/grpc-service';
|
||||||
import { Service } from './service/service';
|
import type { Service } from './service/service';
|
||||||
import { InProcessService } from './service/in-process/in-process-service';
|
import { InProcessService } from './service/in-process/in-process-service';
|
||||||
|
|
||||||
export class FlagdProvider implements Provider {
|
export class FlagdProvider implements Provider {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ClientReadableStream } from '@grpc/grpc-js';
|
import type { ClientReadableStream } from '@grpc/grpc-js';
|
||||||
|
|
||||||
export const closeStreamIfDefined = (stream: ClientReadableStream<unknown> | undefined) => {
|
export const closeStreamIfDefined = (stream: ClientReadableStream<unknown> | undefined) => {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import { ClientReadableStream, ClientUnaryCall, ServiceError, credentials, status, ClientOptions } from '@grpc/grpc-js';
|
import type { ClientOptions, ClientReadableStream, ClientUnaryCall, ServiceError } from '@grpc/grpc-js';
|
||||||
|
import { credentials, status } from '@grpc/grpc-js';
|
||||||
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
|
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
|
||||||
|
import type { EvaluationContext, FlagValue, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
|
||||||
import {
|
import {
|
||||||
EvaluationContext,
|
|
||||||
FlagNotFoundError,
|
FlagNotFoundError,
|
||||||
FlagValue,
|
|
||||||
GeneralError,
|
GeneralError,
|
||||||
JsonValue,
|
|
||||||
Logger,
|
|
||||||
ParseError,
|
ParseError,
|
||||||
ResolutionDetails,
|
|
||||||
StandardResolutionReasons,
|
StandardResolutionReasons,
|
||||||
TypeMismatchError,
|
TypeMismatchError,
|
||||||
} from '@openfeature/server-sdk';
|
} from '@openfeature/server-sdk';
|
||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import {
|
import type {
|
||||||
EventStreamResponse,
|
EventStreamResponse,
|
||||||
ResolveBooleanRequest,
|
ResolveBooleanRequest,
|
||||||
ResolveBooleanResponse,
|
ResolveBooleanResponse,
|
||||||
|
@ -26,12 +23,12 @@ import {
|
||||||
ResolveObjectResponse,
|
ResolveObjectResponse,
|
||||||
ResolveStringRequest,
|
ResolveStringRequest,
|
||||||
ResolveStringResponse,
|
ResolveStringResponse,
|
||||||
ServiceClient,
|
|
||||||
} from '../../../proto/ts/flagd/evaluation/v1/evaluation';
|
} from '../../../proto/ts/flagd/evaluation/v1/evaluation';
|
||||||
import { Config } from '../../configuration';
|
import { ServiceClient } from '../../../proto/ts/flagd/evaluation/v1/evaluation';
|
||||||
|
import type { Config } from '../../configuration';
|
||||||
import { DEFAULT_MAX_CACHE_SIZE, EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from '../../constants';
|
import { DEFAULT_MAX_CACHE_SIZE, EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from '../../constants';
|
||||||
import { FlagdProvider } from '../../flagd-provider';
|
import { FlagdProvider } from '../../flagd-provider';
|
||||||
import { Service } from '../service';
|
import type { Service } from '../service';
|
||||||
import { closeStreamIfDefined } from '../common';
|
import { closeStreamIfDefined } from '../common';
|
||||||
|
|
||||||
type AnyResponse =
|
type AnyResponse =
|
||||||
|
@ -70,6 +67,8 @@ export class GRPCService implements Service {
|
||||||
private _cache: LRUCache<string, ResolutionDetails<FlagValue>> | undefined;
|
private _cache: LRUCache<string, ResolutionDetails<FlagValue>> | undefined;
|
||||||
private _cacheEnabled = false;
|
private _cacheEnabled = false;
|
||||||
private _eventStream: ClientReadableStream<EventStreamResponse> | undefined = undefined;
|
private _eventStream: ClientReadableStream<EventStreamResponse> | undefined = undefined;
|
||||||
|
private _deadline: number;
|
||||||
|
|
||||||
private get _cacheActive() {
|
private get _cacheActive() {
|
||||||
// the cache is "active" (able to be used) if the config enabled it, AND the gRPC stream is live
|
// the cache is "active" (able to be used) if the config enabled it, AND the gRPC stream is live
|
||||||
return this._cacheEnabled && this._client.getChannel().getConnectivityState(false) === ConnectivityState.READY;
|
return this._cacheEnabled && this._client.getChannel().getConnectivityState(false) === ConnectivityState.READY;
|
||||||
|
@ -95,6 +94,7 @@ export class GRPCService implements Service {
|
||||||
tls ? credentials.createSsl() : credentials.createInsecure(),
|
tls ? credentials.createSsl() : credentials.createInsecure(),
|
||||||
clientOptions,
|
clientOptions,
|
||||||
);
|
);
|
||||||
|
this._deadline = config.deadlineMs;
|
||||||
|
|
||||||
if (config.cache === 'lru') {
|
if (config.cache === 'lru') {
|
||||||
this._cacheEnabled = true;
|
this._cacheEnabled = true;
|
||||||
|
@ -165,7 +165,7 @@ export class GRPCService implements Service {
|
||||||
// close the previous stream if we're reconnecting
|
// close the previous stream if we're reconnecting
|
||||||
closeStreamIfDefined(this._eventStream);
|
closeStreamIfDefined(this._eventStream);
|
||||||
|
|
||||||
const stream = this._client.eventStream({}, {});
|
const stream = this._client.eventStream({ waitForReady: true }, {});
|
||||||
stream.on('error', (err: Error) => {
|
stream.on('error', (err: Error) => {
|
||||||
rejectConnect?.(err);
|
rejectConnect?.(err);
|
||||||
this.handleError(reconnectCallback, changedCallback, disconnectCallback);
|
this.handleError(reconnectCallback, changedCallback, disconnectCallback);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { FileFetch } from './file-fetch';
|
import { FileFetch } from './file-fetch';
|
||||||
import { FlagdCore } from '@openfeature/flagd-core';
|
import { FlagdCore } from '@openfeature/flagd-core';
|
||||||
import { Logger } from '@openfeature/server-sdk';
|
import type { Logger } from '@openfeature/server-sdk';
|
||||||
|
|
||||||
jest.mock('fs', () => ({
|
jest.mock('fs', () => ({
|
||||||
...jest.requireActual('fs'),
|
...jest.requireActual('fs'),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Logger, OpenFeatureError, GeneralError } from '@openfeature/server-sdk';
|
import type { Logger } from '@openfeature/server-sdk';
|
||||||
import { DataFetch } from '../data-fetch';
|
import { OpenFeatureError, GeneralError } from '@openfeature/server-sdk';
|
||||||
|
import type { DataFetch } from '../data-fetch';
|
||||||
import { promises as fsPromises, watchFile, unwatchFile } from 'fs';
|
import { promises as fsPromises, watchFile, unwatchFile } from 'fs';
|
||||||
|
|
||||||
const encoding = 'utf8';
|
const encoding = 'utf8';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { GrpcFetch } from './grpc-fetch';
|
import { GrpcFetch } from './grpc-fetch';
|
||||||
import { Config } from '../../../configuration';
|
import type { Config } from '../../../configuration';
|
||||||
import { FlagSyncServiceClient, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
|
import type { FlagSyncServiceClient, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
|
||||||
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
|
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
|
||||||
|
|
||||||
let watchStateCallback: () => void = () => ({});
|
let watchStateCallback: () => void = () => ({});
|
||||||
|
@ -44,7 +44,14 @@ const serviceMock: FlagSyncServiceClient = {
|
||||||
} as unknown as FlagSyncServiceClient;
|
} as unknown as FlagSyncServiceClient;
|
||||||
|
|
||||||
describe('grpc fetch', () => {
|
describe('grpc fetch', () => {
|
||||||
const cfg: Config = { host: 'localhost', port: 8000, tls: false, socketPath: '', defaultAuthority: 'test-authority' };
|
const cfg: Config = {
|
||||||
|
deadlineMs: 500,
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8000,
|
||||||
|
tls: false,
|
||||||
|
socketPath: '',
|
||||||
|
defaultAuthority: 'test-authority',
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { ClientReadableStream, ServiceError, credentials, ClientOptions } from '@grpc/grpc-js';
|
import type { ClientReadableStream, ServiceError, ClientOptions } from '@grpc/grpc-js';
|
||||||
import { GeneralError, Logger } from '@openfeature/server-sdk';
|
import { credentials } from '@grpc/grpc-js';
|
||||||
import { FlagSyncServiceClient, SyncFlagsRequest, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
|
import type { Logger } from '@openfeature/server-sdk';
|
||||||
import { Config } from '../../../configuration';
|
import { GeneralError } from '@openfeature/server-sdk';
|
||||||
|
import type { SyncFlagsRequest, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
|
||||||
|
import { FlagSyncServiceClient } from '../../../../proto/ts/flagd/sync/v1/sync';
|
||||||
|
import type { Config } from '../../../configuration';
|
||||||
import { closeStreamIfDefined } from '../../common';
|
import { closeStreamIfDefined } from '../../common';
|
||||||
import { DataFetch } from '../data-fetch';
|
import type { DataFetch } from '../data-fetch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the gRPC sync contract to fetch flag data.
|
* Implements the gRPC sync contract to fetch flag data.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { DataFetch } from './data-fetch';
|
import type { DataFetch } from './data-fetch';
|
||||||
import { InProcessService } from './in-process-service';
|
import { InProcessService } from './in-process-service';
|
||||||
|
|
||||||
describe('In-process-service', () => {
|
describe('In-process-service', () => {
|
||||||
|
@ -13,7 +13,7 @@ describe('In-process-service', () => {
|
||||||
|
|
||||||
it('should sync and allow to resolve flags', async () => {
|
it('should sync and allow to resolve flags', async () => {
|
||||||
// given
|
// given
|
||||||
const service = new InProcessService({ host: '', port: 0, tls: false }, dataFetcher);
|
const service = new InProcessService({ deadlineMs: 500, host: '', port: 0, tls: false }, dataFetcher);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await service.connect(jest.fn, jest.fn, jest.fn);
|
await service.connect(jest.fn, jest.fn, jest.fn);
|
||||||
|
@ -31,7 +31,7 @@ describe('In-process-service', () => {
|
||||||
it('should include scope as flag metadata', async () => {
|
it('should include scope as flag metadata', async () => {
|
||||||
// given
|
// given
|
||||||
const selector = 'devFlags';
|
const selector = 'devFlags';
|
||||||
const service = new InProcessService({ host: '', port: 0, tls: false, selector }, dataFetcher);
|
const service = new InProcessService({ deadlineMs: 500, host: '', port: 0, tls: false, selector }, dataFetcher);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await service.connect(jest.fn, jest.fn, jest.fn);
|
await service.connect(jest.fn, jest.fn, jest.fn);
|
||||||
|
@ -44,7 +44,7 @@ describe('In-process-service', () => {
|
||||||
it('should not override existing scope in flag metadata', async () => {
|
it('should not override existing scope in flag metadata', async () => {
|
||||||
// given
|
// given
|
||||||
const selector = 'devFlags';
|
const selector = 'devFlags';
|
||||||
const service = new InProcessService({ host: '', port: 0, tls: false, selector }, dataFetcher);
|
const service = new InProcessService({ deadlineMs: 500, host: '', port: 0, tls: false, selector }, dataFetcher);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await service.connect(jest.fn, jest.fn, jest.fn);
|
await service.connect(jest.fn, jest.fn, jest.fn);
|
||||||
|
|
|
@ -7,9 +7,9 @@ import type {
|
||||||
Logger,
|
Logger,
|
||||||
ResolutionDetails,
|
ResolutionDetails,
|
||||||
} from '@openfeature/server-sdk';
|
} from '@openfeature/server-sdk';
|
||||||
import { Config } from '../../configuration';
|
import type { Config } from '../../configuration';
|
||||||
import { Service } from '../service';
|
import type { Service } from '../service';
|
||||||
import { DataFetch } from './data-fetch';
|
import type { DataFetch } from './data-fetch';
|
||||||
import { FileFetch } from './file/file-fetch';
|
import { FileFetch } from './file/file-fetch';
|
||||||
import { GrpcFetch } from './grpc/grpc-fetch';
|
import { GrpcFetch } from './grpc/grpc-fetch';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
|
import type { EvaluationContext, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
|
||||||
|
|
||||||
export interface Service {
|
export interface Service {
|
||||||
connect(
|
connect(
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import {
|
import type {
|
||||||
EvaluationContext,
|
EvaluationContext,
|
||||||
FlagValue,
|
FlagValue,
|
||||||
JsonValue,
|
JsonValue,
|
||||||
Logger,
|
Logger,
|
||||||
OpenFeatureEventEmitter,
|
|
||||||
Provider,
|
Provider,
|
||||||
ProviderEvents,
|
|
||||||
ProviderMetadata,
|
ProviderMetadata,
|
||||||
ResolutionDetails,
|
ResolutionDetails,
|
||||||
ResolutionReason,
|
ResolutionReason,
|
||||||
TypeMismatchError,
|
|
||||||
} from '@openfeature/web-sdk';
|
} from '@openfeature/web-sdk';
|
||||||
|
import { OpenFeatureEventEmitter, ProviderEvents, TypeMismatchError } from '@openfeature/web-sdk';
|
||||||
import { createFlagsmithInstance } from 'flagsmith';
|
import { createFlagsmithInstance } from 'flagsmith';
|
||||||
import { IFlagsmith, IInitConfig, IState } from 'flagsmith/types';
|
import type { IFlagsmith, IInitConfig, IState } from 'flagsmith/types';
|
||||||
import { FlagType, typeFactory } from './type-factory';
|
import type { FlagType } from './type-factory';
|
||||||
|
import { typeFactory } from './type-factory';
|
||||||
|
|
||||||
export class FlagsmithClientProvider implements Provider {
|
export class FlagsmithClientProvider implements Provider {
|
||||||
readonly metadata: ProviderMetadata = {
|
readonly metadata: ProviderMetadata = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IFlagsmithResponse, IInitConfig } from 'flagsmith/types';
|
import type { IFlagsmithResponse, IInitConfig } from 'flagsmith/types';
|
||||||
type Flatten<T> = T extends unknown[] ? T[number] : T;
|
type Flatten<T> = T extends unknown[] ? T[number] : T;
|
||||||
type FeatureResponse = Flatten<IFlagsmithResponse['flags']>;
|
type FeatureResponse = Flatten<IFlagsmithResponse['flags']>;
|
||||||
type Callback = (err: Error | null, val: string | null) => void;
|
type Callback = (err: Error | null, val: string | null) => void;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FlagValue } from '@openfeature/web-sdk';
|
import type { FlagValue } from '@openfeature/web-sdk';
|
||||||
|
|
||||||
export type FlagType = 'string' | 'number' | 'object' | 'boolean';
|
export type FlagType = 'string' | 'number' | 'object' | 'boolean';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,26 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.5](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.4...flipt-web-provider-v0.1.5) (2025-06-06)
|
||||||
|
|
||||||
|
|
||||||
|
### ✨ New Features
|
||||||
|
|
||||||
|
* **flipt-web:** update types to match flipt-client-js ([#1303](https://github.com/open-feature/js-sdk-contrib/issues/1303)) ([9e8496a](https://github.com/open-feature/js-sdk-contrib/commit/9e8496a384e65bee8d6cb096bdfaa909e2bfa311))
|
||||||
|
|
||||||
|
## [0.1.4](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.3...flipt-web-provider-v0.1.4) (2025-06-04)
|
||||||
|
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency @flipt-io/flipt-client-js to v0.0.2 ([#1272](https://github.com/open-feature/js-sdk-contrib/issues/1272)) ([d7f8746](https://github.com/open-feature/js-sdk-contrib/commit/d7f8746434f58333a2458418c35749c125932369))
|
||||||
|
|
||||||
|
## [0.1.3](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.2...flipt-web-provider-v0.1.3) (2025-04-10)
|
||||||
|
|
||||||
|
|
||||||
|
### 🧹 Chore
|
||||||
|
|
||||||
|
* **flipt:** swap underlying flipt web sdk ([#1244](https://github.com/open-feature/js-sdk-contrib/issues/1244)) ([dba2a28](https://github.com/open-feature/js-sdk-contrib/commit/dba2a280014e998341487fc2cb1fcb410275d8d6))
|
||||||
|
|
||||||
## [0.1.2](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.1...flipt-web-provider-v0.1.2) (2025-02-11)
|
## [0.1.2](https://github.com/open-feature/js-sdk-contrib/compare/flipt-web-provider-v0.1.1...flipt-web-provider-v0.1.2) (2025-02-11)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
[Flipt](https://www.flipt.io/) is an open source developer friendly feature flagging solution, that allows for easy management and fast feature evaluation.
|
[Flipt](https://www.flipt.io/) is an open source developer friendly feature flagging solution, that allows for easy management and fast feature evaluation.
|
||||||
|
|
||||||
This provider is an implementation on top of the official [Flipt Browser Client Side SDK](https://www.npmjs.com/package/@flipt-io/flipt-client-browser).
|
This provider is an implementation on top of the official [Flipt JavaScript Client Side SDK](https://www.npmjs.com/package/@flipt-io/flipt-client-js).
|
||||||
|
|
||||||
The main difference between this provider and [`@openfeature/flipt-provider`](https://www.npmjs.com/package/@openfeature/flipt-provider) is that it uses a **static evaluation context**.
|
The main difference between this provider and [`@openfeature/flipt-provider`](https://www.npmjs.com/package/@openfeature/flipt-provider) is that it uses a **static evaluation context**.
|
||||||
This provider is more sustainable for client-side implementation.
|
This provider is more sustainable for client-side implementation.
|
||||||
|
|
|
@ -1,27 +1,30 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/flipt-web-provider",
|
"name": "@openfeature/flipt-web-provider",
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@openfeature/flipt-web-provider",
|
"name": "@openfeature/flipt-web-provider",
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"dependencies": {
|
"license": "Apache-2.0",
|
||||||
"@flipt-io/flipt-client-browser": "^0.3.1",
|
|
||||||
"tslib": "^2.3.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"undici": "^6.13.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@flipt-io/flipt-client-js": "^0.0.1 || ^0.0.2 || ^0.0.6 || ^0.2.0",
|
||||||
"@openfeature/web-sdk": "^1.0.0"
|
"@openfeature/web-sdk": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@flipt-io/flipt-client-browser": {
|
"node_modules/@flipt-io/flipt-client-js": {
|
||||||
"version": "0.3.1",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@flipt-io/flipt-client-browser/-/flipt-client-browser-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@flipt-io/flipt-client-js/-/flipt-client-js-0.2.0.tgz",
|
||||||
"integrity": "sha512-1MFuQuHRENnzVooxrfQjFBLNBfE5uGBJmF2NuPFXTYMZn+sGelFovuNVuKlHqegI3Dqzz9Al2qJlkeFo+MhHxg=="
|
"integrity": "sha512-3VIU2pDUyHafp3ahgxHOde81fKP+5hvSm9Q+8hqt9+D7MCIWNvHTLqFo5hc87WSgq3TlR8ILRWfFC18BE1AmeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^3.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@openfeature/core": {
|
"node_modules/@openfeature/core": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
|
@ -38,18 +41,94 @@
|
||||||
"@openfeature/core": "1.4.0"
|
"@openfeature/core": "1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "2.8.0",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
|
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||||
},
|
"peer": true,
|
||||||
"node_modules/undici": {
|
|
||||||
"version": "6.20.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz",
|
|
||||||
"integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fetch-blob": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-domexception": "^1.0.0",
|
||||||
|
"web-streams-polyfill": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20 || >= 14.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/formdata-polyfill": {
|
||||||
|
"version": "4.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
|
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fetch-blob": "^3.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-domexception": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"data-uri-to-buffer": "^4.0.0",
|
||||||
|
"fetch-blob": "^3.1.4",
|
||||||
|
"formdata-polyfill": "^4.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/node-fetch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/flipt-web-provider",
|
"name": "@openfeature/flipt-web-provider",
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
"typings": "./src/index.d.ts",
|
"typings": "./src/index.d.ts",
|
||||||
|
@ -9,13 +9,7 @@
|
||||||
"current-version": "echo $npm_package_version"
|
"current-version": "echo $npm_package_version"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@openfeature/web-sdk": "^1.0.0"
|
"@openfeature/web-sdk": "^1.0.0",
|
||||||
},
|
"@flipt-io/flipt-client-js": "^0.0.1 || ^0.0.2 || ^0.0.6 || ^0.2.0"
|
||||||
"devDependencies": {
|
|
||||||
"undici": "^6.13.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@flipt-io/flipt-client-browser": "^0.3.1",
|
|
||||||
"undici": "^5.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export * from './lib/flipt-web-provider';
|
export * from './lib/flipt-web-provider';
|
||||||
|
export * from './lib/models';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EvaluationContext } from '@openfeature/server-sdk';
|
import type { EvaluationContext } from '@openfeature/server-sdk';
|
||||||
import { transformContext } from './context-transformer';
|
import { transformContext } from './context-transformer';
|
||||||
|
|
||||||
describe('context-transformer', () => {
|
describe('context-transformer', () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EvaluationContext } from '@openfeature/web-sdk';
|
import type { EvaluationContext } from '@openfeature/web-sdk';
|
||||||
|
|
||||||
export function transformContext(context: EvaluationContext): Record<string, string> {
|
export function transformContext(context: EvaluationContext): Record<string, string> {
|
||||||
const evalContext: Record<string, string> = {};
|
const evalContext: Record<string, string> = {};
|
||||||
|
|
|
@ -28,12 +28,12 @@ describe('FliptWebProvider', () => {
|
||||||
describe('method resolveStringEvaluation', () => {
|
describe('method resolveStringEvaluation', () => {
|
||||||
it('should throw general error for non-existent flag', () => {
|
it('should throw general error for non-existent flag', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
provider.resolveStringEvaluation('nonExistent', 'default', { fizz: 'buzz' });
|
provider.resolveStringEvaluation('nonExistent', 'default', { targetingKey: '1234', fizz: 'buzz' });
|
||||||
}).toThrow(GeneralError);
|
}).toThrow(GeneralError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return right value if key exists', () => {
|
it('should return right value if key exists', () => {
|
||||||
const value = provider.resolveStringEvaluation('flag_string', 'default', { fizz: 'buzz' });
|
const value = provider.resolveStringEvaluation('flag_string', 'default', { targetingKey: '1234', fizz: 'buzz' });
|
||||||
expect(value).toHaveProperty('value', 'variant1');
|
expect(value).toHaveProperty('value', 'variant1');
|
||||||
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
|
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
|
||||||
});
|
});
|
||||||
|
@ -42,12 +42,12 @@ describe('FliptWebProvider', () => {
|
||||||
describe('method resolveNumberEvaluation', () => {
|
describe('method resolveNumberEvaluation', () => {
|
||||||
it('should throw general error for non-existent flag', () => {
|
it('should throw general error for non-existent flag', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
provider.resolveNumberEvaluation('nonExistent', 1, { fizz: 'buzz' });
|
provider.resolveNumberEvaluation('nonExistent', 1, { targetingKey: '1234', fizz: 'buzz' });
|
||||||
}).toThrow(GeneralError);
|
}).toThrow(GeneralError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return right value if key exists', () => {
|
it('should return right value if key exists', () => {
|
||||||
const value = provider.resolveNumberEvaluation('flag_number', 0, { fizz: 'buzz' });
|
const value = provider.resolveNumberEvaluation('flag_number', 0, { targetingKey: '1234', fizz: 'buzz' });
|
||||||
expect(value).toHaveProperty('value', 5);
|
expect(value).toHaveProperty('value', 5);
|
||||||
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
|
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
|
||||||
});
|
});
|
||||||
|
@ -56,12 +56,12 @@ describe('FliptWebProvider', () => {
|
||||||
describe('method resolveBooleanEvaluation', () => {
|
describe('method resolveBooleanEvaluation', () => {
|
||||||
it('should throw general error for non-existent flag', () => {
|
it('should throw general error for non-existent flag', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
provider.resolveBooleanEvaluation('nonExistent', false, { fizz: 'buzz' });
|
provider.resolveBooleanEvaluation('nonExistent', false, { targetingKey: '1234', fizz: 'buzz' });
|
||||||
}).toThrow(GeneralError);
|
}).toThrow(GeneralError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return right value if key exists', () => {
|
it('should return right value if key exists', () => {
|
||||||
const value = provider.resolveBooleanEvaluation('flag_boolean', false, { fizz: 'buzz' });
|
const value = provider.resolveBooleanEvaluation('flag_boolean', false, { targetingKey: '1234', fizz: 'buzz' });
|
||||||
expect(value).toHaveProperty('value', true);
|
expect(value).toHaveProperty('value', true);
|
||||||
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
|
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
|
||||||
});
|
});
|
||||||
|
@ -70,12 +70,16 @@ describe('FliptWebProvider', () => {
|
||||||
describe('method resolveObjectEvaluation', () => {
|
describe('method resolveObjectEvaluation', () => {
|
||||||
it('should throw general error for non-existent flag', () => {
|
it('should throw general error for non-existent flag', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
provider.resolveObjectEvaluation('nonExistent', {}, { fizz: 'buzz' });
|
provider.resolveObjectEvaluation('nonExistent', {}, { targetingKey: '1234', fizz: 'buzz' });
|
||||||
}).toThrow(GeneralError);
|
}).toThrow(GeneralError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return right value if key exists', () => {
|
it('should return right value if key exists', () => {
|
||||||
const value = provider.resolveObjectEvaluation('flag_object', { fizz: 'buzz' }, { fizz: 'buzz' });
|
const value = provider.resolveObjectEvaluation(
|
||||||
|
'flag_object',
|
||||||
|
{ fizz: 'buzz' },
|
||||||
|
{ targetingKey: '1234', fizz: 'buzz' },
|
||||||
|
);
|
||||||
expect(value).toHaveProperty('value', { foo: 'bar' });
|
expect(value).toHaveProperty('value', { foo: 'bar' });
|
||||||
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
|
expect(value).toHaveProperty('reason', 'TARGETING_MATCH');
|
||||||
});
|
});
|
||||||
|
@ -83,7 +87,7 @@ describe('FliptWebProvider', () => {
|
||||||
|
|
||||||
it('should throw TypeMismatchError on non-number value', () => {
|
it('should throw TypeMismatchError on non-number value', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
provider.resolveNumberEvaluation('flag_string', 0, { fizz: 'buzz' });
|
provider.resolveNumberEvaluation('flag_string', 0, { targetingKey: '1234', fizz: 'buzz' });
|
||||||
}).toThrow(TypeMismatchError);
|
}).toThrow(TypeMismatchError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,8 @@
|
||||||
import {
|
import type { EvaluationContext, Provider, JsonValue, ResolutionDetails, Logger } from '@openfeature/web-sdk';
|
||||||
EvaluationContext,
|
import { StandardResolutionReasons, TypeMismatchError, GeneralError, ProviderFatalError } from '@openfeature/web-sdk';
|
||||||
Provider,
|
import { FliptClient } from '@flipt-io/flipt-client-js/browser';
|
||||||
JsonValue,
|
import type { FliptWebProviderOptions } from './models';
|
||||||
ResolutionDetails,
|
import { EvaluationReason } from './models';
|
||||||
Logger,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
TypeMismatchError,
|
|
||||||
GeneralError,
|
|
||||||
ProviderFatalError,
|
|
||||||
} from '@openfeature/web-sdk';
|
|
||||||
import { FliptEvaluationClient } from '@flipt-io/flipt-client-browser';
|
|
||||||
import { EvaluationReason, FliptWebProviderOptions } from './models';
|
|
||||||
import { transformContext } from './context-transformer';
|
import { transformContext } from './context-transformer';
|
||||||
|
|
||||||
export class FliptWebProvider implements Provider {
|
export class FliptWebProvider implements Provider {
|
||||||
|
@ -28,7 +20,7 @@ export class FliptWebProvider implements Provider {
|
||||||
private _options?: FliptWebProviderOptions;
|
private _options?: FliptWebProviderOptions;
|
||||||
|
|
||||||
// client is the Flipt client reference
|
// client is the Flipt client reference
|
||||||
private _client?: FliptEvaluationClient;
|
private _client?: FliptClient;
|
||||||
|
|
||||||
readonly runsOn = 'client';
|
readonly runsOn = 'client';
|
||||||
|
|
||||||
|
@ -48,7 +40,8 @@ export class FliptWebProvider implements Provider {
|
||||||
|
|
||||||
async initializeClient() {
|
async initializeClient() {
|
||||||
try {
|
try {
|
||||||
this._client = await FliptEvaluationClient.init(this._namespace || 'default', {
|
this._client = await FliptClient.init({
|
||||||
|
namespace: this._namespace || 'default',
|
||||||
url: this._options?.url || 'http://localhost:8080',
|
url: this._options?.url || 'http://localhost:8080',
|
||||||
fetcher: this._options?.fetcher,
|
fetcher: this._options?.fetcher,
|
||||||
authentication: this._options?.authentication,
|
authentication: this._options?.authentication,
|
||||||
|
@ -70,7 +63,11 @@ export class FliptWebProvider implements Provider {
|
||||||
const evalContext: Record<string, string> = transformContext(context);
|
const evalContext: Record<string, string> = transformContext(context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = this._client?.evaluateBoolean(flagKey, context.targetingKey ?? '', evalContext);
|
const result = this._client?.evaluateBoolean({
|
||||||
|
flagKey,
|
||||||
|
entityId: context.targetingKey ?? '',
|
||||||
|
context: evalContext,
|
||||||
|
});
|
||||||
|
|
||||||
switch (result?.reason) {
|
switch (result?.reason) {
|
||||||
case EvaluationReason.DEFAULT:
|
case EvaluationReason.DEFAULT:
|
||||||
|
@ -135,7 +132,11 @@ export class FliptWebProvider implements Provider {
|
||||||
const evalContext: Record<string, string> = transformContext(context);
|
const evalContext: Record<string, string> = transformContext(context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = this._client?.evaluateVariant(flagKey, context.targetingKey ?? '', evalContext);
|
const result = this._client?.evaluateVariant({
|
||||||
|
flagKey,
|
||||||
|
entityId: context.targetingKey ?? '',
|
||||||
|
context: evalContext,
|
||||||
|
});
|
||||||
|
|
||||||
if (result?.reason === EvaluationReason.FLAG_DISABLED) {
|
if (result?.reason === EvaluationReason.FLAG_DISABLED) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
export interface FliptWebProviderOptions {
|
export interface FliptWebProviderOptions {
|
||||||
url?: string;
|
url?: string;
|
||||||
authentication?: FliptWebProviderAuthentication;
|
authentication?: FliptWebProviderAuthentication;
|
||||||
fetcher?: () => Promise<Response>;
|
fetcher?: FliptFetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FliptFetcher = (args?: FliptFetcherOptions) => Promise<Response>;
|
||||||
|
|
||||||
|
export interface FliptFetcherOptions {
|
||||||
|
etag?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FliptClientTokenAuthentication {
|
export interface FliptClientTokenAuthentication {
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.3](https://github.com/open-feature/js-sdk-contrib/compare/flipt-provider-v0.1.2...flipt-provider-v0.1.3) (2025-04-10)
|
||||||
|
|
||||||
|
|
||||||
|
### 🧹 Chore
|
||||||
|
|
||||||
|
* update nx packages ([#1147](https://github.com/open-feature/js-sdk-contrib/issues/1147)) ([7f310fe](https://github.com/open-feature/js-sdk-contrib/commit/7f310fe87101b8aa793e1436e63c7602ccc202e3))
|
||||||
|
|
||||||
## [0.1.2](https://github.com/open-feature/js-sdk-contrib/compare/flipt-provider-v0.1.1...flipt-provider-v0.1.2) (2024-10-23)
|
## [0.1.2](https://github.com/open-feature/js-sdk-contrib/compare/flipt-provider-v0.1.1...flipt-provider-v0.1.2) (2024-10-23)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/flipt-provider",
|
"name": "@openfeature/flipt-provider",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@openfeature/flipt-provider",
|
"name": "@openfeature/flipt-provider",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/flipt-provider",
|
"name": "@openfeature/flipt-provider",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
"typings": "./src/index.d.ts",
|
"typings": "./src/index.d.ts",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EvaluationContext } from '@openfeature/server-sdk';
|
import type { EvaluationContext } from '@openfeature/server-sdk';
|
||||||
import { transformContext } from './context-transformer';
|
import { transformContext } from './context-transformer';
|
||||||
|
|
||||||
describe('context-transformer', () => {
|
describe('context-transformer', () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EvaluationContext } from '@openfeature/server-sdk';
|
import type { EvaluationContext } from '@openfeature/server-sdk';
|
||||||
|
|
||||||
export function transformContext(context: EvaluationContext): Record<string, string> {
|
export function transformContext(context: EvaluationContext): Record<string, string> {
|
||||||
const evalContext: Record<string, string> = {};
|
const evalContext: Record<string, string> = {};
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { AuthenticationStrategy, FliptClient } from '@flipt-io/flipt';
|
import type { AuthenticationStrategy } from '@flipt-io/flipt';
|
||||||
import {
|
import { FliptClient } from '@flipt-io/flipt';
|
||||||
|
import type {
|
||||||
EvaluationContext,
|
EvaluationContext,
|
||||||
Provider,
|
Provider,
|
||||||
JsonValue,
|
JsonValue,
|
||||||
ResolutionDetails,
|
ResolutionDetails,
|
||||||
ProviderStatus,
|
|
||||||
ProviderMetadata,
|
ProviderMetadata,
|
||||||
|
} from '@openfeature/server-sdk';
|
||||||
|
import {
|
||||||
|
ProviderStatus,
|
||||||
TypeMismatchError,
|
TypeMismatchError,
|
||||||
ProviderNotReadyError,
|
ProviderNotReadyError,
|
||||||
StandardResolutionReasons,
|
StandardResolutionReasons,
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.2.6](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-web-provider-v0.2.5...go-feature-flag-web-provider-v0.2.6) (2025-05-26)
|
||||||
|
|
||||||
|
|
||||||
|
### ✨ New Features
|
||||||
|
|
||||||
|
* **goff web:** Support tracking events ([#1268](https://github.com/open-feature/js-sdk-contrib/issues/1268)) ([d9ffcec](https://github.com/open-feature/js-sdk-contrib/commit/d9ffcec1652aa96eefccc45dfe079ca126d55142))
|
||||||
|
|
||||||
|
|
||||||
|
### 🧹 Chore
|
||||||
|
|
||||||
|
* **main:** release flipt-web-provider 0.1.3 ([#1253](https://github.com/open-feature/js-sdk-contrib/issues/1253)) ([7be62ae](https://github.com/open-feature/js-sdk-contrib/commit/7be62ae45f4dfbaefecc6205a28060698fdd884d))
|
||||||
|
|
||||||
## [0.2.5](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-web-provider-v0.2.4...go-feature-flag-web-provider-v0.2.5) (2025-01-23)
|
## [0.2.5](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-web-provider-v0.2.4...go-feature-flag-web-provider-v0.2.5) (2025-01-23)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/go-feature-flag-web-provider",
|
"name": "@openfeature/go-feature-flag-web-provider",
|
||||||
"version": "0.2.5",
|
"version": "0.2.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@openfeature/go-feature-flag-web-provider",
|
"name": "@openfeature/go-feature-flag-web-provider",
|
||||||
"version": "0.2.5",
|
"version": "0.2.6",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@openfeature/web-sdk": "*"
|
"@openfeature/web-sdk": "*"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/go-feature-flag-web-provider",
|
"name": "@openfeature/go-feature-flag-web-provider",
|
||||||
"version": "0.2.5",
|
"version": "0.2.6",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"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",
|
"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",
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
import type { Logger } from '@openfeature/web-sdk';
|
||||||
|
import type { ExporterMetadataValue, FeatureEvent, GoFeatureFlagWebProviderOptions, TrackingEvent } from './model';
|
||||||
|
import { GoffApiController } from './controller/goff-api';
|
||||||
|
import { CollectorError } from './errors/collector-error';
|
||||||
|
import { copy } from 'copy-anything';
|
||||||
|
|
||||||
|
type Timer = ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
export class CollectorManager {
|
||||||
|
// bgSchedulerId contains the id of the setInterval that is running.
|
||||||
|
private bgScheduler?: Timer;
|
||||||
|
// dataCollectorBuffer contains all the FeatureEvents that we need to send to the relay-proxy for data collection.
|
||||||
|
private dataCollectorBuffer?: Array<FeatureEvent<any> | TrackingEvent>;
|
||||||
|
// dataFlushInterval interval time (in millisecond) we use to call the relay proxy to collect data.
|
||||||
|
private readonly dataFlushInterval: number;
|
||||||
|
// logger is the Open Feature logger to use
|
||||||
|
private logger?: Logger;
|
||||||
|
// dataCollectorMetadata are the metadata used when calling the data collector endpoint
|
||||||
|
private readonly dataCollectorMetadata: Record<string, ExporterMetadataValue>;
|
||||||
|
|
||||||
|
private readonly goffApiController: GoffApiController;
|
||||||
|
|
||||||
|
constructor(options: GoFeatureFlagWebProviderOptions, logger?: Logger) {
|
||||||
|
this.dataFlushInterval = options.dataFlushInterval || 1000 * 60;
|
||||||
|
this.logger = logger;
|
||||||
|
this.goffApiController = new GoffApiController(options);
|
||||||
|
|
||||||
|
this.dataCollectorMetadata = {
|
||||||
|
provider: 'web',
|
||||||
|
openfeature: true,
|
||||||
|
...options.exporterMetadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bgScheduler = setInterval(async () => await this.callGoffDataCollection(), this.dataFlushInterval);
|
||||||
|
this.dataCollectorBuffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
clearInterval(this.bgScheduler);
|
||||||
|
// We call the data collector with what is still in the buffer.
|
||||||
|
await this.callGoffDataCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(event: FeatureEvent<any> | TrackingEvent) {
|
||||||
|
if (this.dataCollectorBuffer) {
|
||||||
|
this.dataCollectorBuffer.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* callGoffDataCollection is a function called periodically to send the usage of the flag to the
|
||||||
|
* central service in charge of collecting the data.
|
||||||
|
*/
|
||||||
|
async callGoffDataCollection() {
|
||||||
|
const dataToSend = copy(this.dataCollectorBuffer) || [];
|
||||||
|
this.dataCollectorBuffer = [];
|
||||||
|
try {
|
||||||
|
await this.goffApiController.collectData(dataToSend, this.dataCollectorMetadata);
|
||||||
|
} catch (e) {
|
||||||
|
if (!(e instanceof CollectorError)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
this.logger?.error(e);
|
||||||
|
// if we have an issue calling the collector, we put the data back in the buffer
|
||||||
|
this.dataCollectorBuffer = [...this.dataCollectorBuffer, ...dataToSend];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { GoFeatureFlagEvaluationContext } from './model';
|
import type { GoFeatureFlagEvaluationContext } from './model';
|
||||||
import { transformContext } from './context-transformer';
|
import { transformContext } from './context-transformer';
|
||||||
import { TargetingKeyMissingError, EvaluationContext } from '@openfeature/web-sdk';
|
import type { EvaluationContext } from '@openfeature/web-sdk';
|
||||||
|
import { TargetingKeyMissingError } from '@openfeature/web-sdk';
|
||||||
|
|
||||||
describe('contextTransformer', () => {
|
describe('contextTransformer', () => {
|
||||||
it('should use the targetingKey as user key', () => {
|
it('should use the targetingKey as user key', () => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { GoFeatureFlagEvaluationContext } from './model';
|
import type { GoFeatureFlagEvaluationContext } from './model';
|
||||||
import { TargetingKeyMissingError, EvaluationContext } from '@openfeature/web-sdk';
|
import type { EvaluationContext } from '@openfeature/web-sdk';
|
||||||
|
import { TargetingKeyMissingError } from '@openfeature/web-sdk';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* transformContext takes the raw OpenFeature context returns a GoFeatureFlagEvaluationContext.
|
* transformContext takes the raw OpenFeature context returns a GoFeatureFlagEvaluationContext.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import fetchMock from 'fetch-mock-jest';
|
import fetchMock from 'fetch-mock-jest';
|
||||||
import { GoffApiController } from './goff-api';
|
import { GoffApiController } from './goff-api';
|
||||||
import { GoFeatureFlagWebProviderOptions } from '../model';
|
import type { GoFeatureFlagWebProviderOptions } from '../model';
|
||||||
|
|
||||||
describe('Collect Data API', () => {
|
describe('Collect Data API', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { DataCollectorRequest, ExporterMetadataValue, FeatureEvent, GoFeatureFlagWebProviderOptions } from '../model';
|
import type {
|
||||||
|
DataCollectorRequest,
|
||||||
|
ExporterMetadataValue,
|
||||||
|
FeatureEvent,
|
||||||
|
GoFeatureFlagWebProviderOptions,
|
||||||
|
TrackingEvent,
|
||||||
|
} from '../model';
|
||||||
import { CollectorError } from '../errors/collector-error';
|
import { CollectorError } from '../errors/collector-error';
|
||||||
|
|
||||||
export class GoffApiController {
|
export class GoffApiController {
|
||||||
|
@ -15,7 +21,10 @@ export class GoffApiController {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, ExporterMetadataValue>) {
|
async collectData(
|
||||||
|
events: Array<FeatureEvent<any> | TrackingEvent>,
|
||||||
|
dataCollectorMetadata: Record<string, ExporterMetadataValue>,
|
||||||
|
) {
|
||||||
if (events?.length === 0) {
|
if (events?.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +1,14 @@
|
||||||
import { EvaluationDetails, FlagValue, Hook, HookContext, Logger } from '@openfeature/web-sdk';
|
import type { EvaluationDetails, FlagValue, Hook, HookContext } from '@openfeature/web-sdk';
|
||||||
import { ExporterMetadataValue, FeatureEvent, GoFeatureFlagWebProviderOptions } from './model';
|
import type { CollectorManager } from './collector-manager';
|
||||||
import { copy } from 'copy-anything';
|
|
||||||
import { CollectorError } from './errors/collector-error';
|
|
||||||
import { GoffApiController } from './controller/goff-api';
|
|
||||||
|
|
||||||
const defaultTargetingKey = 'undefined-targetingKey';
|
const defaultTargetingKey = 'undefined-targetingKey';
|
||||||
type Timer = ReturnType<typeof setInterval>;
|
type Timer = ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
export class GoFeatureFlagDataCollectorHook implements Hook {
|
export class GoFeatureFlagDataCollectorHook implements Hook {
|
||||||
// bgSchedulerId contains the id of the setInterval that is running.
|
private collectorManagger?: CollectorManager;
|
||||||
private bgScheduler?: Timer;
|
|
||||||
// dataCollectorBuffer contains all the FeatureEvents that we need to send to the relay-proxy for data collection.
|
|
||||||
private dataCollectorBuffer?: FeatureEvent<any>[];
|
|
||||||
// dataFlushInterval interval time (in millisecond) we use to call the relay proxy to collect data.
|
|
||||||
private readonly dataFlushInterval: number;
|
|
||||||
// dataCollectorMetadata are the metadata used when calling the data collector endpoint
|
|
||||||
private readonly dataCollectorMetadata: Record<string, ExporterMetadataValue>;
|
|
||||||
private readonly goffApiController: GoffApiController;
|
|
||||||
// logger is the Open Feature logger to use
|
|
||||||
private logger?: Logger;
|
|
||||||
|
|
||||||
constructor(options: GoFeatureFlagWebProviderOptions, logger?: Logger) {
|
constructor(collectorManager: CollectorManager) {
|
||||||
this.dataFlushInterval = options.dataFlushInterval || 1000 * 60;
|
this.collectorManagger = collectorManager;
|
||||||
this.logger = logger;
|
|
||||||
this.goffApiController = new GoffApiController(options);
|
|
||||||
this.dataCollectorMetadata = {
|
|
||||||
provider: 'web',
|
|
||||||
openfeature: true,
|
|
||||||
...options.exporterMetadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.bgScheduler = setInterval(async () => await this.callGoffDataCollection(), this.dataFlushInterval);
|
|
||||||
this.dataCollectorBuffer = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
clearInterval(this.bgScheduler);
|
|
||||||
// We call the data collector with what is still in the buffer.
|
|
||||||
await this.callGoffDataCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* callGoffDataCollection is a function called periodically to send the usage of the flag to the
|
|
||||||
* central service in charge of collecting the data.
|
|
||||||
*/
|
|
||||||
async callGoffDataCollection() {
|
|
||||||
const dataToSend = copy(this.dataCollectorBuffer) || [];
|
|
||||||
this.dataCollectorBuffer = [];
|
|
||||||
try {
|
|
||||||
await this.goffApiController.collectData(dataToSend, this.dataCollectorMetadata);
|
|
||||||
} catch (e) {
|
|
||||||
if (!(e instanceof CollectorError)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
this.logger?.error(e);
|
|
||||||
// if we have an issue calling the collector, we put the data back in the buffer
|
|
||||||
this.dataCollectorBuffer = [...this.dataCollectorBuffer, ...dataToSend];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
after(hookContext: HookContext, evaluationDetails: EvaluationDetails<FlagValue>) {
|
after(hookContext: HookContext, evaluationDetails: EvaluationDetails<FlagValue>) {
|
||||||
|
@ -74,7 +23,7 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
|
||||||
userKey: hookContext.context.targetingKey || defaultTargetingKey,
|
userKey: hookContext.context.targetingKey || defaultTargetingKey,
|
||||||
source: 'PROVIDER_CACHE',
|
source: 'PROVIDER_CACHE',
|
||||||
};
|
};
|
||||||
this.dataCollectorBuffer?.push(event);
|
this.collectorManagger?.add(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(hookContext: HookContext) {
|
error(hookContext: HookContext) {
|
||||||
|
@ -89,6 +38,6 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
|
||||||
userKey: hookContext.context.targetingKey || defaultTargetingKey,
|
userKey: hookContext.context.targetingKey || defaultTargetingKey,
|
||||||
source: 'PROVIDER_CACHE',
|
source: 'PROVIDER_CACHE',
|
||||||
};
|
};
|
||||||
this.dataCollectorBuffer?.push(event);
|
this.collectorManagger?.add(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
import { GoFeatureFlagWebProvider } from './go-feature-flag-web-provider';
|
import { GoFeatureFlagWebProvider } from './go-feature-flag-web-provider';
|
||||||
import {
|
import type { EvaluationContext, EvaluationDetails, JsonValue } from '@openfeature/web-sdk';
|
||||||
ErrorCode,
|
import { ErrorCode, OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/web-sdk';
|
||||||
EvaluationContext,
|
|
||||||
EvaluationDetails,
|
|
||||||
JsonValue,
|
|
||||||
OpenFeature,
|
|
||||||
ProviderEvents,
|
|
||||||
StandardResolutionReasons,
|
|
||||||
} from '@openfeature/web-sdk';
|
|
||||||
import WS from 'jest-websocket-mock';
|
import WS from 'jest-websocket-mock';
|
||||||
import TestLogger from './test-logger';
|
import TestLogger from './test-logger';
|
||||||
import { DataCollectorRequest, GOFeatureFlagWebsocketResponse } from './model';
|
import type { DataCollectorRequest, GOFeatureFlagWebsocketResponse, TrackingEvent } from './model';
|
||||||
import fetchMock from 'fetch-mock-jest';
|
import fetchMock from 'fetch-mock-jest';
|
||||||
|
|
||||||
describe('GoFeatureFlagWebProvider', () => {
|
describe('GoFeatureFlagWebProvider', () => {
|
||||||
|
@ -456,156 +449,204 @@ describe('GoFeatureFlagWebProvider', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('data collector testing', () => {
|
describe('data collector testing', () => {
|
||||||
it('should call the data collector when closing Open Feature', async () => {
|
describe('tracking event', () => {
|
||||||
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
it('should send tracking event to the data collector', async () => {
|
||||||
await OpenFeature.setContext(defaultContext);
|
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
||||||
const p = new GoFeatureFlagWebProvider(
|
await OpenFeature.setContext(defaultContext);
|
||||||
{
|
const p = new GoFeatureFlagWebProvider(
|
||||||
endpoint: endpoint,
|
{
|
||||||
apiTimeout: 1000,
|
endpoint: endpoint,
|
||||||
maxRetries: 1,
|
apiTimeout: 1000,
|
||||||
dataFlushInterval: 10000,
|
maxRetries: 1,
|
||||||
apiKey: 'toto',
|
dataFlushInterval: 10000,
|
||||||
},
|
},
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
|
|
||||||
await OpenFeature.setProviderAndWait(clientName, p);
|
await OpenFeature.setProviderAndWait(clientName, p);
|
||||||
const client = OpenFeature.getClient(clientName);
|
const client = OpenFeature.getClient(clientName);
|
||||||
await websocketMockServer.connected;
|
await websocketMockServer.connected;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
|
||||||
client.getBooleanDetails('bool_flag', false);
|
client.getBooleanDetails('bool_flag', false);
|
||||||
client.getBooleanDetails('bool_flag', false);
|
client.getBooleanDetails('bool_flag', false);
|
||||||
|
client.track('event-key-123abc', { value: 99.77, currency: 'USD' });
|
||||||
|
|
||||||
await OpenFeature.close();
|
await OpenFeature.close();
|
||||||
|
|
||||||
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
|
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
|
||||||
expect(fetchMock.lastOptions(dataCollectorEndpoint)?.headers).toEqual({
|
const reqBody = fetchMock.lastOptions(dataCollectorEndpoint)?.body;
|
||||||
'Content-Type': 'application/json',
|
const parsedBody = JSON.parse(reqBody as never) as DataCollectorRequest<never>;
|
||||||
Accept: 'application/json',
|
expect(parsedBody.events.length).toBe(3);
|
||||||
Authorization: 'Bearer toto',
|
expect(parsedBody.events.filter((event) => event.kind === 'tracking').length).toBe(1);
|
||||||
|
expect(parsedBody.events.filter((event) => event.kind === 'feature').length).toBe(2);
|
||||||
|
|
||||||
|
const trackingEvent = parsedBody.events.find((event) => event.kind === 'tracking');
|
||||||
|
expect(trackingEvent).not.toBeUndefined();
|
||||||
|
const c = trackingEvent as TrackingEvent;
|
||||||
|
expect(c.key).toEqual('event-key-123abc');
|
||||||
|
expect(c.kind).toEqual('tracking');
|
||||||
|
expect(c.contextKind).toEqual('user');
|
||||||
|
expect(c.userKey).toEqual(defaultContext.targetingKey);
|
||||||
|
expect(c.creationDate).toBeGreaterThan(0);
|
||||||
|
expect(c.evaluationContext).toEqual(defaultContext);
|
||||||
|
expect(c.trackingEventDetails).toEqual({ value: 99.77, currency: 'USD' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the data collector when waiting more than the dataFlushInterval', async () => {
|
describe('feature event', () => {
|
||||||
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
it('should call the data collector when closing Open Feature', async () => {
|
||||||
await OpenFeature.setContext(defaultContext);
|
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
||||||
const p = new GoFeatureFlagWebProvider(
|
await OpenFeature.setContext(defaultContext);
|
||||||
{
|
const p = new GoFeatureFlagWebProvider(
|
||||||
endpoint: endpoint,
|
{
|
||||||
apiTimeout: 1000,
|
endpoint: endpoint,
|
||||||
maxRetries: 1,
|
apiTimeout: 1000,
|
||||||
dataFlushInterval: 200,
|
maxRetries: 1,
|
||||||
},
|
dataFlushInterval: 10000,
|
||||||
logger,
|
apiKey: 'toto',
|
||||||
);
|
},
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
await OpenFeature.setProviderAndWait(clientName, p);
|
await OpenFeature.setProviderAndWait(clientName, p);
|
||||||
const client = OpenFeature.getClient(clientName);
|
const client = OpenFeature.getClient(clientName);
|
||||||
await websocketMockServer.connected;
|
await websocketMockServer.connected;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
|
||||||
client.getBooleanDetails('bool_flag', false);
|
client.getBooleanDetails('bool_flag', false);
|
||||||
client.getBooleanDetails('bool_flag', false);
|
client.getBooleanDetails('bool_flag', false);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await OpenFeature.close();
|
||||||
|
|
||||||
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
|
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
|
||||||
expect(fetchMock.lastOptions(dataCollectorEndpoint)?.headers).toEqual({
|
expect(fetchMock.lastOptions(dataCollectorEndpoint)?.headers).toEqual({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
Authorization: 'Bearer toto',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await OpenFeature.close();
|
|
||||||
});
|
|
||||||
it('should call the data collector multiple time while waiting dataFlushInterval time', async () => {
|
|
||||||
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
|
||||||
await OpenFeature.setContext(defaultContext);
|
|
||||||
const p = new GoFeatureFlagWebProvider(
|
|
||||||
{
|
|
||||||
endpoint: endpoint,
|
|
||||||
apiTimeout: 1000,
|
|
||||||
maxRetries: 1,
|
|
||||||
dataFlushInterval: 200,
|
|
||||||
},
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
await OpenFeature.setProviderAndWait(clientName, p);
|
it('should call the data collector when waiting more than the dataFlushInterval', async () => {
|
||||||
const client = OpenFeature.getClient(clientName);
|
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
||||||
await websocketMockServer.connected;
|
await OpenFeature.setContext(defaultContext);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
const p = new GoFeatureFlagWebProvider(
|
||||||
client.getBooleanDetails('bool_flag', false);
|
{
|
||||||
client.getBooleanDetails('bool_flag', false);
|
endpoint: endpoint,
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
apiTimeout: 1000,
|
||||||
client.getBooleanDetails('bool_flag', false);
|
maxRetries: 1,
|
||||||
client.getBooleanDetails('bool_flag', false);
|
dataFlushInterval: 200,
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
},
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(2);
|
await OpenFeature.setProviderAndWait(clientName, p);
|
||||||
await OpenFeature.close();
|
const client = OpenFeature.getClient(clientName);
|
||||||
});
|
await websocketMockServer.connected;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
|
||||||
it('should not call the data collector before the dataFlushInterval', async () => {
|
client.getBooleanDetails('bool_flag', false);
|
||||||
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
client.getBooleanDetails('bool_flag', false);
|
||||||
await OpenFeature.setContext(defaultContext);
|
|
||||||
const p = new GoFeatureFlagWebProvider(
|
|
||||||
{
|
|
||||||
endpoint: endpoint,
|
|
||||||
apiTimeout: 1000,
|
|
||||||
maxRetries: 1,
|
|
||||||
dataFlushInterval: 200,
|
|
||||||
},
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
await OpenFeature.setProviderAndWait(clientName, p);
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
const client = OpenFeature.getClient(clientName);
|
|
||||||
await websocketMockServer.connected;
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
||||||
client.getBooleanDetails('bool_flag', false);
|
|
||||||
client.getBooleanDetails('bool_flag', false);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(0);
|
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
|
||||||
await OpenFeature.close();
|
expect(fetchMock.lastOptions(dataCollectorEndpoint)?.headers).toEqual({
|
||||||
});
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
});
|
||||||
|
await OpenFeature.close();
|
||||||
|
});
|
||||||
|
it('should call the data collector multiple time while waiting dataFlushInterval time', async () => {
|
||||||
|
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
||||||
|
await OpenFeature.setContext(defaultContext);
|
||||||
|
const p = new GoFeatureFlagWebProvider(
|
||||||
|
{
|
||||||
|
endpoint: endpoint,
|
||||||
|
apiTimeout: 1000,
|
||||||
|
maxRetries: 1,
|
||||||
|
dataFlushInterval: 200,
|
||||||
|
},
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
it('should have a log when data collector is not available', async () => {
|
await OpenFeature.setProviderAndWait(clientName, p);
|
||||||
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
const client = OpenFeature.getClient(clientName);
|
||||||
fetchMock.post(dataCollectorEndpoint, 500, { overwriteRoutes: true });
|
await websocketMockServer.connected;
|
||||||
await OpenFeature.setContext(defaultContext);
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
const p = new GoFeatureFlagWebProvider(
|
client.getBooleanDetails('bool_flag', false);
|
||||||
{
|
client.getBooleanDetails('bool_flag', false);
|
||||||
endpoint: endpoint,
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
apiTimeout: 1000,
|
client.getBooleanDetails('bool_flag', false);
|
||||||
maxRetries: 1,
|
client.getBooleanDetails('bool_flag', false);
|
||||||
dataFlushInterval: 200,
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
},
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
await OpenFeature.setProviderAndWait(clientName, p);
|
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(2);
|
||||||
const client = OpenFeature.getClient(clientName);
|
await OpenFeature.close();
|
||||||
await websocketMockServer.connected;
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
||||||
client.getBooleanDetails('bool_flag', false);
|
|
||||||
client.getBooleanDetails('bool_flag', false);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
||||||
|
|
||||||
fetchMock.post(dataCollectorEndpoint, 500, { overwriteRoutes: true });
|
it('should not call the data collector before the dataFlushInterval', async () => {
|
||||||
|
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
||||||
|
await OpenFeature.setContext(defaultContext);
|
||||||
|
const p = new GoFeatureFlagWebProvider(
|
||||||
|
{
|
||||||
|
endpoint: endpoint,
|
||||||
|
apiTimeout: 1000,
|
||||||
|
maxRetries: 1,
|
||||||
|
dataFlushInterval: 200,
|
||||||
|
},
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
client.getBooleanDetails('bool_flag', false);
|
await OpenFeature.setProviderAndWait(clientName, p);
|
||||||
client.getBooleanDetails('bool_flag', false);
|
const client = OpenFeature.getClient(clientName);
|
||||||
fetchMock.post(dataCollectorEndpoint, 200, { overwriteRoutes: true });
|
await websocketMockServer.connected;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
client.getBooleanDetails('bool_flag', false);
|
||||||
|
client.getBooleanDetails('bool_flag', false);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
const lastBody = fetchMock.lastOptions(dataCollectorEndpoint)?.body;
|
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(0);
|
||||||
const parsedBody = JSON.parse(lastBody as never);
|
await OpenFeature.close();
|
||||||
expect(parsedBody['events'].length).toBe(4);
|
});
|
||||||
await OpenFeature.close();
|
|
||||||
|
it('should have a log when data collector is not available', async () => {
|
||||||
|
const clientName = expect.getState().currentTestName ?? 'test-provider';
|
||||||
|
fetchMock.post(dataCollectorEndpoint, 500, { overwriteRoutes: true });
|
||||||
|
await OpenFeature.setContext(defaultContext);
|
||||||
|
const p = new GoFeatureFlagWebProvider(
|
||||||
|
{
|
||||||
|
endpoint: endpoint,
|
||||||
|
apiTimeout: 1000,
|
||||||
|
maxRetries: 1,
|
||||||
|
dataFlushInterval: 200,
|
||||||
|
},
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
|
await OpenFeature.setProviderAndWait(clientName, p);
|
||||||
|
const client = OpenFeature.getClient(clientName);
|
||||||
|
await websocketMockServer.connected;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
client.getBooleanDetails('bool_flag', false);
|
||||||
|
client.getBooleanDetails('bool_flag', false);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
|
||||||
|
fetchMock.post(dataCollectorEndpoint, 500, { overwriteRoutes: true });
|
||||||
|
|
||||||
|
client.getBooleanDetails('bool_flag', false);
|
||||||
|
client.getBooleanDetails('bool_flag', false);
|
||||||
|
fetchMock.post(dataCollectorEndpoint, 200, { overwriteRoutes: true });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
|
||||||
|
const lastBody = fetchMock.lastOptions(dataCollectorEndpoint)?.body;
|
||||||
|
const parsedBody = JSON.parse(lastBody as never);
|
||||||
|
expect(parsedBody['events'].length).toBe(4);
|
||||||
|
await OpenFeature.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve when WebSocket is open', async () => {
|
it('should resolve when WebSocket is open', async () => {
|
||||||
const provider = new GoFeatureFlagWebProvider({ endpoint: 'http://localhost:1031', apiTimeout: 1000 });
|
const provider = new GoFeatureFlagWebProvider({ endpoint: 'http://localhost:1031', apiTimeout: 1000 });
|
||||||
await provider.initialize({ targetingKey: 'user-key' });
|
await provider.initialize({ targetingKey: 'user-key' });
|
||||||
|
@ -658,7 +699,13 @@ describe('GoFeatureFlagWebProvider', () => {
|
||||||
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
|
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
|
||||||
const jsonBody = fetchMock.lastOptions(dataCollectorEndpoint)?.body;
|
const jsonBody = fetchMock.lastOptions(dataCollectorEndpoint)?.body;
|
||||||
const body = JSON.parse(jsonBody as never) as DataCollectorRequest<never>;
|
const body = JSON.parse(jsonBody as never) as DataCollectorRequest<never>;
|
||||||
expect(body.meta).toEqual({ browser: 'chrome', version: '1.0.0', score: 123, openfeature: true, provider: 'web' });
|
expect(body.meta).toEqual({
|
||||||
|
browser: 'chrome',
|
||||||
|
version: '1.0.0',
|
||||||
|
score: 123,
|
||||||
|
openfeature: true,
|
||||||
|
provider: 'web',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,32 @@
|
||||||
import {
|
import type {
|
||||||
EvaluationContext,
|
EvaluationContext,
|
||||||
FlagNotFoundError,
|
|
||||||
FlagValue,
|
FlagValue,
|
||||||
Hook,
|
Hook,
|
||||||
Logger,
|
Logger,
|
||||||
|
Provider,
|
||||||
|
ResolutionDetails,
|
||||||
|
TrackingEventDetails,
|
||||||
|
} from '@openfeature/web-sdk';
|
||||||
|
import {
|
||||||
|
FlagNotFoundError,
|
||||||
OpenFeature,
|
OpenFeature,
|
||||||
OpenFeatureEventEmitter,
|
OpenFeatureEventEmitter,
|
||||||
Provider,
|
|
||||||
ProviderEvents,
|
ProviderEvents,
|
||||||
ResolutionDetails,
|
|
||||||
StandardResolutionReasons,
|
StandardResolutionReasons,
|
||||||
TypeMismatchError,
|
TypeMismatchError,
|
||||||
} from '@openfeature/web-sdk';
|
} from '@openfeature/web-sdk';
|
||||||
import {
|
import type {
|
||||||
FlagState,
|
FlagState,
|
||||||
GoFeatureFlagAllFlagRequest,
|
GoFeatureFlagAllFlagRequest,
|
||||||
GOFeatureFlagAllFlagsResponse,
|
GOFeatureFlagAllFlagsResponse,
|
||||||
GoFeatureFlagWebProviderOptions,
|
GoFeatureFlagWebProviderOptions,
|
||||||
GOFeatureFlagWebsocketResponse,
|
GOFeatureFlagWebsocketResponse,
|
||||||
|
TrackingEvent,
|
||||||
} from './model';
|
} from './model';
|
||||||
import { transformContext } from './context-transformer';
|
import { transformContext } from './context-transformer';
|
||||||
import { FetchError } from './errors/fetch-error';
|
import { FetchError } from './errors/fetch-error';
|
||||||
import { GoFeatureFlagDataCollectorHook } from './data-collector-hook';
|
import { GoFeatureFlagDataCollectorHook } from './data-collector-hook';
|
||||||
|
import { CollectorManager } from './collector-manager';
|
||||||
|
|
||||||
export class GoFeatureFlagWebProvider implements Provider {
|
export class GoFeatureFlagWebProvider implements Provider {
|
||||||
metadata = {
|
metadata = {
|
||||||
|
@ -49,6 +54,8 @@ export class GoFeatureFlagWebProvider implements Provider {
|
||||||
private _websocket?: WebSocket;
|
private _websocket?: WebSocket;
|
||||||
// _flags is the in memory representation of all the flags.
|
// _flags is the in memory representation of all the flags.
|
||||||
private _flags: { [key: string]: ResolutionDetails<FlagValue> } = {};
|
private _flags: { [key: string]: ResolutionDetails<FlagValue> } = {};
|
||||||
|
|
||||||
|
private readonly _collectorManager: CollectorManager;
|
||||||
private readonly _dataCollectorHook: GoFeatureFlagDataCollectorHook;
|
private readonly _dataCollectorHook: GoFeatureFlagDataCollectorHook;
|
||||||
// disableDataCollection set to true if you don't want to collect the usage of flags retrieved in the cache.
|
// disableDataCollection set to true if you don't want to collect the usage of flags retrieved in the cache.
|
||||||
private readonly _disableDataCollection: boolean;
|
private readonly _disableDataCollection: boolean;
|
||||||
|
@ -62,13 +69,15 @@ export class GoFeatureFlagWebProvider implements Provider {
|
||||||
this._maxRetries = options.maxRetries || 10;
|
this._maxRetries = options.maxRetries || 10;
|
||||||
this._apiKey = options.apiKey;
|
this._apiKey = options.apiKey;
|
||||||
this._disableDataCollection = options.disableDataCollection || false;
|
this._disableDataCollection = options.disableDataCollection || false;
|
||||||
this._dataCollectorHook = new GoFeatureFlagDataCollectorHook(options, logger);
|
|
||||||
|
this._collectorManager = new CollectorManager(options, logger);
|
||||||
|
this._dataCollectorHook = new GoFeatureFlagDataCollectorHook(this._collectorManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(context: EvaluationContext): Promise<void> {
|
async initialize(context: EvaluationContext): Promise<void> {
|
||||||
if (!this._disableDataCollection && this._dataCollectorHook) {
|
if (!this._disableDataCollection && this._dataCollectorHook) {
|
||||||
this.hooks = [this._dataCollectorHook];
|
this.hooks = [this._dataCollectorHook];
|
||||||
this._dataCollectorHook.init();
|
this._collectorManager.init();
|
||||||
}
|
}
|
||||||
return Promise.all([this.fetchAll(context), this.connectWebsocket()])
|
return Promise.all([this.fetchAll(context), this.connectWebsocket()])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -157,8 +166,8 @@ export class GoFeatureFlagWebProvider implements Provider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClose(): Promise<void> {
|
async onClose(): Promise<void> {
|
||||||
if (!this._disableDataCollection && this._dataCollectorHook) {
|
if (!this._disableDataCollection && this._collectorManager) {
|
||||||
await this._dataCollectorHook?.close();
|
await this._collectorManager?.close();
|
||||||
}
|
}
|
||||||
this._websocket?.close(1000, 'Closing GO Feature Flag provider');
|
this._websocket?.close(1000, 'Closing GO Feature Flag provider');
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -187,6 +196,29 @@ export class GoFeatureFlagWebProvider implements Provider {
|
||||||
return this.evaluate(flagKey, 'boolean');
|
return this.evaluate(flagKey, 'boolean');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track allows to send tracking events to a tracking exporter.
|
||||||
|
*
|
||||||
|
* Warning: Note that you need to have a relay proxy with version 1.45.0 or upper to use this feature.
|
||||||
|
* If you are using a version lower than 1.45.0, the events may look weird in your exporter.
|
||||||
|
*
|
||||||
|
* @param trackingEventName
|
||||||
|
* @param context
|
||||||
|
* @param trackingEventDetails
|
||||||
|
*/
|
||||||
|
track(trackingEventName: string, context: EvaluationContext, trackingEventDetails: TrackingEventDetails): void {
|
||||||
|
const trackingEvent: TrackingEvent = {
|
||||||
|
kind: 'tracking',
|
||||||
|
contextKind: context['anonymous'] ? 'anonymousUser' : 'user',
|
||||||
|
creationDate: Math.round(Date.now() / 1000),
|
||||||
|
key: trackingEventName,
|
||||||
|
evaluationContext: context,
|
||||||
|
trackingEventDetails: trackingEventDetails,
|
||||||
|
userKey: context.targetingKey || 'undefined-targetingKey',
|
||||||
|
};
|
||||||
|
this._collectorManager?.add(trackingEvent);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* extract flag names from the websocket answer
|
* extract flag names from the websocket answer
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { ErrorCode, EvaluationContextValue, FlagValue } from '@openfeature/web-sdk';
|
import type {
|
||||||
|
ErrorCode,
|
||||||
|
EvaluationContext,
|
||||||
|
EvaluationContextValue,
|
||||||
|
FlagValue,
|
||||||
|
TrackingEventDetails,
|
||||||
|
} from '@openfeature/web-sdk';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GoFeatureFlagEvaluationContext is the representation of a user for GO Feature Flag
|
* GoFeatureFlagEvaluationContext is the representation of a user for GO Feature Flag
|
||||||
|
@ -106,7 +112,7 @@ export interface GOFeatureFlagWebsocketResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataCollectorRequest<T> {
|
export interface DataCollectorRequest<T> {
|
||||||
events: FeatureEvent<T>[];
|
events: Array<FeatureEvent<T> | TrackingEvent>;
|
||||||
meta: Record<string, ExporterMetadataValue>;
|
meta: Record<string, ExporterMetadataValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,3 +128,13 @@ export interface FeatureEvent<T> {
|
||||||
version?: string;
|
version?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TrackingEvent {
|
||||||
|
kind: string;
|
||||||
|
contextKind: string;
|
||||||
|
userKey: string;
|
||||||
|
creationDate: number;
|
||||||
|
key: string;
|
||||||
|
evaluationContext: EvaluationContext;
|
||||||
|
trackingEventDetails: TrackingEventDetails;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,25 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.7.8](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-provider-v0.7.7...go-feature-flag-provider-v0.7.8) (2025-05-26)
|
||||||
|
|
||||||
|
|
||||||
|
### ✨ New Features
|
||||||
|
|
||||||
|
* **gofeatureflag:** added cache option ([#1284](https://github.com/open-feature/js-sdk-contrib/issues/1284)) ([7083655](https://github.com/open-feature/js-sdk-contrib/commit/7083655c78eed957502f46dc202522a604024886))
|
||||||
|
|
||||||
|
## [0.7.7](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-provider-v0.7.6...go-feature-flag-provider-v0.7.7) (2025-04-11)
|
||||||
|
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency axios to v1.8.4 ([#1273](https://github.com/open-feature/js-sdk-contrib/issues/1273)) ([18a0cb9](https://github.com/open-feature/js-sdk-contrib/commit/18a0cb906a0c7e437b52f4645538a859d817ae18))
|
||||||
|
* **gofeatureflag:** Error for configurationHasChanged when slash was missing in endpoint ([#1229](https://github.com/open-feature/js-sdk-contrib/issues/1229)) ([ce6a8e1](https://github.com/open-feature/js-sdk-contrib/commit/ce6a8e1a800604b263ee76b305f5ca801aa2641d))
|
||||||
|
|
||||||
|
|
||||||
|
### 🧹 Chore
|
||||||
|
|
||||||
|
* **main:** release flipt-web-provider 0.1.3 ([#1253](https://github.com/open-feature/js-sdk-contrib/issues/1253)) ([7be62ae](https://github.com/open-feature/js-sdk-contrib/commit/7be62ae45f4dfbaefecc6205a28060698fdd884d))
|
||||||
|
|
||||||
## [0.7.6](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-provider-v0.7.5...go-feature-flag-provider-v0.7.6) (2025-03-12)
|
## [0.7.6](https://github.com/open-feature/js-sdk-contrib/compare/go-feature-flag-provider-v0.7.5...go-feature-flag-provider-v0.7.6) (2025-03-12)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openfeature/go-feature-flag-provider",
|
"name": "@openfeature/go-feature-flag-provider",
|
||||||
"version": "0.7.6",
|
"version": "0.7.8",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"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",
|
"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",
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"lru-cache": "^11.0.0",
|
"lru-cache": "^11.0.0",
|
||||||
"axios": "1.8.2",
|
"axios": "1.10.0",
|
||||||
"copy-anything": "^3.0.5"
|
"copy-anything": "^3.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue