Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ components:
libs/providers/flagd-web:
- beeme1mr
- toddbaert
libs/providers/flipt:
- markphelps
libs/providers/flipt-web:
- markphelps
libs/providers/go-feature-flag:
- thomaspoignant
libs/providers/go-feature-flag-web:
Expand All @@ -28,10 +32,8 @@ components:
- kinyoklion
- mateoc
- sago2k8
libs/providers/flipt:
- markphelps
libs/providers/flipt-web:
- markphelps
libs/providers/rocketflag:
- jgunnink
libs/providers/unleash-web:
- jarebudev

Expand Down
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"libs/shared/config-cat-core": "0.1.1",
"libs/providers/unleash-web": "0.1.1",
"libs/providers/growthbook": "0.1.2",
"libs/providers/aws-ssm": "0.1.3"
"libs/providers/aws-ssm": "0.1.3",
"libs/providers/rocketflag": "0.1.0"
}
30 changes: 30 additions & 0 deletions libs/providers/rocketflag/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"extends": "../../../.eslintrc.json",
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"]
}
]
}
}
]
}
15 changes: 15 additions & 0 deletions libs/providers/rocketflag/README.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some basic usage information, include a link to relevant Rocketflag pages, and mention limitations like that the provider only supports booleans. It doesn't have to be too elaborate but we typically direct folks to the readme from the OpenFeature Ecosystem page.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know thank you. I will update the readme soon.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# RocketFlag Provider

## Installation

```
$ npm install @openfeature/rocketflag-provider
```

## Building

Run `npx nx run RocketFlag:package` to build the library.

## Running unit tests

Run `npx nx run RocketFlag:test` to execute the unit tests via [Jest](https://jestjs.io) from the root of the repo.
3 changes: 3 additions & 0 deletions libs/providers/rocketflag/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": [["minify", { "builtIns": false }]]
}
9 changes: 9 additions & 0 deletions libs/providers/rocketflag/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
displayName: 'RocketFlag',
preset: '../../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/providers/rocketflag',
};
17 changes: 17 additions & 0 deletions libs/providers/rocketflag/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@openfeature/rocketflag-provider",
Copy link
Member

@lukas-reining lukas-reining Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you plan to add a Node provider, it would be good to include in the name that this is meant for web.
The conventions tends to be suffixing the name with -web. So in this case rocketflag-web-provider
You can check others here: https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers

"version": "0.1.0",
"dependencies": {
"tslib": "^2.3.0"
},
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"scripts": {
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
"current-version": "echo $npm_package_version"
},
"license": "Apache-2.0",
"peerDependencies": {
"@openfeature/web-sdk": "^1.6.0"
}
}
77 changes: 77 additions & 0 deletions libs/providers/rocketflag/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"name": "RocketFlag",
"$schema": "../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/providers/src",
"projectType": "library",
"release": {
"version": {
"generatorOptions": {
"packageRoot": "dist/{projectRoot}",
"currentVersionResolver": "git-tag"
}
}
},
"tags": [],
"targets": {
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "{projectRoot}/jest.config.ts"
}
},
"package": {
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "libs/providers/rocketflag/package.json",
"outputPath": "dist/libs/providers/rocketflag",
"entryFile": "libs/providers/rocketflag/src/index.ts",
"tsConfig": "libs/providers/rocketflag/tsconfig.lib.json",
"compiler": "tsc",
"generateExportsField": true,
"umdName": "RocketFlag",
"external": "all",
"format": ["cjs", "esm"],
"assets": [
{
"glob": "package.json",
"input": "./assets",
"output": "./src/"
},
{
"glob": "LICENSE",
"input": "./",
"output": "./"
},
{
"glob": "README.md",
"input": "./libs/providers/rocketflag",
"output": "./"
}
]
}
},
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "npm run publish-if-not-exists",
"cwd": "dist/libs/providers/rocketflag"
},
"dependsOn": [
{
"projects": "self",
"target": "package"
}
]
}
}
}
1 change: 1 addition & 0 deletions libs/providers/rocketflag/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/rocketflag-provider';
116 changes: 116 additions & 0 deletions libs/providers/rocketflag/src/lib/rocketflag-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { EvaluationContext } from '@openfeature/web-sdk';
import { OpenFeature, StandardResolutionReasons, ErrorCode } from '@openfeature/web-sdk';
import type { FlagStatus, UserContext } from './rocketflag-provider';
import { createRocketFlagProvider } from './rocketflag-provider';

// Create a mock RocketFlag client for testing
const mockClient = {
getFlag: jest.fn<Promise<FlagStatus>, [string, UserContext]>(),
};

// Mock Logger
const mockLogger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
};

describe('RocketFlagProvider', () => {
beforeEach(() => jest.clearAllMocks());

it('should have the correct metadata name', () => {
const provider = createRocketFlagProvider(mockClient);
expect(provider.metadata.name).toBe('RocketFlagProvider');
});

describe('resolveBooleanEvaluation', () => {
it('should return STALE initially, then resolve to the correct value with TARGETING_MATCH', async () => {
const provider = createRocketFlagProvider(mockClient);
const flagKey = 'test-flag-targeting';
const targetingContext: EvaluationContext = { targetingKey: '[email protected]' };

mockClient.getFlag.mockResolvedValue({ enabled: true });

const initialDetails = provider.resolveBooleanEvaluation(flagKey, false, targetingContext, mockLogger);
expect(initialDetails.reason).toBe(StandardResolutionReasons.STALE);
expect(initialDetails.value).toBe(false);

await new Promise((resolve) => setTimeout(resolve, 0));

const finalDetails = provider.resolveBooleanEvaluation(flagKey, false, targetingContext, mockLogger);

expect(finalDetails.value).toBe(true);
expect(finalDetails.reason).toBe(StandardResolutionReasons.TARGETING_MATCH);
expect(mockClient.getFlag).toHaveBeenCalledWith(flagKey, { cohort: '[email protected]' });
expect(mockClient.getFlag).toHaveBeenCalledTimes(2);
});

it('should return STALE initially, then resolve with DEFAULT reason when no targetingKey is provided', async () => {
const provider = createRocketFlagProvider(mockClient);
const flagKey = 'test-flag-default';

mockClient.getFlag.mockResolvedValue({ enabled: true });

const initialDetails = provider.resolveBooleanEvaluation(flagKey, false, {}, mockLogger);
expect(initialDetails.reason).toBe(StandardResolutionReasons.STALE);

await new Promise((resolve) => setTimeout(resolve, 0));

const finalDetails = provider.resolveBooleanEvaluation(flagKey, false, {}, mockLogger);

expect(finalDetails.value).toBe(true);
expect(finalDetails.reason).toBe(StandardResolutionReasons.DEFAULT);
expect(mockClient.getFlag).toHaveBeenCalledWith(flagKey, {});
});

it('should return STALE initially, then resolve with an ERROR if the client rejects', async () => {
const provider = createRocketFlagProvider(mockClient);
OpenFeature.setProvider(provider);
const client = OpenFeature.getClient();
const flagKey = 'test-flag-error';
const errorMessage = 'Network error';

mockClient.getFlag.mockRejectedValue(new Error(errorMessage));

const initialDetails = provider.resolveBooleanEvaluation(flagKey, false, {}, mockLogger);
expect(initialDetails.reason).toBe(StandardResolutionReasons.STALE);

await new Promise((resolve) => setTimeout(resolve, 0));

const finalDetails = client.getBooleanDetails(flagKey, false);

expect(finalDetails.value).toBe(false); // Default value
expect(finalDetails.reason).toBe(StandardResolutionReasons.ERROR);
expect(finalDetails.errorCode).toBe(ErrorCode.GENERAL);
expect(finalDetails.errorMessage).toBe(errorMessage);
});
});

// Tests for other evaluation types to ensure they return TYPE_MISMATCH
describe('Unsupported Evaluations', () => {
const provider = createRocketFlagProvider(mockClient);

it('resolveStringEvaluation should return TYPE_MISMATCH error', () => {
const details = provider.resolveStringEvaluation('flag', 'default', {}, mockLogger);
expect(details.reason).toBe(StandardResolutionReasons.ERROR);
expect(details.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
expect(details.value).toBe('default');
});

it('resolveNumberEvaluation should return TYPE_MISMATCH error', () => {
const details = provider.resolveNumberEvaluation('flag', 123, {}, mockLogger);
expect(details.reason).toBe(StandardResolutionReasons.ERROR);
expect(details.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
expect(details.value).toBe(123);
});

it('resolveObjectEvaluation should return TYPE_MISMATCH error', () => {
const defaultValue = { key: 'value' };
const details = provider.resolveObjectEvaluation('flag', defaultValue, {}, mockLogger);
expect(details.reason).toBe(StandardResolutionReasons.ERROR);
expect(details.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
expect(details.value).toEqual(defaultValue);
});
});
});
Loading