Skip to content

Commit 0e6a486

Browse files
authored
feat: Create a Growthbook server side provider (#938)
Signed-off-by: Michael Samper <[email protected]>
1 parent b4da066 commit 0e6a486

19 files changed

+636
-6
lines changed

.release-please-manifest.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
"libs/providers/multi-provider-web": "0.0.3",
1919
"libs/providers/growthbook-client": "0.1.2",
2020
"libs/providers/config-cat-web": "0.1.3",
21-
"libs/shared/config-cat-core": "0.1.0"
21+
"libs/shared/config-cat-core": "0.1.0",
22+
"libs/providers/growthbook": "0.1.1"
2223
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"extends": ["../../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7+
"rules": {}
8+
},
9+
{
10+
"files": ["*.ts", "*.tsx"],
11+
"rules": {}
12+
},
13+
{
14+
"files": ["*.js", "*.jsx"],
15+
"rules": {}
16+
},
17+
{
18+
"files": ["*.json"],
19+
"parser": "jsonc-eslint-parser",
20+
"rules": {
21+
"@nx/dependency-checks": "error"
22+
}
23+
}
24+
]
25+
}

libs/providers/growthbook/README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# growthbook Provider
2+
3+
## Installation
4+
5+
```
6+
$ npm install @openfeature/growthbook-provider
7+
```
8+
9+
## Example Setup
10+
11+
```typescript
12+
import { GrowthBookClient, ClientOptions, InitOptions } from '@growthbook/growthbook';
13+
import { GrowthbookProvider } from '@openfeature/growthbook-provider';
14+
import { OpenFeature } from '@openfeature/server-sdk';
15+
16+
/*
17+
* Configure your GrowthBook instance with GrowthBook context
18+
* @see https://docs.growthbook.io/lib/js#step-1-configure-your-app
19+
*/
20+
const gbClientOptions: ClientOptions = {
21+
apiHost: 'https://cdn.growthbook.io',
22+
clientKey: 'sdk-abc123',
23+
// Only required if you have feature encryption enabled in GrowthBook
24+
decryptionKey: 'key_abc123',
25+
};
26+
27+
/*
28+
* optional init options
29+
* @see https://docs.growthbook.io/lib/js#switching-to-init
30+
*/
31+
const initOptions: InitOptions = {
32+
timeout: 2000,
33+
streaming: true,
34+
};
35+
36+
OpenFeature.setProvider(new GrowthbookProvider(gbClientOptions, initOptions));
37+
```
38+
39+
## Building
40+
41+
Run `nx package providers-growthbook` to build the library.
42+
43+
## Running unit tests
44+
45+
Run `nx test providers-growthbook` to execute the unit tests via [Jest](https://jestjs.io).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": [["minify", { "builtIns": false }]]
3+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* eslint-disable */
2+
export default {
3+
displayName: 'providers-growthbook',
4+
preset: '../../../jest.preset.js',
5+
transform: {
6+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
7+
},
8+
moduleFileExtensions: ['ts', 'js', 'html'],
9+
coverageDirectory: '../../../coverage/libs/providers/growthbook',
10+
};
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@openfeature/growthbook-provider",
3+
"version": "0.1.1",
4+
"dependencies": {
5+
"tslib": "^2.3.0"
6+
},
7+
"main": "./src/index.js",
8+
"typings": "./src/index.d.ts",
9+
"scripts": {
10+
"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",
11+
"current-version": "echo $npm_package_version"
12+
},
13+
"peerDependencies": {
14+
"@growthbook/growthbook": "^1.3.1",
15+
"@openfeature/server-sdk": "^1.13.0"
16+
}
17+
}
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"name": "providers-growthbook",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "libs/providers/growthbook/src",
5+
"projectType": "library",
6+
"targets": {
7+
"publish": {
8+
"executor": "nx:run-commands",
9+
"options": {
10+
"command": "npm run publish-if-not-exists",
11+
"cwd": "dist/libs/providers/growthbook"
12+
},
13+
"dependsOn": [
14+
{
15+
"projects": "self",
16+
"target": "package"
17+
}
18+
]
19+
},
20+
"lint": {
21+
"executor": "@nx/linter:eslint",
22+
"outputs": ["{options.outputFile}"],
23+
"options": {
24+
"lintFilePatterns": ["libs/providers/growthbook/**/*.ts", "libs/providers/growthbook/package.json"]
25+
}
26+
},
27+
"test": {
28+
"executor": "@nx/jest:jest",
29+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
30+
"options": {
31+
"jestConfig": "libs/providers/growthbook/jest.config.ts",
32+
"passWithNoTests": true
33+
},
34+
"configurations": {
35+
"ci": {
36+
"ci": true,
37+
"codeCoverage": true
38+
}
39+
}
40+
},
41+
"package": {
42+
"executor": "@nx/rollup:rollup",
43+
"outputs": ["{options.outputPath}"],
44+
"options": {
45+
"project": "libs/providers/growthbook/package.json",
46+
"outputPath": "dist/libs/providers/growthbook",
47+
"entryFile": "libs/providers/growthbook/src/index.ts",
48+
"tsConfig": "libs/providers/growthbook/tsconfig.lib.json",
49+
"buildableProjectDepsInPackageJsonType": "dependencies",
50+
"compiler": "tsc",
51+
"generateExportsField": true,
52+
"umdName": "growthbook",
53+
"external": "all",
54+
"format": ["cjs", "esm"],
55+
"assets": [
56+
{
57+
"glob": "package.json",
58+
"input": "./assets",
59+
"output": "./src/"
60+
},
61+
{
62+
"glob": "LICENSE",
63+
"input": "./",
64+
"output": "./"
65+
},
66+
{
67+
"glob": "README.md",
68+
"input": "./libs/providers/growthbook",
69+
"output": "./"
70+
}
71+
]
72+
}
73+
}
74+
},
75+
"tags": []
76+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib/growthbook-provider';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { ClientOptions, GrowthBookClient, InitOptions } from '@growthbook/growthbook';
2+
import { GrowthbookProvider } from './growthbook-provider';
3+
import { Client, OpenFeature } from '@openfeature/server-sdk';
4+
5+
jest.mock('@growthbook/growthbook');
6+
7+
const testFlagKey = 'flag-key';
8+
const growthbookOptionsMock: ClientOptions = {
9+
apiHost: 'http://api.growthbook.io',
10+
clientKey: 'sdk-test-key',
11+
globalAttributes: {
12+
id: 1,
13+
},
14+
};
15+
16+
const initOptionsMock: InitOptions = {
17+
timeout: 5000,
18+
};
19+
20+
describe('GrowthbookProvider', () => {
21+
let gbProvider: GrowthbookProvider;
22+
let ofClient: Client;
23+
24+
beforeAll(() => {
25+
gbProvider = new GrowthbookProvider(growthbookOptionsMock, initOptionsMock);
26+
OpenFeature.setProvider(gbProvider);
27+
ofClient = OpenFeature.getClient();
28+
});
29+
beforeEach(() => {
30+
jest.clearAllMocks();
31+
});
32+
33+
it('should be and instance of GrowthbookProvider', () => {
34+
expect(new GrowthbookProvider(growthbookOptionsMock, initOptionsMock)).toBeInstanceOf(GrowthbookProvider);
35+
});
36+
37+
describe('constructor', () => {
38+
it('should set the growthbook options & initOptions correctly', () => {
39+
const provider = new GrowthbookProvider(growthbookOptionsMock, initOptionsMock);
40+
41+
expect(provider['options']).toEqual(growthbookOptionsMock);
42+
expect(provider['_initOptions']).toEqual(initOptionsMock);
43+
});
44+
});
45+
46+
describe('initialize', () => {
47+
const provider = new GrowthbookProvider(growthbookOptionsMock, initOptionsMock);
48+
49+
it('should call growthbook initialize function with correct arguments', async () => {
50+
const evalContext = { serverIp: '10.1.1.1' };
51+
await provider.initialize({ serverIp: '10.1.1.1' });
52+
53+
const options = {
54+
...provider['options'],
55+
globalAttributes: { ...provider['options'].globalAttributes, ...evalContext },
56+
};
57+
58+
expect(GrowthBookClient).toHaveBeenCalledWith(options);
59+
expect(provider['_client']?.init).toHaveBeenCalledWith(initOptionsMock);
60+
});
61+
});
62+
63+
describe('resolveBooleanEvaluation', () => {
64+
it('handles correct return types for boolean variations', async () => {
65+
jest.spyOn(GrowthBookClient.prototype, 'evalFeature').mockImplementation(() => ({
66+
value: true,
67+
source: 'experiment',
68+
on: true,
69+
off: false,
70+
ruleId: 'test',
71+
experimentResult: {
72+
value: true,
73+
variationId: 1,
74+
key: 'treatment',
75+
inExperiment: true,
76+
hashAttribute: 'id',
77+
hashValue: 'abc',
78+
featureId: testFlagKey,
79+
},
80+
}));
81+
82+
const res = await ofClient.getBooleanDetails(testFlagKey, false);
83+
expect(res).toEqual({
84+
flagKey: testFlagKey,
85+
flagMetadata: {},
86+
value: true,
87+
reason: 'experiment',
88+
variant: 'treatment',
89+
});
90+
});
91+
});
92+
93+
describe('resolveStringEvaluation', () => {
94+
it('handles correct return types for string variations', async () => {
95+
jest.spyOn(GrowthBookClient.prototype, 'evalFeature').mockImplementation(() => ({
96+
value: 'Experiment fearlessly, deliver confidently',
97+
source: 'experiment',
98+
on: true,
99+
off: false,
100+
ruleId: 'test',
101+
experimentResult: {
102+
value: 'Experiment fearlessly, deliver confidently',
103+
variationId: 1,
104+
key: 'treatment',
105+
inExperiment: true,
106+
hashAttribute: 'id',
107+
hashValue: 'abc',
108+
featureId: testFlagKey,
109+
},
110+
}));
111+
112+
const res = await ofClient.getStringDetails(testFlagKey, '');
113+
expect(res).toEqual({
114+
flagKey: testFlagKey,
115+
flagMetadata: {},
116+
value: 'Experiment fearlessly, deliver confidently',
117+
reason: 'experiment',
118+
variant: 'treatment',
119+
});
120+
});
121+
});
122+
123+
describe('resolveNumberEvaluation', () => {
124+
it('handles correct return types for number variations', async () => {
125+
jest.spyOn(GrowthBookClient.prototype, 'evalFeature').mockImplementation(() => ({
126+
value: 12345,
127+
source: 'experiment',
128+
on: true,
129+
off: false,
130+
ruleId: 'test',
131+
experimentResult: {
132+
value: 12345,
133+
variationId: 1,
134+
key: 'treatment',
135+
inExperiment: true,
136+
hashAttribute: 'id',
137+
hashValue: 'abc',
138+
featureId: testFlagKey,
139+
},
140+
}));
141+
142+
const res = await ofClient.getNumberDetails(testFlagKey, 1);
143+
expect(res).toEqual({
144+
flagKey: testFlagKey,
145+
flagMetadata: {},
146+
value: 12345,
147+
reason: 'experiment',
148+
variant: 'treatment',
149+
});
150+
});
151+
});
152+
153+
describe('resolveObjectEvaluation', () => {
154+
it('handles correct return types for object variations', async () => {
155+
jest.spyOn(GrowthBookClient.prototype, 'evalFeature').mockImplementation(() => ({
156+
value: { test: true },
157+
source: 'experiment',
158+
on: true,
159+
off: false,
160+
ruleId: 'test',
161+
experimentResult: {
162+
value: { test: true },
163+
variationId: 1,
164+
key: 'treatment',
165+
inExperiment: true,
166+
hashAttribute: 'id',
167+
hashValue: 'abc',
168+
featureId: testFlagKey,
169+
},
170+
}));
171+
172+
const res = await ofClient.getObjectDetails(testFlagKey, {});
173+
expect(res).toEqual({
174+
flagKey: testFlagKey,
175+
flagMetadata: {},
176+
value: { test: true },
177+
reason: 'experiment',
178+
variant: 'treatment',
179+
});
180+
});
181+
});
182+
});

0 commit comments

Comments
 (0)