@growthbook/edge-utils 0.1.7 → 0.2.0
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/README.md +23 -1
- package/dist/app.d.ts +1 -1
- package/dist/app.js +161 -60
- package/dist/app.js.map +1 -1
- package/dist/attributes.d.ts +1 -1
- package/dist/attributes.js +5 -6
- package/dist/attributes.js.map +1 -1
- package/dist/config.d.ts +8 -1
- package/dist/config.js +44 -19
- package/dist/config.js.map +1 -1
- package/dist/domMutations.d.ts +5 -2
- package/dist/domMutations.js +21 -7
- package/dist/domMutations.js.map +1 -1
- package/dist/generated/sdkWrapper.d.ts +1 -1
- package/dist/generated/sdkWrapper.js +1 -1
- package/dist/generated/sdkWrapper.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inject.d.ts +5 -4
- package/dist/inject.js +6 -6
- package/dist/inject.js.map +1 -1
- package/dist/redirect.d.ts +4 -4
- package/dist/redirect.js +4 -4
- package/dist/redirect.js.map +1 -1
- package/dist/routing.d.ts +1 -1
- package/dist/routing.js +8 -8
- package/dist/routing.js.map +1 -1
- package/dist/stickyBucketService.js +1 -1
- package/dist/stickyBucketService.js.map +1 -1
- package/dist/types.d.ts +57 -7
- package/package.json +8 -6
- package/src/app.ts +177 -91
- package/src/attributes.ts +2 -3
- package/src/config.ts +42 -2
- package/src/domMutations.ts +16 -4
- package/src/generated/sdkWrapper.ts +1 -1
- package/src/index.ts +9 -0
- package/src/inject.ts +10 -7
- package/src/redirect.ts +6 -6
- package/src/routing.ts +9 -7
- package/src/types.ts +60 -7
package/src/app.ts
CHANGED
|
@@ -12,6 +12,15 @@ import { applyDomMutations } from "./domMutations";
|
|
|
12
12
|
import redirect from "./redirect";
|
|
13
13
|
import { getRoute } from "./routing";
|
|
14
14
|
import { EdgeStickyBucketService } from "./stickyBucketService";
|
|
15
|
+
import { HTMLElement, parse } from "node-html-parser";
|
|
16
|
+
import pako from "pako";
|
|
17
|
+
|
|
18
|
+
interface OriginResponse {
|
|
19
|
+
status: number;
|
|
20
|
+
headers: Record<string, string | undefined>;
|
|
21
|
+
text: () => Promise<string>;
|
|
22
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
23
|
+
}
|
|
15
24
|
|
|
16
25
|
export async function edgeApp<Req, Res>(
|
|
17
26
|
context: Context<Req, Res>,
|
|
@@ -20,68 +29,78 @@ export async function edgeApp<Req, Res>(
|
|
|
20
29
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
30
|
next?: any,
|
|
22
31
|
) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 1. Init app variables
|
|
34
|
+
*/
|
|
35
|
+
// Request vars:
|
|
36
|
+
let requestUrl = context.helpers.getRequestURL(req);
|
|
37
|
+
let originUrl = getOriginUrl(context, requestUrl);
|
|
38
|
+
// Response vars:
|
|
39
|
+
let originResponse: (OriginResponse & Res) | undefined = undefined;
|
|
40
|
+
let resHeaders: Record<string, string | undefined> = {};
|
|
41
|
+
const respCookies: Record<string, string> = {};
|
|
42
|
+
const setRespCookie = (key: string, value: string) => {
|
|
43
|
+
respCookies[key] = value;
|
|
28
44
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
// Initial hook:
|
|
46
|
+
let hookResp: Res | undefined | void;
|
|
47
|
+
hookResp = await context?.hooks?.onRequest?.({ context, req, res, next, requestUrl, originUrl });
|
|
48
|
+
if (hookResp) return hookResp;
|
|
49
|
+
|
|
50
|
+
// DOM mutations
|
|
51
|
+
let domChanges: AutoExperimentVariation[] = [];
|
|
52
|
+
const resetDomChanges = () => (domChanges = []);
|
|
53
|
+
|
|
54
|
+
// Experiments that triggered prior to final redirect
|
|
55
|
+
let preRedirectChangeIds: string[] = [];
|
|
56
|
+
const setPreRedirectChangeIds = (changeIds: string[]) =>
|
|
57
|
+
(preRedirectChangeIds = changeIds);
|
|
38
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 2. Early exits based on method, routes, etc
|
|
61
|
+
*/
|
|
39
62
|
// Non GET requests are proxied
|
|
40
|
-
if (context.helpers.getRequestMethod
|
|
41
|
-
return context.helpers.proxyRequest
|
|
63
|
+
if (context.helpers.getRequestMethod(req) !== "GET") {
|
|
64
|
+
return context.helpers.proxyRequest(context, req, res, next);
|
|
42
65
|
}
|
|
43
66
|
// Check the url for routing rules (default behavior is intercept)
|
|
44
|
-
const route = getRoute(context
|
|
67
|
+
const route = getRoute(context, requestUrl);
|
|
45
68
|
if (route.behavior === "error") {
|
|
46
|
-
return context.helpers.sendResponse
|
|
47
|
-
context,
|
|
48
|
-
res,
|
|
49
|
-
headers,
|
|
50
|
-
route.body || "",
|
|
51
|
-
cookies,
|
|
52
|
-
route.statusCode,
|
|
53
|
-
);
|
|
69
|
+
return context.helpers.sendResponse(context, res, {}, route.body || "", {}, route.statusCode);
|
|
54
70
|
}
|
|
55
71
|
if (route.behavior === "proxy") {
|
|
56
|
-
return context.helpers.proxyRequest
|
|
72
|
+
return context.helpers.proxyRequest(context, req, res, next);
|
|
57
73
|
}
|
|
74
|
+
// Custom route behavior via hook:
|
|
75
|
+
hookResp = await context?.hooks?.onRoute?.({ context, req, res, next, requestUrl, originUrl, route });
|
|
76
|
+
if (hookResp) return hookResp;
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
78
|
+
/**
|
|
79
|
+
* 3. User attributes & uuid
|
|
80
|
+
*/
|
|
81
|
+
const attributes = getUserAttributes(context, req, requestUrl, setRespCookie);
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
// Hook to allow enriching user attributes, etc
|
|
84
|
+
hookResp = await context?.hooks?.onUserAttributes?.({ context, req, res, next, requestUrl, originUrl, route, attributes });
|
|
85
|
+
if (hookResp) return hookResp;
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
/**
|
|
88
|
+
* 4. Init GrowthBook SDK
|
|
89
|
+
*/
|
|
90
|
+
setPolyfills({
|
|
91
|
+
localStorage: context.config?.localStorage,
|
|
92
|
+
SubtleCrypto: context.config?.crypto,
|
|
93
|
+
});
|
|
72
94
|
if (context.config.staleTTL !== undefined)
|
|
73
95
|
configureCache({ staleTTL: context.config.staleTTL });
|
|
74
96
|
if (context.config.fetchFeaturesCall)
|
|
75
97
|
helpers.fetchFeaturesCall = context.config.fetchFeaturesCall;
|
|
76
98
|
|
|
77
|
-
let stickyBucketService:
|
|
78
|
-
| EdgeStickyBucketService<Req, Res>
|
|
79
|
-
| StickyBucketService
|
|
80
|
-
| undefined = undefined;
|
|
99
|
+
let stickyBucketService: EdgeStickyBucketService<Req, Res> | StickyBucketService | undefined;
|
|
81
100
|
if (context.config.enableStickyBucketing) {
|
|
82
101
|
stickyBucketService =
|
|
83
102
|
context.config.edgeStickyBucketService ??
|
|
84
|
-
new EdgeStickyBucketService
|
|
103
|
+
new EdgeStickyBucketService({
|
|
85
104
|
context,
|
|
86
105
|
prefix: context.config.stickyBucketPrefix,
|
|
87
106
|
req,
|
|
@@ -94,9 +113,10 @@ export async function edgeApp<Req, Res>(
|
|
|
94
113
|
attributes,
|
|
95
114
|
applyDomChangesCallback: (changes: AutoExperimentVariation) => {
|
|
96
115
|
domChanges.push(changes);
|
|
97
|
-
return () => {
|
|
116
|
+
return () => {
|
|
117
|
+
};
|
|
98
118
|
},
|
|
99
|
-
url,
|
|
119
|
+
url: requestUrl,
|
|
100
120
|
disableVisualExperiments: ["skip", "browser"].includes(
|
|
101
121
|
context.config.runVisualEditorExperiments,
|
|
102
122
|
),
|
|
@@ -115,81 +135,140 @@ export async function edgeApp<Req, Res>(
|
|
|
115
135
|
payload: context.config.payload,
|
|
116
136
|
});
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
// Hook to perform any custom logic given the initialized SDK
|
|
139
|
+
hookResp = await context?.hooks?.onGrowthbookInit?.({ context, req, res, next, requestUrl, originUrl, route, attributes, growthbook });
|
|
140
|
+
if (hookResp) return hookResp;
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 5. Run URL redirect tests before fetching from origin
|
|
145
|
+
*/
|
|
146
|
+
const redirectRequestUrl = await redirect({
|
|
147
|
+
context,
|
|
121
148
|
req,
|
|
122
|
-
|
|
149
|
+
setRespCookie,
|
|
123
150
|
growthbook,
|
|
124
|
-
previousUrl:
|
|
151
|
+
previousUrl: requestUrl,
|
|
125
152
|
resetDomChanges,
|
|
126
153
|
setPreRedirectChangeIds: setPreRedirectChangeIds,
|
|
127
154
|
});
|
|
155
|
+
originUrl = getOriginUrl(context, redirectRequestUrl);
|
|
128
156
|
|
|
129
|
-
|
|
157
|
+
// Pre-origin-fetch hook (after redirect logic):
|
|
158
|
+
hookResp = await context?.hooks?.onBeforeOriginFetch?.({ context, req, res, next, requestUrl, redirectRequestUrl, originUrl, route, attributes, growthbook });
|
|
159
|
+
if (hookResp) return hookResp;
|
|
130
160
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
| undefined = undefined;
|
|
161
|
+
/**
|
|
162
|
+
* 6. Fetch from origin, parse body / DOM
|
|
163
|
+
*/
|
|
135
164
|
try {
|
|
136
|
-
|
|
137
|
-
context
|
|
165
|
+
originResponse = await context.helpers.fetch(
|
|
166
|
+
context,
|
|
138
167
|
originUrl,
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
const status = parseInt(fetchedResponse.status ? fetchedResponse.status + "" : "400");
|
|
142
|
-
if (status >= 500) {
|
|
143
|
-
console.error("Fetch: 5xx status returned");
|
|
144
|
-
return context.helpers.sendResponse?.(
|
|
145
|
-
context,
|
|
146
|
-
res,
|
|
147
|
-
headers,
|
|
148
|
-
"Error fetching page",
|
|
149
|
-
cookies,
|
|
150
|
-
500,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
if (status >= 400) {
|
|
154
|
-
return context.helpers.proxyRequest?.(context, req, res, next);
|
|
155
|
-
}
|
|
168
|
+
req,
|
|
169
|
+
) as OriginResponse & Res;
|
|
156
170
|
} catch (e) {
|
|
157
171
|
console.error(e);
|
|
158
|
-
return context.helpers.sendResponse?.(
|
|
159
|
-
context,
|
|
160
|
-
res,
|
|
161
|
-
headers,
|
|
162
|
-
"Error fetching page",
|
|
163
|
-
cookies,
|
|
164
|
-
500,
|
|
165
|
-
);
|
|
166
172
|
}
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
const originStatus = originResponse ? parseInt(originResponse.status ? originResponse.status + "" : "400") : 500;
|
|
174
|
+
|
|
175
|
+
// On fetch hook (for custom response processing, etc)
|
|
176
|
+
hookResp = await context?.hooks?.onOriginFetch?.({ context, req, res, next, requestUrl, redirectRequestUrl, originUrl, route, attributes, growthbook, originResponse, originStatus });
|
|
177
|
+
if (hookResp) return hookResp;
|
|
178
|
+
|
|
179
|
+
// Standard error response handling
|
|
180
|
+
if (originStatus >= 500 || !originResponse) {
|
|
181
|
+
console.error("Fetch: 5xx status returned");
|
|
182
|
+
return context.helpers.sendResponse(context, res, {}, "Error fetching page", {}, 500);
|
|
183
|
+
}
|
|
184
|
+
if (originStatus >= 400) {
|
|
185
|
+
return originResponse;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Got a valid response, begin processing
|
|
189
|
+
const originHeaders = headersToObject(originResponse.headers);
|
|
190
|
+
if (context.config.forwardProxyHeaders) {
|
|
191
|
+
resHeaders = { ...originHeaders, ...resHeaders };
|
|
192
|
+
}
|
|
193
|
+
// At minimum, the content-type is forwarded
|
|
194
|
+
resHeaders["content-type"] = originHeaders?.["content-type"];
|
|
195
|
+
|
|
196
|
+
if (context.config.useDefaultContentType && !resHeaders["content-type"]) {
|
|
197
|
+
resHeaders["content-type"] = "text/html";
|
|
198
|
+
}
|
|
199
|
+
if (context.config.processTextHtmlOnly && !(resHeaders["content-type"] ?? "").includes("text/html")) {
|
|
200
|
+
return context.helpers.proxyRequest(context, req, res, next);
|
|
169
201
|
}
|
|
170
|
-
body = await fetchedResponse.text();
|
|
171
202
|
|
|
172
|
-
|
|
203
|
+
const { csp, nonce } = getCspInfo(context);
|
|
204
|
+
if (csp) {
|
|
205
|
+
resHeaders["content-security-policy"] = csp;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let body: string = "";
|
|
209
|
+
try {
|
|
210
|
+
// Check if content-encoding is gzip
|
|
211
|
+
if (originHeaders["content-encoding"] === "gzip") {
|
|
212
|
+
const buffer = await originResponse.arrayBuffer();
|
|
213
|
+
body = pako.inflate(new Uint8Array(buffer), { to: "string" });
|
|
214
|
+
delete resHeaders["content-encoding"]; // do not forward this header since it's now unzipped
|
|
215
|
+
} else {
|
|
216
|
+
body = await originResponse?.text() ?? "";
|
|
217
|
+
}
|
|
218
|
+
} catch(e) {
|
|
219
|
+
console.error(e);
|
|
220
|
+
}
|
|
221
|
+
let setBody = (s: string) => {
|
|
222
|
+
body = s;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let root: HTMLElement | undefined;
|
|
226
|
+
if (context.config.alwaysParseDOM) {
|
|
227
|
+
root = parse(body);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Body ready hook (pre-DOM-mutations):
|
|
231
|
+
hookResp = await context?.hooks?.onBodyReady?.({ context, req, res, next, requestUrl, redirectRequestUrl, originUrl, route, attributes, growthbook, originResponse, originStatus, originHeaders, resHeaders, body, setBody, root });
|
|
232
|
+
if (hookResp) return hookResp;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 7. Apply visual editor DOM mutations
|
|
236
|
+
*/
|
|
237
|
+
await applyDomMutations({
|
|
173
238
|
body,
|
|
239
|
+
setBody,
|
|
240
|
+
root,
|
|
174
241
|
nonce,
|
|
175
242
|
domChanges,
|
|
176
243
|
});
|
|
177
244
|
|
|
178
|
-
|
|
179
|
-
|
|
245
|
+
/**
|
|
246
|
+
* 8. Inject the client-facing GrowthBook SDK (auto-wrapper)
|
|
247
|
+
*/
|
|
248
|
+
injectScript({
|
|
249
|
+
context,
|
|
180
250
|
body,
|
|
251
|
+
setBody,
|
|
181
252
|
nonce,
|
|
182
253
|
growthbook,
|
|
183
254
|
attributes,
|
|
184
255
|
preRedirectChangeIds,
|
|
185
|
-
url,
|
|
186
|
-
oldUrl,
|
|
256
|
+
url: redirectRequestUrl,
|
|
257
|
+
oldUrl: requestUrl,
|
|
187
258
|
});
|
|
188
259
|
|
|
189
|
-
|
|
260
|
+
// Final hook (post-mutations) before sending back
|
|
261
|
+
hookResp = await context?.hooks?.onBeforeResponse?.({ context, req, res, next, requestUrl, redirectRequestUrl, originUrl, route, attributes, growthbook, originResponse, originStatus, originHeaders, resHeaders, body, setBody });
|
|
262
|
+
if (hookResp) return hookResp;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 9. Send mutated response
|
|
266
|
+
*/
|
|
267
|
+
return context.helpers.sendResponse(context, res, resHeaders, body, respCookies);
|
|
190
268
|
}
|
|
191
269
|
|
|
192
|
-
|
|
270
|
+
|
|
271
|
+
export function getOriginUrl<Req, Res>(context: Context<Req, Res>, currentURL: string): string {
|
|
193
272
|
const proxyTarget = context.config.proxyTarget;
|
|
194
273
|
const currentParsedURL = new URL(currentURL);
|
|
195
274
|
const proxyParsedURL = new URL(proxyTarget);
|
|
@@ -220,3 +299,10 @@ export function getOriginUrl(context: Context, currentURL: string): string {
|
|
|
220
299
|
|
|
221
300
|
return newURL;
|
|
222
301
|
}
|
|
302
|
+
|
|
303
|
+
function headersToObject(headers: any) {
|
|
304
|
+
if (headers && typeof headers.entries === "function") {
|
|
305
|
+
return Object.fromEntries(headers.entries());
|
|
306
|
+
}
|
|
307
|
+
return headers || {};
|
|
308
|
+
}
|
package/src/attributes.ts
CHANGED
|
@@ -6,7 +6,7 @@ export function getUserAttributes<Req, Res>(
|
|
|
6
6
|
ctx: Context<Req, Res>,
|
|
7
7
|
req: Req,
|
|
8
8
|
url: string,
|
|
9
|
-
|
|
9
|
+
setRespCookie: (key: string, value: string) => void,
|
|
10
10
|
): Attributes {
|
|
11
11
|
const { config, helpers } = ctx;
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ export function getUserAttributes<Req, Res>(
|
|
|
20
20
|
if (!helpers?.setCookie) {
|
|
21
21
|
throw new Error("Missing required dependencies");
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
setRespCookie(config.uuidCookieName, uuid);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const autoAttributes = getAutoAttributes(ctx, req, url);
|
|
@@ -34,7 +34,6 @@ export function getUUID<Req, Res>(ctx: Context<Req, Res>, req: Req) {
|
|
|
34
34
|
const { config, helpers } = ctx;
|
|
35
35
|
|
|
36
36
|
const crypto = config?.crypto || globalThis?.crypto;
|
|
37
|
-
|
|
38
37
|
if (!crypto || !helpers?.getCookie) {
|
|
39
38
|
throw new Error("Missing required dependencies");
|
|
40
39
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { Config, Context, ExperimentRunEnvironment } from "./types";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type Req = any; // placeholder
|
|
4
|
+
type Res = any; // placeholder
|
|
5
|
+
|
|
6
|
+
export const defaultContext: Context<Req, Res> = {
|
|
4
7
|
config: {
|
|
5
8
|
proxyTarget: "/",
|
|
6
9
|
forwardProxyHeaders: true,
|
|
10
|
+
followRedirects: true,
|
|
11
|
+
useDefaultContentType: false,
|
|
12
|
+
processTextHtmlOnly: true,
|
|
7
13
|
environment: "production",
|
|
8
14
|
maxPayloadSize: "2mb",
|
|
9
15
|
runVisualEditorExperiments: "everywhere",
|
|
10
16
|
disableJsInjection: false,
|
|
17
|
+
alwaysParseDOM: false,
|
|
11
18
|
runUrlRedirectExperiments: "browser",
|
|
12
19
|
runCrossOriginUrlRedirectExperiments: "browser",
|
|
13
20
|
injectRedirectUrlScript: true,
|
|
@@ -24,12 +31,32 @@ export const defaultContext: Context = {
|
|
|
24
31
|
uuidKey: "id",
|
|
25
32
|
skipAutoAttributes: false,
|
|
26
33
|
},
|
|
27
|
-
helpers: {
|
|
34
|
+
helpers: {
|
|
35
|
+
getRequestURL: function(req: Req): string {
|
|
36
|
+
throw new Error("getRequestURL not implemented");
|
|
37
|
+
},
|
|
38
|
+
getRequestMethod: function(req: Req): string {
|
|
39
|
+
throw new Error("getRequestMethod not implemented");
|
|
40
|
+
},
|
|
41
|
+
sendResponse: function(ctx: Context<Req, Res>, res?: any, headers?: Record<string, any> | undefined, body?: string | undefined, cookies?: Record<string, string> | undefined, status?: number | undefined): unknown {
|
|
42
|
+
throw new Error("sendResponse not implemented");
|
|
43
|
+
},
|
|
44
|
+
fetch: function(ctx: Context<Req, Res>, url: string, req: Req): Promise<Res> {
|
|
45
|
+
throw new Error("fetchFn not implemented");
|
|
46
|
+
},
|
|
47
|
+
proxyRequest: function(ctx: Context<Req, Res>, req: Req, res?: any, next?: any): Promise<unknown> {
|
|
48
|
+
throw new Error("proxyRequest not implemented");
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
hooks: {},
|
|
28
52
|
};
|
|
29
53
|
|
|
30
54
|
export interface ConfigEnv {
|
|
31
55
|
PROXY_TARGET?: string;
|
|
32
56
|
FORWARD_PROXY_HEADERS?: string;
|
|
57
|
+
FOLLOW_REDIRECTS?: string;
|
|
58
|
+
USE_DEFAULT_CONTENT_TYPE?: string;
|
|
59
|
+
PROCESS_TEXT_HTML_ONLY?: string;
|
|
33
60
|
NODE_ENV?: string;
|
|
34
61
|
MAX_PAYLOAD_SIZE?: string;
|
|
35
62
|
|
|
@@ -37,6 +64,7 @@ export interface ConfigEnv {
|
|
|
37
64
|
|
|
38
65
|
RUN_VISUAL_EDITOR_EXPERIMENTS?: ExperimentRunEnvironment;
|
|
39
66
|
DISABLE_JS_INJECTION?: string;
|
|
67
|
+
ALWAYS_PARSE_DOM?: string;
|
|
40
68
|
|
|
41
69
|
RUN_URL_REDIRECT_EXPERIMENTS?: ExperimentRunEnvironment;
|
|
42
70
|
RUN_CROSS_ORIGIN_URL_REDIRECT_EXPERIMENTS?: ExperimentRunEnvironment;
|
|
@@ -79,6 +107,15 @@ export function getConfig(env: ConfigEnv): Config {
|
|
|
79
107
|
config.forwardProxyHeaders = ["true", "1"].includes(
|
|
80
108
|
env.FORWARD_PROXY_HEADERS ?? "" + defaultContext.config.forwardProxyHeaders,
|
|
81
109
|
);
|
|
110
|
+
config.followRedirects = ["true", "1"].includes(
|
|
111
|
+
env.FOLLOW_REDIRECTS ?? "" + defaultContext.config.followRedirects,
|
|
112
|
+
);
|
|
113
|
+
config.useDefaultContentType = ["true", "1"].includes(
|
|
114
|
+
env.USE_DEFAULT_CONTENT_TYPE ?? "" + defaultContext.config.useDefaultContentType,
|
|
115
|
+
);
|
|
116
|
+
config.processTextHtmlOnly = ["true", "1"].includes(
|
|
117
|
+
env.PROCESS_TEXT_HTML_ONLY ?? "" + defaultContext.config.processTextHtmlOnly,
|
|
118
|
+
);
|
|
82
119
|
config.environment = env.NODE_ENV ?? defaultContext.config.environment;
|
|
83
120
|
config.maxPayloadSize =
|
|
84
121
|
env.MAX_PAYLOAD_SIZE ?? defaultContext.config.maxPayloadSize;
|
|
@@ -96,6 +133,9 @@ export function getConfig(env: ConfigEnv): Config {
|
|
|
96
133
|
config.disableJsInjection = ["true", "1"].includes(
|
|
97
134
|
env.DISABLE_JS_INJECTION ?? "" + defaultContext.config.disableJsInjection,
|
|
98
135
|
);
|
|
136
|
+
config.alwaysParseDOM = ["true", "1"].includes(
|
|
137
|
+
env.ALWAYS_PARSE_DOM ?? "" + defaultContext.config.alwaysParseDOM,
|
|
138
|
+
);
|
|
99
139
|
|
|
100
140
|
config.runUrlRedirectExperiments = (env.RUN_URL_REDIRECT_EXPERIMENTS ??
|
|
101
141
|
defaultContext.config
|
package/src/domMutations.ts
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import { AutoExperimentVariation, DOMMutation } from "@growthbook/growthbook";
|
|
2
|
-
import { parse } from "node-html-parser";
|
|
2
|
+
import { HTMLElement, parse } from "node-html-parser";
|
|
3
3
|
|
|
4
4
|
export async function applyDomMutations({
|
|
5
5
|
body,
|
|
6
|
+
setBody,
|
|
7
|
+
root,
|
|
6
8
|
nonce,
|
|
7
9
|
domChanges,
|
|
8
10
|
}: {
|
|
9
11
|
body: string;
|
|
12
|
+
setBody: (s: string) => void;
|
|
13
|
+
root?: HTMLElement;
|
|
10
14
|
nonce?: string;
|
|
11
15
|
domChanges: AutoExperimentVariation[];
|
|
12
16
|
}) {
|
|
13
|
-
if (!domChanges.length) return
|
|
17
|
+
if (!domChanges.length) return;
|
|
18
|
+
root = root ?? parse(body);
|
|
19
|
+
if (!root) return;
|
|
14
20
|
|
|
15
|
-
const root = parse(body);
|
|
16
21
|
const headEl = root.querySelector("head");
|
|
17
22
|
|
|
18
23
|
domChanges.forEach(({ domMutations, css, js }) => {
|
|
19
24
|
if (css) {
|
|
20
25
|
const parentEl = headEl || root;
|
|
26
|
+
if (!parentEl) return;
|
|
21
27
|
const el = parse(`<style>${css}</style>`);
|
|
22
28
|
parentEl.appendChild(el);
|
|
23
29
|
}
|
|
@@ -86,9 +92,11 @@ export async function applyDomMutations({
|
|
|
86
92
|
});
|
|
87
93
|
|
|
88
94
|
body = root.toString();
|
|
89
|
-
|
|
95
|
+
setBody(body);
|
|
96
|
+
return;
|
|
90
97
|
|
|
91
98
|
function html(selector: string, cb: (val: string) => string) {
|
|
99
|
+
if (!root) return;
|
|
92
100
|
const els = root.querySelectorAll(selector);
|
|
93
101
|
els.map((el) => {
|
|
94
102
|
el.innerHTML = cb(el.innerHTML);
|
|
@@ -96,6 +104,7 @@ export async function applyDomMutations({
|
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
function classes(selector: string, cb: (val: Set<string>) => void) {
|
|
107
|
+
if (!root) return;
|
|
99
108
|
const els = root.querySelectorAll(selector);
|
|
100
109
|
els.map((el) => {
|
|
101
110
|
const classList = new Set(el.classNames);
|
|
@@ -109,6 +118,7 @@ export async function applyDomMutations({
|
|
|
109
118
|
attr: string,
|
|
110
119
|
cb: (val: string | null) => string | null,
|
|
111
120
|
) {
|
|
121
|
+
if (!root) return;
|
|
112
122
|
const validAttributeName = /^[a-zA-Z:_][a-zA-Z0-9:_.-]*$/;
|
|
113
123
|
if (!validAttributeName.test(attr)) {
|
|
114
124
|
return;
|
|
@@ -139,8 +149,10 @@ export async function applyDomMutations({
|
|
|
139
149
|
selector: string,
|
|
140
150
|
cb: () => { insertBeforeSelector?: string; parentSelector: string },
|
|
141
151
|
) {
|
|
152
|
+
if (!root) return;
|
|
142
153
|
const els = root.querySelectorAll(selector);
|
|
143
154
|
els.map((el) => {
|
|
155
|
+
if (!root) return;
|
|
144
156
|
const { insertBeforeSelector, parentSelector } = cb();
|
|
145
157
|
const parent = root.querySelector(parentSelector);
|
|
146
158
|
const insertBefore = insertBeforeSelector
|