Skip to content

feat: Create a Growthbook server side provider #938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
"libs/providers/multi-provider-web": "0.0.3",
"libs/providers/growthbook-client": "0.1.2",
"libs/providers/config-cat-web": "0.1.3",
"libs/shared/config-cat-core": "0.1.0"
"libs/shared/config-cat-core": "0.1.0",
"libs/providers/growthbook": "0.1.1"
}
25 changes: 25 additions & 0 deletions libs/providers/growthbook/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"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"
}
}
]
}
45 changes: 45 additions & 0 deletions libs/providers/growthbook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# growthbook Provider

## Installation

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

## Example Setup

```typescript
import { GrowthBookClient, ClientOptions, InitOptions } from '@growthbook/growthbook';
import { GrowthbookProvider } from '@openfeature/growthbook-provider';
import { OpenFeature } from '@openfeature/server-sdk';

/*
* Configure your GrowthBook instance with GrowthBook context
* @see https://docs.growthbook.io/lib/js#step-1-configure-your-app
*/
const gbClientOptions: ClientOptions = {
apiHost: 'https://cdn.growthbook.io',
clientKey: 'sdk-abc123',
// Only required if you have feature encryption enabled in GrowthBook
decryptionKey: 'key_abc123',
};

/*
* optional init options
* @see https://docs.growthbook.io/lib/js#switching-to-init
*/
const initOptions: InitOptions = {
timeout: 2000,
streaming: true,
};

OpenFeature.setProvider(new GrowthbookProvider(gbClientOptions, initOptions));
```

## Building

Run `nx package providers-growthbook` to build the library.

## Running unit tests

Run `nx test providers-growthbook` to execute the unit tests via [Jest](https://jestjs.io).
3 changes: 3 additions & 0 deletions libs/providers/growthbook/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": [["minify", { "builtIns": false }]]
}
10 changes: 10 additions & 0 deletions libs/providers/growthbook/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable */
export default {
displayName: 'providers-growthbook',
preset: '../../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/providers/growthbook',
};
17 changes: 17 additions & 0 deletions libs/providers/growthbook/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@openfeature/growthbook-provider",
"version": "0.1.1",
"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"
},
"peerDependencies": {
"@growthbook/growthbook": "^1.3.1",
"@openfeature/server-sdk": "^1.13.0"
}
}
76 changes: 76 additions & 0 deletions libs/providers/growthbook/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"name": "providers-growthbook",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/providers/growthbook/src",
"projectType": "library",
"targets": {
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "npm run publish-if-not-exists",
"cwd": "dist/libs/providers/growthbook"
},
"dependsOn": [
{
"projects": "self",
"target": "package"
}
]
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/providers/growthbook/**/*.ts", "libs/providers/growthbook/package.json"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/providers/growthbook/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"package": {
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "libs/providers/growthbook/package.json",
"outputPath": "dist/libs/providers/growthbook",
"entryFile": "libs/providers/growthbook/src/index.ts",
"tsConfig": "libs/providers/growthbook/tsconfig.lib.json",
"buildableProjectDepsInPackageJsonType": "dependencies",
"compiler": "tsc",
"generateExportsField": true,
"umdName": "growthbook",
"external": "all",
"format": ["cjs", "esm"],
"assets": [
{
"glob": "package.json",
"input": "./assets",
"output": "./src/"
},
{
"glob": "LICENSE",
"input": "./",
"output": "./"
},
{
"glob": "README.md",
"input": "./libs/providers/growthbook",
"output": "./"
}
]
}
}
},
"tags": []
}
1 change: 1 addition & 0 deletions libs/providers/growthbook/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/growthbook-provider';
182 changes: 182 additions & 0 deletions libs/providers/growthbook/src/lib/growthbook-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { ClientOptions, GrowthBookClient, InitOptions } from '@growthbook/growthbook';
import { GrowthbookProvider } from './growthbook-provider';
import { Client, OpenFeature } from '@openfeature/server-sdk';

jest.mock('@growthbook/growthbook');

const testFlagKey = 'flag-key';
const growthbookOptionsMock: ClientOptions = {
apiHost: 'http://api.growthbook.io',
clientKey: 'sdk-test-key',
globalAttributes: {
id: 1,
},
};

const initOptionsMock: InitOptions = {
timeout: 5000,
};

describe('GrowthbookProvider', () => {
let gbProvider: GrowthbookProvider;
let ofClient: Client;

beforeAll(() => {
gbProvider = new GrowthbookProvider(growthbookOptionsMock, initOptionsMock);
OpenFeature.setProvider(gbProvider);
ofClient = OpenFeature.getClient();
});
beforeEach(() => {
jest.clearAllMocks();
});

it('should be and instance of GrowthbookProvider', () => {
expect(new GrowthbookProvider(growthbookOptionsMock, initOptionsMock)).toBeInstanceOf(GrowthbookProvider);
});

describe('constructor', () => {
it('should set the growthbook options & initOptions correctly', () => {
const provider = new GrowthbookProvider(growthbookOptionsMock, initOptionsMock);

expect(provider['options']).toEqual(growthbookOptionsMock);
expect(provider['_initOptions']).toEqual(initOptionsMock);
});
});

describe('initialize', () => {
const provider = new GrowthbookProvider(growthbookOptionsMock, initOptionsMock);

it('should call growthbook initialize function with correct arguments', async () => {
const evalContext = { serverIp: '10.1.1.1' };
await provider.initialize({ serverIp: '10.1.1.1' });

const options = {
...provider['options'],
globalAttributes: { ...provider['options'].globalAttributes, ...evalContext },
};

expect(GrowthBookClient).toHaveBeenCalledWith(options);
expect(provider['_client']?.init).toHaveBeenCalledWith(initOptionsMock);
});
});

describe('resolveBooleanEvaluation', () => {
it('handles correct return types for boolean variations', async () => {
jest.spyOn(GrowthBookClient.prototype, 'evalFeature').mockImplementation(() => ({
value: true,
source: 'experiment',
on: true,
off: false,
ruleId: 'test',
experimentResult: {
value: true,
variationId: 1,
key: 'treatment',
inExperiment: true,
hashAttribute: 'id',
hashValue: 'abc',
featureId: testFlagKey,
},
}));

const res = await ofClient.getBooleanDetails(testFlagKey, false);
expect(res).toEqual({
flagKey: testFlagKey,
flagMetadata: {},
value: true,
reason: 'experiment',
variant: 'treatment',
});
});
});

describe('resolveStringEvaluation', () => {
it('handles correct return types for string variations', async () => {
jest.spyOn(GrowthBookClient.prototype, 'evalFeature').mockImplementation(() => ({
value: 'Experiment fearlessly, deliver confidently',
source: 'experiment',
on: true,
off: false,
ruleId: 'test',
experimentResult: {
value: 'Experiment fearlessly, deliver confidently',
variationId: 1,
key: 'treatment',
inExperiment: true,
hashAttribute: 'id',
hashValue: 'abc',
featureId: testFlagKey,
},
}));

const res = await ofClient.getStringDetails(testFlagKey, '');
expect(res).toEqual({
flagKey: testFlagKey,
flagMetadata: {},
value: 'Experiment fearlessly, deliver confidently',
reason: 'experiment',
variant: 'treatment',
});
});
});

describe('resolveNumberEvaluation', () => {
it('handles correct return types for number variations', async () => {
jest.spyOn(GrowthBookClient.prototype, 'evalFeature').mockImplementation(() => ({
value: 12345,
source: 'experiment',
on: true,
off: false,
ruleId: 'test',
experimentResult: {
value: 12345,
variationId: 1,
key: 'treatment',
inExperiment: true,
hashAttribute: 'id',
hashValue: 'abc',
featureId: testFlagKey,
},
}));

const res = await ofClient.getNumberDetails(testFlagKey, 1);
expect(res).toEqual({
flagKey: testFlagKey,
flagMetadata: {},
value: 12345,
reason: 'experiment',
variant: 'treatment',
});
});
});

describe('resolveObjectEvaluation', () => {
it('handles correct return types for object variations', async () => {
jest.spyOn(GrowthBookClient.prototype, 'evalFeature').mockImplementation(() => ({
value: { test: true },
source: 'experiment',
on: true,
off: false,
ruleId: 'test',
experimentResult: {
value: { test: true },
variationId: 1,
key: 'treatment',
inExperiment: true,
hashAttribute: 'id',
hashValue: 'abc',
featureId: testFlagKey,
},
}));

const res = await ofClient.getObjectDetails(testFlagKey, {});
expect(res).toEqual({
flagKey: testFlagKey,
flagMetadata: {},
value: { test: true },
reason: 'experiment',
variant: 'treatment',
});
});
});
});
Loading
Loading