@growthbook/edge-utils 0.0.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/LICENSE +21 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.js +171 -0
- package/dist/app.js.map +1 -0
- package/dist/attributes.d.ts +5 -0
- package/dist/attributes.js +76 -0
- package/dist/attributes.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +91 -0
- package/dist/config.js.map +1 -0
- package/dist/domMutations.d.ts +6 -0
- package/dist/domMutations.js +151 -0
- package/dist/domMutations.js.map +1 -0
- package/dist/generated/sdkWrapper.d.ts +1 -0
- package/dist/generated/sdkWrapper.js +5 -0
- package/dist/generated/sdkWrapper.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/inject.d.ts +16 -0
- package/dist/inject.js +136 -0
- package/dist/inject.js.map +1 -0
- package/dist/redirect.d.ts +11 -0
- package/dist/redirect.js +43 -0
- package/dist/redirect.js.map +1 -0
- package/dist/routing.d.ts +2 -0
- package/dist/routing.js +47 -0
- package/dist/routing.js.map +1 -0
- package/dist/stickyBucketService.d.ts +14 -0
- package/dist/stickyBucketService.js +49 -0
- package/dist/stickyBucketService.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +31 -0
- package/scripts/generate-sdk-wrapper.js +22 -0
- package/src/app.ts +210 -0
- package/src/attributes.ts +97 -0
- package/src/config.ts +166 -0
- package/src/domMutations.ts +157 -0
- package/src/generated/sdkWrapper.ts +2 -0
- package/src/index.ts +13 -0
- package/src/inject.ts +230 -0
- package/src/redirect.ts +53 -0
- package/src/routing.ts +44 -0
- package/src/stickyBucketService.ts +48 -0
- package/src/types.ts +98 -0
- package/tsconfig.json +27 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Attributes, FeatureApiResponse, LocalStorageCompat, StickyBucketService, TrackingCallback } from "@growthbook/growthbook";
|
|
2
|
+
export interface Context<Req = unknown, Res = unknown> {
|
|
3
|
+
config: Config;
|
|
4
|
+
helpers: Helpers<Req, Res>;
|
|
5
|
+
}
|
|
6
|
+
export interface Config {
|
|
7
|
+
proxyTarget: string;
|
|
8
|
+
forwardProxyHeaders: boolean;
|
|
9
|
+
environment: string;
|
|
10
|
+
maxPayloadSize?: string;
|
|
11
|
+
routes?: Route[];
|
|
12
|
+
runVisualEditorExperiments: ExperimentRunEnvironment;
|
|
13
|
+
disableJsInjection: boolean;
|
|
14
|
+
runUrlRedirectExperiments: ExperimentRunEnvironment;
|
|
15
|
+
runCrossOriginUrlRedirectExperiments: ExperimentRunEnvironment;
|
|
16
|
+
injectRedirectUrlScript: boolean;
|
|
17
|
+
maxRedirects: number;
|
|
18
|
+
scriptInjectionPattern: string;
|
|
19
|
+
disableInjections: boolean;
|
|
20
|
+
enableStreaming: boolean;
|
|
21
|
+
enableStickyBucketing: boolean;
|
|
22
|
+
stickyBucketPrefix?: string;
|
|
23
|
+
contentSecurityPolicy?: string;
|
|
24
|
+
nonce?: string;
|
|
25
|
+
crypto?: any;
|
|
26
|
+
localStorage?: LocalStorageCompat;
|
|
27
|
+
growthbook: {
|
|
28
|
+
apiHost: string;
|
|
29
|
+
clientKey: string;
|
|
30
|
+
decryptionKey?: string;
|
|
31
|
+
trackingCallback?: string;
|
|
32
|
+
edgeTrackingCallback?: TrackingCallback;
|
|
33
|
+
attributes?: Attributes;
|
|
34
|
+
edgeStickyBucketService?: StickyBucketService;
|
|
35
|
+
payload?: FeatureApiResponse;
|
|
36
|
+
};
|
|
37
|
+
persistUuid: boolean;
|
|
38
|
+
uuidCookieName: string;
|
|
39
|
+
uuidKey: string;
|
|
40
|
+
skipAutoAttributes: boolean;
|
|
41
|
+
}
|
|
42
|
+
export type ExperimentRunEnvironment = "everywhere" | "edge" | "browser" | "skip";
|
|
43
|
+
export interface Helpers<Req, Res> {
|
|
44
|
+
getRequestURL?: (req: Req) => string;
|
|
45
|
+
getRequestMethod?: (req: Req) => string;
|
|
46
|
+
getRequestHeader?: (req: Req, key: string) => string | undefined;
|
|
47
|
+
sendResponse?: (ctx: Context<Req, Res>, res?: Res, headers?: Record<string, any>, body?: string, cookies?: Record<string, string>, status?: number) => unknown;
|
|
48
|
+
fetch?: (ctx: Context<Req, Res>, url: string) => Promise<Res>;
|
|
49
|
+
proxyRequest?: (ctx: Context<Req, Res>, req: Req, res?: Res, next?: any) => Promise<unknown>;
|
|
50
|
+
getCookie?: (req: Req, key: string) => string;
|
|
51
|
+
setCookie?: (res: Res, key: string, value: string) => void;
|
|
52
|
+
}
|
|
53
|
+
export type Route = {
|
|
54
|
+
pattern: string;
|
|
55
|
+
type?: "regex" | "simple";
|
|
56
|
+
behavior?: "intercept" | "proxy" | "error";
|
|
57
|
+
includeFileExtensions?: boolean;
|
|
58
|
+
statusCode?: number;
|
|
59
|
+
body?: string;
|
|
60
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,uDAAuD"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@growthbook/edge-utils",
|
|
3
|
+
"description": "Edge worker base app",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/growthbook/growthbook-proxy.git",
|
|
10
|
+
"directory": "packages/shared/edge-utils"
|
|
11
|
+
},
|
|
12
|
+
"author": "Bryce Fitzsimons",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build:clean": "rimraf -rf dist",
|
|
15
|
+
"build:typescript": "tsc",
|
|
16
|
+
"build": "yarn build:clean && yarn generate-sdk-wrapper && yarn build:typescript",
|
|
17
|
+
"generate-sdk-wrapper": "node scripts/generate-sdk-wrapper.js",
|
|
18
|
+
"type-check": "tsc --pretty --noEmit",
|
|
19
|
+
"dev": "node scripts/generate-sdk-wrapper.js && tsc --watch"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@growthbook/growthbook": "^1.0.0",
|
|
23
|
+
"node-html-parser": "^6.1.13"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/jsdom": "^21.1.6",
|
|
27
|
+
"@types/node": "^20.8.2",
|
|
28
|
+
"rimraf": "^5.0.5",
|
|
29
|
+
"typescript": "5.2.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
|
|
3
|
+
// Read the vendor .js file
|
|
4
|
+
const vendorScriptPath =
|
|
5
|
+
"./node_modules/@growthbook/growthbook/dist/bundles/auto.min.js";
|
|
6
|
+
const vendorScriptContent = fs
|
|
7
|
+
.readFileSync(vendorScriptPath, "utf8")
|
|
8
|
+
.replace(/\\/g, "\\\\") // Escape backslashes
|
|
9
|
+
.replace(/'/g, "\\'") // Escape single quotes
|
|
10
|
+
.replace(/"/g, '\\"') // Escape double quotes
|
|
11
|
+
.replace(/\n/g, "\\n"); // Escape newlines
|
|
12
|
+
|
|
13
|
+
// Generate TypeScript file with embedded content
|
|
14
|
+
const outputFile = "./src/generated/sdkWrapper.ts";
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
outputFile,
|
|
17
|
+
`
|
|
18
|
+
export const sdkWrapper = "${vendorScriptContent}";
|
|
19
|
+
`,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
console.log("Vendor script file generated successfully.");
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AutoExperimentVariation,
|
|
3
|
+
GrowthBook,
|
|
4
|
+
setPolyfills,
|
|
5
|
+
StickyBucketService,
|
|
6
|
+
} from "@growthbook/growthbook";
|
|
7
|
+
import { Context } from "./types";
|
|
8
|
+
import { getUserAttributes } from "./attributes";
|
|
9
|
+
import { getCspInfo, injectScript } from "./inject";
|
|
10
|
+
import { applyDomMutations } from "./domMutations";
|
|
11
|
+
import redirect from "./redirect";
|
|
12
|
+
import { getRoute } from "./routing";
|
|
13
|
+
import { EdgeStickyBucketService } from "./stickyBucketService";
|
|
14
|
+
|
|
15
|
+
export async function edgeApp<Req, Res>(
|
|
16
|
+
context: Context<Req, Res>,
|
|
17
|
+
req: Req,
|
|
18
|
+
res?: Res,
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
next?: any,
|
|
21
|
+
) {
|
|
22
|
+
// todo: import default helpers, overwrite with context helpers
|
|
23
|
+
|
|
24
|
+
let url = context.helpers.getRequestURL?.(req) || "";
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
let headers: Record<string, any> = {
|
|
28
|
+
"Content-Type": "text/html",
|
|
29
|
+
};
|
|
30
|
+
const cookies: Record<string, string> = {};
|
|
31
|
+
const setCookie = (key: string, value: string) => {
|
|
32
|
+
cookies[key] = value;
|
|
33
|
+
};
|
|
34
|
+
const { csp, nonce } = getCspInfo(context as Context<unknown, unknown>);
|
|
35
|
+
if (csp) {
|
|
36
|
+
headers["Content-Security-Policy"] = csp;
|
|
37
|
+
}
|
|
38
|
+
let body = "";
|
|
39
|
+
|
|
40
|
+
// Non GET requests are proxied
|
|
41
|
+
if (context.helpers.getRequestMethod?.(req) !== "GET") {
|
|
42
|
+
return context.helpers.proxyRequest?.(context, req, res, next);
|
|
43
|
+
}
|
|
44
|
+
// Check the url for routing rules (default behavior is intercept)
|
|
45
|
+
const route = getRoute(context as Context<unknown, unknown>, url);
|
|
46
|
+
if (route.behavior === "error") {
|
|
47
|
+
return context.helpers.sendResponse?.(
|
|
48
|
+
context,
|
|
49
|
+
res,
|
|
50
|
+
headers,
|
|
51
|
+
route.body || "",
|
|
52
|
+
cookies,
|
|
53
|
+
route.statusCode,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (route.behavior === "proxy") {
|
|
57
|
+
return context.helpers.proxyRequest?.(context, req, res, next);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const attributes = getUserAttributes(context, req, url, setCookie);
|
|
61
|
+
|
|
62
|
+
let domChanges: AutoExperimentVariation[] = [];
|
|
63
|
+
const resetDomChanges = () => (domChanges = []);
|
|
64
|
+
|
|
65
|
+
let preRedirectChangeIds: string[] = [];
|
|
66
|
+
const setPreRedirectChangeIds = (changeIds: string[]) =>
|
|
67
|
+
(preRedirectChangeIds = changeIds);
|
|
68
|
+
|
|
69
|
+
context.config.localStorage &&
|
|
70
|
+
setPolyfills({ localStorage: context.config.localStorage });
|
|
71
|
+
context.config.crypto &&
|
|
72
|
+
setPolyfills({ SubtleCrypto: context.config.crypto });
|
|
73
|
+
|
|
74
|
+
let stickyBucketService:
|
|
75
|
+
| EdgeStickyBucketService<Req, Res>
|
|
76
|
+
| StickyBucketService
|
|
77
|
+
| undefined = undefined;
|
|
78
|
+
if (context.config.enableStickyBucketing) {
|
|
79
|
+
stickyBucketService =
|
|
80
|
+
context.config.growthbook.edgeStickyBucketService ??
|
|
81
|
+
new EdgeStickyBucketService<Req, Res>({
|
|
82
|
+
context,
|
|
83
|
+
prefix: context.config.stickyBucketPrefix,
|
|
84
|
+
req,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const growthbook = new GrowthBook({
|
|
88
|
+
apiHost: context.config.growthbook.apiHost,
|
|
89
|
+
clientKey: context.config.growthbook.clientKey,
|
|
90
|
+
decryptionKey: context.config.growthbook.decryptionKey,
|
|
91
|
+
attributes,
|
|
92
|
+
applyDomChangesCallback: (changes: AutoExperimentVariation) => {
|
|
93
|
+
domChanges.push(changes);
|
|
94
|
+
return () => {};
|
|
95
|
+
},
|
|
96
|
+
url,
|
|
97
|
+
disableVisualExperiments: ["skip", "browser"].includes(
|
|
98
|
+
context.config.runVisualEditorExperiments,
|
|
99
|
+
),
|
|
100
|
+
disableJsInjection: context.config.disableJsInjection,
|
|
101
|
+
disableUrlRedirectExperiments: ["skip", "browser"].includes(
|
|
102
|
+
context.config.runUrlRedirectExperiments,
|
|
103
|
+
),
|
|
104
|
+
disableCrossOriginUrlRedirectExperiments: ["skip", "browser"].includes(
|
|
105
|
+
context.config.runCrossOriginUrlRedirectExperiments,
|
|
106
|
+
),
|
|
107
|
+
stickyBucketService,
|
|
108
|
+
trackingCallback: context.config.disableInjections
|
|
109
|
+
? context.config.growthbook.edgeTrackingCallback
|
|
110
|
+
: undefined,
|
|
111
|
+
debug: true, // todo: remove
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await growthbook.init({
|
|
115
|
+
payload: context.config.growthbook.payload,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const oldUrl = url;
|
|
119
|
+
url = await redirect({
|
|
120
|
+
context: context as Context<unknown, unknown>,
|
|
121
|
+
req,
|
|
122
|
+
setCookie,
|
|
123
|
+
growthbook,
|
|
124
|
+
previousUrl: url,
|
|
125
|
+
resetDomChanges,
|
|
126
|
+
setPreRedirectChangeIds: setPreRedirectChangeIds,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const originUrl = getOriginUrl(context as Context<unknown, unknown>, url);
|
|
130
|
+
|
|
131
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
132
|
+
let fetchedResponse:
|
|
133
|
+
| (Res & { ok: boolean; headers: Record<string, any>; text: any })
|
|
134
|
+
| undefined = undefined;
|
|
135
|
+
try {
|
|
136
|
+
fetchedResponse = (await context.helpers.fetch?.(
|
|
137
|
+
context as Context<Req, Res>,
|
|
138
|
+
originUrl,
|
|
139
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
140
|
+
)) as Res & { ok: boolean; headers: Record<string, any>; text: any };
|
|
141
|
+
if (!fetchedResponse?.ok) {
|
|
142
|
+
throw new Error("Fetch: non-2xx status returned");
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.error(e);
|
|
146
|
+
return context.helpers.sendResponse?.(
|
|
147
|
+
context,
|
|
148
|
+
res,
|
|
149
|
+
headers,
|
|
150
|
+
"Error fetching page",
|
|
151
|
+
cookies,
|
|
152
|
+
500,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (context.config.forwardProxyHeaders && fetchedResponse?.headers) {
|
|
156
|
+
headers = { ...fetchedResponse.headers, ...headers };
|
|
157
|
+
}
|
|
158
|
+
body = await fetchedResponse.text();
|
|
159
|
+
|
|
160
|
+
body = await applyDomMutations({
|
|
161
|
+
body,
|
|
162
|
+
nonce,
|
|
163
|
+
domChanges,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
body = injectScript({
|
|
167
|
+
context: context as Context<unknown, unknown>,
|
|
168
|
+
body,
|
|
169
|
+
nonce,
|
|
170
|
+
growthbook,
|
|
171
|
+
attributes,
|
|
172
|
+
preRedirectChangeIds,
|
|
173
|
+
url,
|
|
174
|
+
oldUrl,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return context.helpers.sendResponse?.(context, res, headers, body, cookies);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getOriginUrl(context: Context, currentURL: string): string {
|
|
181
|
+
const proxyTarget = context.config.proxyTarget;
|
|
182
|
+
const currentParsedURL = new URL(currentURL);
|
|
183
|
+
const proxyParsedURL = new URL(proxyTarget);
|
|
184
|
+
|
|
185
|
+
const protocol = proxyParsedURL.protocol
|
|
186
|
+
? proxyParsedURL.protocol
|
|
187
|
+
: currentParsedURL.protocol;
|
|
188
|
+
const hostname = proxyParsedURL.hostname
|
|
189
|
+
? proxyParsedURL.hostname
|
|
190
|
+
: currentParsedURL.hostname;
|
|
191
|
+
const port = proxyParsedURL.port
|
|
192
|
+
? proxyParsedURL.port
|
|
193
|
+
: protocol === "http:"
|
|
194
|
+
? "80"
|
|
195
|
+
: "443";
|
|
196
|
+
|
|
197
|
+
let newURL = `${protocol}//${hostname}`;
|
|
198
|
+
if ((protocol === "http" && port !== "80") || port !== "443") {
|
|
199
|
+
newURL += `:${port}`;
|
|
200
|
+
}
|
|
201
|
+
newURL += `${currentParsedURL.pathname}`;
|
|
202
|
+
if (currentParsedURL.search) {
|
|
203
|
+
newURL += currentParsedURL.search;
|
|
204
|
+
}
|
|
205
|
+
if (currentParsedURL.hash) {
|
|
206
|
+
newURL += currentParsedURL.hash;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return newURL;
|
|
210
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Attributes } from "@growthbook/growthbook";
|
|
2
|
+
import { Context } from "./types";
|
|
3
|
+
|
|
4
|
+
// Get the user's attributes by merging the UUID cookie with any auto-attributes
|
|
5
|
+
export function getUserAttributes<Req, Res>(
|
|
6
|
+
ctx: Context<Req, Res>,
|
|
7
|
+
req: Req,
|
|
8
|
+
url: string,
|
|
9
|
+
setCookie: (key: string, value: string) => void,
|
|
10
|
+
): Attributes {
|
|
11
|
+
const { config, helpers } = ctx;
|
|
12
|
+
|
|
13
|
+
const providedAttributes = config.growthbook.attributes || {};
|
|
14
|
+
if (config.skipAutoAttributes) {
|
|
15
|
+
return providedAttributes;
|
|
16
|
+
}
|
|
17
|
+
// get any saved attributes from the cookie
|
|
18
|
+
const uuid = getUUID(ctx, req);
|
|
19
|
+
if (config.persistUuid) {
|
|
20
|
+
if (!helpers?.setCookie) {
|
|
21
|
+
throw new Error("Missing required dependencies");
|
|
22
|
+
}
|
|
23
|
+
setCookie(config.uuidCookieName, uuid);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const autoAttributes = getAutoAttributes(ctx, req, url);
|
|
27
|
+
return { ...autoAttributes, ...providedAttributes };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get or create a UUID for the user:
|
|
31
|
+
// - Try to get the UUID from the cookie
|
|
32
|
+
// - Or create a new one and store in the cookie
|
|
33
|
+
export function getUUID<Req, Res>(ctx: Context<Req, Res>, req: Req) {
|
|
34
|
+
const { config, helpers } = ctx;
|
|
35
|
+
|
|
36
|
+
const crypto = config?.crypto || globalThis?.crypto;
|
|
37
|
+
|
|
38
|
+
if (!crypto || !helpers?.getCookie) {
|
|
39
|
+
throw new Error("Missing required dependencies");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const genUUID = () => {
|
|
43
|
+
if (crypto.randomUUID) return crypto.randomUUID();
|
|
44
|
+
return ("" + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
|
45
|
+
(
|
|
46
|
+
(c as unknown as number) ^
|
|
47
|
+
(crypto.getRandomValues(new Uint8Array(1))[0] &
|
|
48
|
+
(15 >> ((c as unknown as number) / 4)))
|
|
49
|
+
).toString(16),
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// get the existing UUID from cookie if set, otherwise create one
|
|
54
|
+
return helpers.getCookie(req, config.uuidCookieName) || genUUID();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Infer attributes from the request
|
|
58
|
+
// - UUID will come from the cookie or be generated
|
|
59
|
+
// - Other attributes come from the request headers and URL
|
|
60
|
+
export function getAutoAttributes<Req, Res>(
|
|
61
|
+
ctx: Context<Req, Res>,
|
|
62
|
+
req: Req,
|
|
63
|
+
url: string,
|
|
64
|
+
): Attributes {
|
|
65
|
+
const { config, helpers } = ctx;
|
|
66
|
+
|
|
67
|
+
const getHeader = helpers?.getRequestHeader;
|
|
68
|
+
|
|
69
|
+
const autoAttributes: Attributes = {
|
|
70
|
+
[config.uuidKey]: getUUID(ctx, req),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const ua = getHeader?.(req, "user-agent") || "";
|
|
74
|
+
autoAttributes.browser = ua.match(/Edg/)
|
|
75
|
+
? "edge"
|
|
76
|
+
: ua.match(/Chrome/)
|
|
77
|
+
? "chrome"
|
|
78
|
+
: ua.match(/Firefox/)
|
|
79
|
+
? "firefox"
|
|
80
|
+
: ua.match(/Safari/)
|
|
81
|
+
? "safari"
|
|
82
|
+
: "unknown";
|
|
83
|
+
autoAttributes.deviceType = ua.match(/Mobi/) ? "mobile" : "desktop";
|
|
84
|
+
|
|
85
|
+
autoAttributes.url = url;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const urlObj = new URL(url);
|
|
89
|
+
autoAttributes.path = urlObj.pathname;
|
|
90
|
+
autoAttributes.host = urlObj.host;
|
|
91
|
+
autoAttributes.query = urlObj.search;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return autoAttributes;
|
|
97
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Config, Context, ExperimentRunEnvironment } from "./types";
|
|
2
|
+
|
|
3
|
+
export const defaultContext: Context = {
|
|
4
|
+
config: {
|
|
5
|
+
proxyTarget: "/",
|
|
6
|
+
forwardProxyHeaders: true,
|
|
7
|
+
environment: "production",
|
|
8
|
+
maxPayloadSize: "2mb",
|
|
9
|
+
runVisualEditorExperiments: "everywhere",
|
|
10
|
+
disableJsInjection: false,
|
|
11
|
+
runUrlRedirectExperiments: "browser",
|
|
12
|
+
runCrossOriginUrlRedirectExperiments: "browser",
|
|
13
|
+
injectRedirectUrlScript: true,
|
|
14
|
+
maxRedirects: 5,
|
|
15
|
+
scriptInjectionPattern: "</head>",
|
|
16
|
+
disableInjections: false,
|
|
17
|
+
enableStreaming: false,
|
|
18
|
+
enableStickyBucketing: false,
|
|
19
|
+
growthbook: {
|
|
20
|
+
apiHost: "",
|
|
21
|
+
clientKey: "",
|
|
22
|
+
},
|
|
23
|
+
persistUuid: false,
|
|
24
|
+
uuidCookieName: "gbuuid",
|
|
25
|
+
uuidKey: "id",
|
|
26
|
+
skipAutoAttributes: false,
|
|
27
|
+
},
|
|
28
|
+
helpers: {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface ConfigEnv {
|
|
32
|
+
PROXY_TARGET?: string;
|
|
33
|
+
FORWARD_PROXY_HEADERS?: string;
|
|
34
|
+
NODE_ENV?: string;
|
|
35
|
+
MAX_PAYLOAD_SIZE?: string;
|
|
36
|
+
|
|
37
|
+
ROUTES?: string;
|
|
38
|
+
|
|
39
|
+
RUN_VISUAL_EDITOR_EXPERIMENTS?: ExperimentRunEnvironment;
|
|
40
|
+
DISABLE_JS_INJECTION?: string;
|
|
41
|
+
|
|
42
|
+
RUN_URL_REDIRECT_EXPERIMENTS?: ExperimentRunEnvironment;
|
|
43
|
+
RUN_CROSS_ORIGIN_URL_REDIRECT_EXPERIMENTS?: ExperimentRunEnvironment;
|
|
44
|
+
INJECT_REDIRECT_URL_SCRIPT?: string;
|
|
45
|
+
MAX_REDIRECTS?: string;
|
|
46
|
+
|
|
47
|
+
SCRIPT_INJECTION_PATTERN?: string;
|
|
48
|
+
DISABLE_INJECTIONS?: string;
|
|
49
|
+
|
|
50
|
+
ENABLE_STREAMING?: string;
|
|
51
|
+
ENABLE_STICKY_BUCKETING?: string;
|
|
52
|
+
STICKY_BUCKET_PREFIX?: string;
|
|
53
|
+
|
|
54
|
+
CONTENT_SECURITY_POLICY?: string;
|
|
55
|
+
NONCE?: string;
|
|
56
|
+
|
|
57
|
+
GROWTHBOOK_API_HOST?: string;
|
|
58
|
+
GROWTHBOOK_CLIENT_KEY?: string;
|
|
59
|
+
GROWTHBOOK_DECRYPTION_KEY?: string;
|
|
60
|
+
GROWTHBOOK_TRACKING_CALLBACK?: string;
|
|
61
|
+
GROWTHBOOK_PAYLOAD?: string;
|
|
62
|
+
|
|
63
|
+
PERSIST_UUID?: string;
|
|
64
|
+
UUID_COOKIE_NAME?: string;
|
|
65
|
+
UUID_KEY?: string;
|
|
66
|
+
|
|
67
|
+
SKIP_AUTO_ATTRIBUTES?: string;
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
[key: string]: any;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
export function getConfig(env: ConfigEnv): Config {
|
|
75
|
+
const config = defaultContext.config;
|
|
76
|
+
|
|
77
|
+
config.proxyTarget = env.PROXY_TARGET ?? defaultContext.config.proxyTarget;
|
|
78
|
+
config.forwardProxyHeaders = ["true", "1"].includes(
|
|
79
|
+
env.FORWARD_PROXY_HEADERS ?? "" + defaultContext.config.forwardProxyHeaders,
|
|
80
|
+
);
|
|
81
|
+
config.environment = env.NODE_ENV ?? defaultContext.config.environment;
|
|
82
|
+
config.maxPayloadSize =
|
|
83
|
+
env.MAX_PAYLOAD_SIZE ?? defaultContext.config.maxPayloadSize;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
config.routes = JSON.parse(env.ROUTES || "[]");
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error("Error parsing ROUTES", e);
|
|
89
|
+
config.routes = [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
config.runVisualEditorExperiments = (env.RUN_VISUAL_EDITOR_EXPERIMENTS ??
|
|
93
|
+
defaultContext.config
|
|
94
|
+
.runVisualEditorExperiments) as ExperimentRunEnvironment;
|
|
95
|
+
config.disableJsInjection = ["true", "1"].includes(
|
|
96
|
+
env.DISABLE_JS_INJECTION ?? "" + defaultContext.config.disableJsInjection,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
config.runUrlRedirectExperiments = (env.RUN_URL_REDIRECT_EXPERIMENTS ??
|
|
100
|
+
defaultContext.config
|
|
101
|
+
.runUrlRedirectExperiments) as ExperimentRunEnvironment;
|
|
102
|
+
config.runCrossOriginUrlRedirectExperiments =
|
|
103
|
+
(env.RUN_CROSS_ORIGIN_URL_REDIRECT_EXPERIMENTS ??
|
|
104
|
+
defaultContext.config
|
|
105
|
+
.runCrossOriginUrlRedirectExperiments) as ExperimentRunEnvironment;
|
|
106
|
+
config.injectRedirectUrlScript = ["true", "1"].includes(
|
|
107
|
+
env.INJECT_REDIRECT_URL_SCRIPT ??
|
|
108
|
+
"" + defaultContext.config.injectRedirectUrlScript,
|
|
109
|
+
);
|
|
110
|
+
config.maxRedirects = parseInt(
|
|
111
|
+
env.MAX_REDIRECTS || "" + defaultContext.config.maxRedirects,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
config.scriptInjectionPattern =
|
|
115
|
+
env.SCRIPT_INJECTION_PATTERN ||
|
|
116
|
+
defaultContext.config.scriptInjectionPattern;
|
|
117
|
+
config.disableInjections = ["true", "1"].includes(
|
|
118
|
+
env.DISABLE_INJECTIONS ?? "" + defaultContext.config.disableInjections,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
config.enableStreaming = ["true", "1"].includes(
|
|
122
|
+
env.ENABLE_STREAMING ?? "" + defaultContext.config.enableStreaming,
|
|
123
|
+
);
|
|
124
|
+
config.enableStickyBucketing = ["true", "1"].includes(
|
|
125
|
+
env.ENABLE_STICKY_BUCKETING ??
|
|
126
|
+
"" + defaultContext.config.enableStickyBucketing,
|
|
127
|
+
);
|
|
128
|
+
"STICKY_BUCKET_PREFIX" in env &&
|
|
129
|
+
(config.stickyBucketPrefix = env.STICKY_BUCKET_PREFIX);
|
|
130
|
+
|
|
131
|
+
config.contentSecurityPolicy = env.CONTENT_SECURITY_POLICY || "";
|
|
132
|
+
// warning: for testing only; nonce should be unique per request
|
|
133
|
+
config.nonce = env.NONCE || undefined;
|
|
134
|
+
|
|
135
|
+
config.crypto = crypto;
|
|
136
|
+
|
|
137
|
+
// config.growthbook
|
|
138
|
+
config.growthbook.apiHost = (env.GROWTHBOOK_API_HOST ?? "").replace(
|
|
139
|
+
/\/*$/,
|
|
140
|
+
"",
|
|
141
|
+
);
|
|
142
|
+
config.growthbook.clientKey = env.GROWTHBOOK_CLIENT_KEY ?? "";
|
|
143
|
+
"GROWTHBOOK_DECRYPTION_KEY" in env &&
|
|
144
|
+
(config.growthbook.decryptionKey = env.GROWTHBOOK_DECRYPTION_KEY);
|
|
145
|
+
"GROWTHBOOK_TRACKING_CALLBACK" in env &&
|
|
146
|
+
(config.growthbook.trackingCallback = env.GROWTHBOOK_TRACKING_CALLBACK);
|
|
147
|
+
try {
|
|
148
|
+
"GROWTHBOOK_PAYLOAD" in env &&
|
|
149
|
+
(config.growthbook.payload = JSON.parse(env.GROWTHBOOK_PAYLOAD || ""));
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error("Error parsing GROWTHBOOK_PAYLOAD", e);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
config.persistUuid = ["true", "1"].includes(
|
|
155
|
+
env.PERSIST_UUID ?? "" + defaultContext.config.persistUuid,
|
|
156
|
+
);
|
|
157
|
+
config.uuidCookieName =
|
|
158
|
+
env.UUID_COOKIE_NAME || defaultContext.config.uuidCookieName;
|
|
159
|
+
config.uuidKey = env.UUID_KEY || defaultContext.config.uuidKey;
|
|
160
|
+
|
|
161
|
+
config.skipAutoAttributes = ["true", "1"].includes(
|
|
162
|
+
env.SKIP_AUTO_ATTRIBUTES ?? "" + defaultContext.config.skipAutoAttributes,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return config;
|
|
166
|
+
}
|