@growthbook/proxy-eval 1.0.8 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +35 -10
- package/dist/index.js.map +1 -1
- package/example/cloudflare/package.json +19 -0
- package/example/cloudflare/src/index.ts +119 -0
- package/example/cloudflare/tsconfig.json +20 -0
- package/example/cloudflare/wrangler.toml.example.toml +14 -0
- package/package.json +2 -2
- package/src/index.ts +47 -13
package/dist/index.js
CHANGED
|
@@ -52,21 +52,46 @@ function evaluateFeatures(_a) {
|
|
|
52
52
|
}
|
|
53
53
|
const gbFeatures = gb.getFeatures();
|
|
54
54
|
for (const key in gbFeatures) {
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
55
|
+
const featureResult = gb.evalFeature(key);
|
|
56
|
+
// Check if we have any deferred tracking calls (including prerequisite experiments)
|
|
57
|
+
const deferredCalls = gb.getDeferredTrackingCalls();
|
|
58
|
+
const hasDeferredCalls = deferredCalls && deferredCalls.length > 0;
|
|
59
|
+
const hasValue = featureResult.value !== undefined;
|
|
60
|
+
// legacy check (if deferred calls are missing)
|
|
61
|
+
const hasExperiment = featureResult.source === "experiment" && featureResult.experimentResult !== undefined;
|
|
62
|
+
if (hasValue || hasDeferredCalls) {
|
|
57
63
|
// reduced feature definition
|
|
58
64
|
evaluatedFeatures[key] = {
|
|
59
|
-
defaultValue:
|
|
65
|
+
defaultValue: featureResult.value,
|
|
60
66
|
};
|
|
61
|
-
if (
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
if (hasDeferredCalls) {
|
|
68
|
+
// Process all experiment exposures (including prerequisites)
|
|
69
|
+
const tracks = deferredCalls
|
|
70
|
+
.filter(call => call.experiment && call.result) // Defensive: ensure call has required properties
|
|
71
|
+
.map(call => ({
|
|
72
|
+
experiment: scrubExperiment(call.experiment, call.result.variationId),
|
|
73
|
+
result: call.result,
|
|
74
|
+
}));
|
|
66
75
|
evaluatedFeatures[key].rules = [
|
|
67
76
|
{
|
|
68
|
-
force:
|
|
69
|
-
tracks
|
|
77
|
+
force: featureResult.value,
|
|
78
|
+
tracks,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
gb.setDeferredTrackingCalls([]);
|
|
82
|
+
}
|
|
83
|
+
else if (hasExperiment) {
|
|
84
|
+
// Fallback for direct experiments when no deferred calls
|
|
85
|
+
const scrubbedResultExperiment = ((_b = featureResult === null || featureResult === void 0 ? void 0 : featureResult.experimentResult) === null || _b === void 0 ? void 0 : _b.variationId) !== undefined
|
|
86
|
+
? scrubExperiment(featureResult.experiment, featureResult.experimentResult.variationId)
|
|
87
|
+
: featureResult.experiment;
|
|
88
|
+
evaluatedFeatures[key].rules = [
|
|
89
|
+
{
|
|
90
|
+
force: featureResult.value,
|
|
91
|
+
tracks: [{
|
|
92
|
+
experiment: scrubbedResultExperiment,
|
|
93
|
+
result: featureResult.experimentResult,
|
|
94
|
+
}],
|
|
70
95
|
},
|
|
71
96
|
];
|
|
72
97
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "growthbook-edge-eval-cloudflare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GrowthBook edge evaluation for Cloudflare Workers",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"build": "wrangler build"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@growthbook/proxy-eval": "^1.1.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@cloudflare/workers-types": "^4.20250926.0",
|
|
16
|
+
"typescript": "^5.8.2",
|
|
17
|
+
"wrangler": "^4.40.1"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { evaluateFeatures } from "@growthbook/proxy-eval";
|
|
2
|
+
import { StickyBucketService } from "@growthbook/growthbook";
|
|
3
|
+
|
|
4
|
+
interface Env {
|
|
5
|
+
ENVIRONMENT: string;
|
|
6
|
+
|
|
7
|
+
KV_GB_PAYLOAD: KVNamespace;
|
|
8
|
+
// NOTE: Be sure to connect a GrowthBook SDK Webhook to your KV store.
|
|
9
|
+
// Use the webhook type "Cloudflare KV" in the SDK webhook settings.
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PostBody {
|
|
13
|
+
payload: any;
|
|
14
|
+
attributes: Record<string, any>;
|
|
15
|
+
forcedVariations?: Record<string, number>;
|
|
16
|
+
forcedFeatures?: Map<string, any>;
|
|
17
|
+
url?: string;
|
|
18
|
+
// NOTE: For advanced experimentation, you may want to connect a KV or cookie Sticky Bucket service
|
|
19
|
+
stickyBucketService?:
|
|
20
|
+
| (StickyBucketService & {
|
|
21
|
+
// For cookie-based service, should be a no-op:
|
|
22
|
+
connect: () => Promise<void>;
|
|
23
|
+
// For write-buffer flushes (ex: GB Proxy's implementation of RedisStickyBucketService):
|
|
24
|
+
onEvaluate?: () => Promise<void>;
|
|
25
|
+
})
|
|
26
|
+
| null;
|
|
27
|
+
ctx?: { verboseDebugging?: boolean };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const KV_KEY = "gb_payload";
|
|
31
|
+
const CACHE_TTL = 60 * 1000; // 1 min
|
|
32
|
+
|
|
33
|
+
// Cache payload from KV
|
|
34
|
+
let cachedPayload: any = null;
|
|
35
|
+
let lastFetch = 0;
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
fetch: async function (
|
|
40
|
+
request: Request,
|
|
41
|
+
env: Env,
|
|
42
|
+
ctx: ExecutionContext,
|
|
43
|
+
): Promise<Response> {
|
|
44
|
+
// Handle CORS preflight requests
|
|
45
|
+
if (request.method === 'OPTIONS') {
|
|
46
|
+
return new Response(null, {
|
|
47
|
+
status: 204,
|
|
48
|
+
headers: getCORSHeaders(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Only allow POST requests
|
|
53
|
+
if (request.method !== 'POST') {
|
|
54
|
+
return new Response(null, {
|
|
55
|
+
status: 405,
|
|
56
|
+
headers: {
|
|
57
|
+
'Allow': 'POST',
|
|
58
|
+
...getCORSHeaders(),
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const body = await request.json<PostBody>().catch(() => null);
|
|
65
|
+
if (!body || typeof body !== "object") {
|
|
66
|
+
return handleInvalidRequest();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!cachedPayload || Date.now() - lastFetch > CACHE_TTL) {
|
|
70
|
+
cachedPayload = await env.KV_GB_PAYLOAD.get(KV_KEY, 'json');
|
|
71
|
+
lastFetch = Date.now();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { attributes = {}, forcedVariations = {}, forcedFeatures = [], url = "" } = body;
|
|
75
|
+
const forcedFeaturesMap = new Map(forcedFeatures);
|
|
76
|
+
|
|
77
|
+
const evalResponse = await evaluateFeatures({
|
|
78
|
+
payload: cachedPayload,
|
|
79
|
+
attributes,
|
|
80
|
+
forcedVariations,
|
|
81
|
+
forcedFeatures: forcedFeaturesMap,
|
|
82
|
+
url,
|
|
83
|
+
// stickyBucketService,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Return success response
|
|
87
|
+
return new Response(
|
|
88
|
+
JSON.stringify(evalResponse),
|
|
89
|
+
{
|
|
90
|
+
status: 200,
|
|
91
|
+
headers: {
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
...getCORSHeaders(),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(error);
|
|
100
|
+
return handleInvalidRequest();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getCORSHeaders(): Record<string, string> {
|
|
106
|
+
return {
|
|
107
|
+
'Access-Control-Allow-Origin': '*', // Configure this appropriately for production
|
|
108
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
109
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
110
|
+
'Access-Control-Max-Age': '86400',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleInvalidRequest(): Response {
|
|
115
|
+
return new Response(null, {
|
|
116
|
+
status: 500,
|
|
117
|
+
headers: getCORSHeaders(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "ES2022",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowSyntheticDefaultImports": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"types": ["@cloudflare/workers-types"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name = "growthbook-edge-evaluator"
|
|
2
|
+
main = "src/index.ts"
|
|
3
|
+
compatibility_date = "2025-09-25"
|
|
4
|
+
|
|
5
|
+
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
|
|
6
|
+
# Note: Use secrets to store sensitive data.
|
|
7
|
+
# Docs: https://developers.cloudflare.com/workers/platform/environment-variables
|
|
8
|
+
|
|
9
|
+
kv_namespaces = [
|
|
10
|
+
{ binding = "KV_GB_PAYLOAD", id = "qwerty123" }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[vars]
|
|
14
|
+
NODE_ENV="production"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthbook/proxy-eval",
|
|
3
3
|
"description": "Remote evaluation service for proxies, edge workers, or backend APIs",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dev": "tsc --watch"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@growthbook/growthbook": "^1.6.
|
|
21
|
+
"@growthbook/growthbook": "^1.6.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"rimraf": "^6.0.1",
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
GrowthBook,
|
|
4
4
|
Context as GBContext,
|
|
5
5
|
StickyBucketService,
|
|
6
|
+
FeatureDefinition,
|
|
7
|
+
FeatureRule,
|
|
8
|
+
AutoExperiment,
|
|
6
9
|
} from "@growthbook/growthbook";
|
|
7
10
|
|
|
8
11
|
export async function evaluateFeatures({
|
|
@@ -27,8 +30,8 @@ export async function evaluateFeatures({
|
|
|
27
30
|
| null;
|
|
28
31
|
ctx?: any;
|
|
29
32
|
}) {
|
|
30
|
-
const evaluatedFeatures: Record<string,
|
|
31
|
-
const evaluatedExperiments:
|
|
33
|
+
const evaluatedFeatures: Record<string, FeatureDefinition> = {};
|
|
34
|
+
const evaluatedExperiments: AutoExperiment[] = [];
|
|
32
35
|
|
|
33
36
|
const features = payload?.features;
|
|
34
37
|
const experiments = payload?.experiments;
|
|
@@ -67,25 +70,56 @@ export async function evaluateFeatures({
|
|
|
67
70
|
|
|
68
71
|
const gbFeatures = gb.getFeatures();
|
|
69
72
|
for (const key in gbFeatures) {
|
|
70
|
-
const
|
|
71
|
-
|
|
73
|
+
const featureResult = gb.evalFeature(key);
|
|
74
|
+
|
|
75
|
+
// Check if we have any deferred tracking calls (including prerequisite experiments)
|
|
76
|
+
const deferredCalls = gb.getDeferredTrackingCalls();
|
|
77
|
+
const hasDeferredCalls = deferredCalls && deferredCalls.length > 0;
|
|
78
|
+
const hasValue = featureResult.value !== undefined;
|
|
79
|
+
|
|
80
|
+
// legacy check (if deferred calls are missing)
|
|
81
|
+
const hasExperiment = featureResult.source === "experiment" && featureResult.experimentResult !== undefined;
|
|
82
|
+
|
|
83
|
+
if (hasValue || hasDeferredCalls) {
|
|
72
84
|
// reduced feature definition
|
|
73
85
|
evaluatedFeatures[key] = {
|
|
74
|
-
defaultValue:
|
|
86
|
+
defaultValue: featureResult.value,
|
|
75
87
|
};
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
|
|
89
|
+
if (hasDeferredCalls) {
|
|
90
|
+
// Process all experiment exposures (including prerequisites)
|
|
91
|
+
const tracks: FeatureRule['tracks'] = deferredCalls
|
|
92
|
+
.filter(call => call.experiment && call.result) // Defensive: ensure call has required properties
|
|
93
|
+
.map(call => ({
|
|
94
|
+
experiment: scrubExperiment(call.experiment, call.result.variationId),
|
|
95
|
+
result: call.result,
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
evaluatedFeatures[key].rules = [
|
|
99
|
+
{
|
|
100
|
+
force: featureResult.value,
|
|
101
|
+
tracks,
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
gb.setDeferredTrackingCalls([]);
|
|
105
|
+
|
|
106
|
+
} else if (hasExperiment) {
|
|
107
|
+
// Fallback for direct experiments when no deferred calls
|
|
78
108
|
const scrubbedResultExperiment =
|
|
79
|
-
|
|
109
|
+
featureResult?.experimentResult?.variationId !== undefined
|
|
80
110
|
? scrubExperiment(
|
|
81
|
-
|
|
82
|
-
|
|
111
|
+
featureResult.experiment,
|
|
112
|
+
featureResult.experimentResult.variationId,
|
|
83
113
|
)
|
|
84
|
-
:
|
|
114
|
+
: featureResult.experiment;
|
|
115
|
+
|
|
85
116
|
evaluatedFeatures[key].rules = [
|
|
86
117
|
{
|
|
87
|
-
force:
|
|
88
|
-
tracks: [{
|
|
118
|
+
force: featureResult.value,
|
|
119
|
+
tracks: [{
|
|
120
|
+
experiment: scrubbedResultExperiment,
|
|
121
|
+
result: featureResult.experimentResult!,
|
|
122
|
+
}],
|
|
89
123
|
},
|
|
90
124
|
];
|
|
91
125
|
}
|