Skip to main content

GrowthBook Edge App for other providers
alpha

Overview

GrowthBook currently supports two levels of integration with most edge workers:

  1. Our turnkey Edge App

    • Automatically run server-side or hybrid Visual Experiments without redraw flicker.
    • Automatically run server-side or hybrid URL Redirect Experiments without flicker or delay.
    • Optionally inject the JavaScript SDK with hydrated payload, allowing the front-end to pick up where the edge left off without any extra network requests. We use an enhanced version of our HTML Script Tag for this purpose.
  2. Support for edge apps using our JavaScript SDK

References

  • Our base Edge App repository, which supports the above use cases, is here
  • You may find it useful to review our JavaScript SDK. Many of the concepts which apply to both on-edge and injected frontend SDKs are based on our JS SDK.

Turnkey Edge App

Our Edge App runs as a smart proxy layer between your application and your end users. In absence of Visual or URL Redirect experiments, the Edge App will simply proxy the user request to your site and return the response, optionally injecting a fully-bootstrapped JavaScript SDK onto the rendered HTML page. If the request URL matches an Visual or URL Redirect experiment and the targeting conditions are satisfied, the Edge App may also perform one or more URL redirects behind the scenes (the public-facing URL does not change) and/or mutate the DOM for Visual Experiments.

URL Redirects on edge

The Edge App defaults to running URL Redirect Experiments in the browser only. This is because edge redirects load a separate page's content without altering the URL. After the redirect, some sites may experience problems with loading assets or endpoints with relative paths.

You can enable URL Redirects on edge by setting environment variable RUN_URL_REDIRECT_EXPERIMENTS (see below).

Setting up our turnkey Edge App is usually simple for most JavaScript-based edge workers.

In this guide we do not discuss bootstrapping your project, but rather how to implement our Edge App once you have a basic edge worker environment set up.

Install the SDK

npm install --save @growthbook/edge-utils

Implement the Edge App request handler

A basic implementation of our Edge App only requires a few lines of code. Your implementation however will be vendor specific:

import { edgeApp, getConfig, defaultContext } from "@growthbook/edge-utils";

// Build a request handler
// note: we assume availability of `env` (environment variables)
export async function handler(request, env) {
const context = await init(env);
return edgeApp(context, request);
}

// Build context for the app
function init(env) {
// Automatically parse environment variables into app context
const context = defaultContext;
context.config = getConfig(env);
context.helpers = {
// define utility functions for request/response manipulation
};
return context;
}

You will need to write our own set of helper methods (context.helpers) that correspond to how your edge provider handles various request and response utilities.

Define helper methods

You will need to build helper methods based on the following interfaces:

{
// Get the full URL from the user request
getRequestURL?: (req: Req) => string;

// Get the request method (GET, POST, etc)
getRequestMethod?: (req: Req) => string;

// Get a specific header from the request
getRequestHeader?: (req: Req, key: string) => string | undefined;

// Construct or prepare a response for sending to the client
// note: For many vendors, this function should build a new response object
sendResponse?: (
ctx: Context<Req, Res>,
res?: Res, // Only needed for engines where res already exists
headers?: Record<string, any>,
body?: string,
cookies?: Record<string, string>, // Optionally use ctx.helpers.setCookie(resp, key, cookies[key]) to assign.
status?: number,
) => unknown;

// Fetch an external resource from the edge app
// note: For many vendors, you can ignore ctx
fetch?: (ctx: Context<Req, Res>, url: string) => Promise<Res>;

// For sending a pass-through response without Edge App interception
// note: For many vendors, this will map directly to helpers.fetch
proxyRequest?: (
ctx: Context<Req, Res>,
req: Req,
res?: Res,
next?: any,
) => Promise<unknown>;

// Get cookie from request (for UUID and StickyBucketing persistence)
getCookie?: (req: Req, key: string) => string;

// Set a cookie on response (unused unless you choose to persist on edge)
setCookie?: (res: Res, key: string, value: string) => void;
}

You may reference example implementations of these methods in our Cloudflare repository .

Configure the Edge App

Use a combination of environment variables and optional runtime configuration to add required fields and to customize the Edge App behavior.

Environment variables

Add these required fields, at minimum, to your environment variables:

PROXY_TARGET="https://internal.mysite.io"  # The non-edge URL to your website
GROWTHBOOK_API_HOST="https://cdn.growthbook.io"
GROWTHBOOK_CLIENT_KEY="sdk-abc123"
GROWTHBOOK_DECRYPTION_KEY="key_abc123" # Only include for encrypted SDK Connections

You may want to further customize the app. Here is a list of common customization variables:

# Disable or change the rendering behavior of Visual Experiments:
# ==========
RUN_VISUAL_EDITOR_EXPERIMENTS="everywhere"|"edge"|"browser"|"skip" # default: "everywhere"

# URL Redirect Experiments are disabled on edge by default. Because the URL does not change, some sites
# may experience problems with loading assets or endpoints with relative paths:
# ==========
RUN_URL_REDIRECT_EXPERIMENTS="everywhere"|"edge"|"browser"|"skip" # default: "browser"
RUN_CROSS_ORIGIN_URL_REDIRECT_EXPERIMENTS="everywhere"|"edge"|"browser"|"skip" # default: "browser"
# Mutate browser URL via window.history.replaceState() to reflect the new URL:
INJECT_REDIRECT_URL_SCRIPT="true" # default "true".

# Do not inject a bootstrapped JavaScript SDK onto the page:
# ==========
DISABLE_INJECTIONS="true" # default "false"


# Customize the edge or injected browser SDK behavior:
# ==========
ENABLE_STREAMING="true" # default "false". Streaming SSE updates on browser.
ENABLE_STICKY_BUCKETING="true" # default "false". Use cookie-based sticky bucketing on edge and browser.

Runtime configuration

You may want to provide context to your edge app at runtime rather than using environment variables. For example, if you have additional targeting attributes available, you may inject them by modifying your request handler code:

import { edgeApp, getConfig } from "@growthbook/edge-utils";

export async function handler(request, env) {
const context = await init(env);

const userId = context.helpers.getCookie(request, "userId");
context.config.attributes.userType = userId ? "logged in" : "anonymous";
return edgeApp(context, request);
}

More customization options

For a full list of customizations, view our Edge Utility repository .

Tracking Experiment Views

Running A/B tests requires a tracking callback. Our turnkey Edge App defaults to using built-in front-end tracking. The tracking call automatically integrates with Segment.io, GA4, and Google Tag Manager by using the mechanism outlined in our HTML Script Tag. In order to do this, the app keeps track of tracking calls triggered on edge and injects them into the front-end SDK to be automatically triggered on page load.

You may wish to either customize front-end tracking or switch to edge tracking (or use both concurrently if running hybrid edge + front-end experiments).

Why might you be interested in tracking on edge? Tracking on an edge or backend environment allows you to ensure the callback is fired before any differentiation across variations, eliminating experimental bias. While not eliminating this risk, the default injected front-end tracking introduced by our Edge App does reduce this risk relative to solely using a front-end SDK.

To change the front-end tracking callback, set the GROWTHBOOK_TRACKING_CALLBACK to your custom tracking JS code:

# todo: replace with your own tracking library
GROWTHBOOK_TRACKING_CALLBACK="(experiment, results) => { console.log('browser tracking callback', {experiment, results}); }"

To track on edge, you must inject your own tracking callback into the edge request handler code. Any experiments that run on edge will use the edge tracking callback and not the front-end callback (hybrid edge + front-end experiments being an exception):

import { edgeApp, getConfig } from "@growthbook/edge-utils";

export async function handler(request, env) {
const context = await init(env);

context.config.edgeTrackingCallback = (experiment, results) => {
// todo: replace with your tracking library
console.log('edge tracking callback', {experiment, results});
}
return edgeApp(context, request);
}

Targeting Attributes

The following targeting attributes are set automatically by the Edge App.

  • id - creates a long-lived gbuuid cookie if it doesn't exist already
  • url
  • path
  • host
  • query
  • pageTitle
  • deviceType - either mobile or desktop
  • browser - one of chrome, edge, firefox, safari, or unknown
  • utmSource
  • utmMedium
  • utmCampaign
  • utmTerm
  • utmContent

You can customize both the primary identifier name (id) and cookie name (gbuuid) by setting the UUID_KEY and UUID_COOKIE_NAME environment variables respectively.

As shown in the runtime configuration section above, you can also pass custom attributes via runtime config. You can also skip automatic attribute generation and rely solely on custom attributes by setting the environment variable SKIP_AUTO_ATTRIBUTES="true".

Routing

By default, the Edge App will process all GET requests (other HTTP verbs are proxied through without running through our app logic).

It is generally preferable to configure your routing rules outside of our Edge App. For instance, you may only want to invoke the Edge App at https://yourdomain.io/landing-page.

There may be situations when you will need to provide finer-grained routing / URL targeting rules within our Edge App. You will need to include a JSON encoded string of route rules in your ROUTES environment variable.

For instance, you may want to do a proxy pass-through (do not process) for mysite.io/account/* or mysite.io/settings/*. Your routes may look like this:

ROUTES='[{ "pattern":"mysite.io/account/*", "behavior":"proxy" }, { "pattern":"mysite.io/settings/*", "behavior":"proxy" }]'

A route uses the following interface, with many of the properties being optional:

{
pattern: string;
type?: "regex" | "simple"; // default: "simple"
behavior?: "intercept" | "proxy" | "error"; // default: "intercept"
includeFileExtensions?: boolean; // Include requests to filenames like "*.jpg". default: false (pass-through).
statusCode?: number; // Alter the status code (default is 404 when using "error")
body?: string; // Alter the body (for setting an error message body)
}

When multiple routes are included in your ROUTES array, only the first match is used.

By default, the Edge App will persist a random unique identifier in a first-party cookie named gbuuid. Its purpose is to provide a consistent user experience to your visitors by preventing them from being re-bucketed into different A/B test variations. It follows the same mechanism as discussed in our HTML Script Tag docs.

If you must delay persisting the gbuuid cookie until a user consents, you can set the environment variable NO_AUTO_COOKIES="true".

This will still generate a UUID for the user, but will not persist it. That means, if the user refreshes the page, they will have a new random UUID generated.environment

You have the option to manually persist this cookie at any time, for example when a user grants consent on your cookie banner. All you need to do is fire this custom event from javascript on the rendered page:

document.dispatchEvent(new CustomEvent("growthbookpersist"));
note

If you are using Sticky Bucketing, a persistent sticky bucket assignments cookie will automatically be generated. If you require user permission before writing cookies, you should:

  • Either do not enable Sticky Bucketing on edge (do not use ENABLE_STICKY_BUCKETING)
  • Or only enable Sticky Bucketing per each user via runtime configuration. (only pass config.enableStickyBucketing: true if user has consented — identifiable by checking for presence of the gbuuid cookie).

Manual SDK Integration on Edge

You may be interested in building your own edge application using the GrowthBook SDK and not using our turnkey Edge App. Or you may want to do custom feature flagging on specific routes while running our Edge App on other routes.

To use the GrowthBook on edge, simply include our standard JavaScript SDK (@growthbook/growthbook NPM package).

import { GrowthBook, setPolyfills } from "@growthbook/growthbook";

export default {
async fetch(request) {
// 1. Init the GrowthBook SDK and choose an optional caching strategy

// A. Use the KV as a managed payload store to eliminate SDK requests to the GrowthBook API entirely.
// Requires setting up an SDK Webhook.
const payload = await getPayloadFromProvider(env); // not implemented, build your own
const growthbook = new GrowthBook(gbContext);
await growthbook.init({ payload: payload });

// B. Or provide a KV cache layer so that the GrowthBook SDK doesn't need to make as many requests
// to the GrowthBook API. No SDK Webhook needed.
const localStoragePolyfill = getLocalStoragePolyfill(env); // not implemented, build your own
setPolyfills({ localStorage: localStoragePolyfill });
await growthbook.init();

// 2. Start feature flagging
if (growthbook.isOn("my-feature")) {
return new Response("<h1>foo</h1>");
}
return new Response("<h1>bar</h1>");
}
}

Payload Caching via edge datastore

By default, the Edge App will make a network request to the GrowthBook API on each user request in order to fetch the current feature and experiment values. This is a blocking call that delays page delivery. There is an in-memory short-lived cache layer on this call, but it won't always protect you.

If you have access to a distributed key-val store on your edge, you can likely overcome this problem. There are 2 levels of key-val integration available:

  1. You can either completely eliminate the blocking call to the GrowthBook API by implementing a GrowthBook-to-edge-keyval push model via SDK Webhooks.
  2. Alternatively, you can eliminate most of these network requests by using an edge key-val store as a just-in-time payload cache.

You can also use these strategies in your own manual SDK integration.

We are unable to offer specific guidance about how to configure or connect to your key-val store because the details vary heavily by vendor. For inspiration, you can see how we've addressed key-val implementation with Cloudflare Workers in our Cloudflare docs.

Configuring a SDK Webhook

For key-val stored payloads (1), we eliminate network requests from edge to GrowthBook by using a GrowthBook SDK Webhook to push the SDK payload to the key-val store on change.

  1. Create an SDK Webhook on the same SDK Connection that you are using for edge integration.

  2. Set the Endpoint URL to your key-val provider's REST API endpoint. However, not all edge vendors will have a public REST endpoint for setting the key-val cache. You may need to build your own endpoint to handle the webhook, in which case webhook verification may be important.

  3. Enable the Send Payload toggle.

  4. Change the Method to PUT (or whichever verb is required by your vendor or endpoint).

  5. Add any vendor-specific authorization headers and/or and content-type headers. This may not be required depending on your specific vendor:

{
"Authorization": "Bearer YOUR_REST_API_TOKEN",
"Content-Type": "application/json"
}

Now whenever feature and experiment values change, your edge worker will have immediate access to the latest values. You can also test the webhook by using the "Test Webhook" button on the SDK Connection page.