Fastly Compute Edge App & SDK beta
Overview
GrowthBook currently supports two levels of integration with most edge workers, including Fastly:
-
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.
-
Support for edge apps using our JavaScript SDK
- Enhanced support and examples for using our JavaScript SDK in an edge environment
Regardless of your use case, our Fastly integration makes easy to synchronize feature and experiment values between GrowthBook and Fastly's KV store. This eliminates the network request to the GrowthBook API, unlocking blazingly fast edge-side and client-side SDK performance.
References
- Our Fastly Compute SDK repository, which supports the above use cases, is here
- A turnkey implementation of the Edge App (compatible with Viceroy) 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.
Worker Configuration
This tutorial assumes some familiarity with building and deploying Fastly Compute applications. You can quickly get up to speed by following the Fastly Compute Developer guide .
You may either use our turnkey Edge App for Fastly Compute or build your own app from scratch using our JavaScript and Fastly SDKs.
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.
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).
By default, Fastly allows 50ms of CPU time per request. When running Visual Experiments on edge with Fastly, it is common to see CPU time exceed the allotted 50ms. If your responses exceed 50ms and you receive a 503 error, you have a few options:
- Upgrade your Fastly Compute account to support longer CPU time.
- Or set the environment variable
RUN_VISUAL_EDITOR_EXPERIMENTS="browser"
. Users will still receive a flicker-free experience because the bootstrapped SDK and DOM mutations are injected into the page<head>
and triggered immediately on page load.
Setting up our turnkey Edge App is simple. Assuming that you have a basic Fastly Compute service set up, simply install the SDK and implement our custom request handler. Or if you prefer, you may pull down our fully-functional example implementation and follow along.
Install the SDK
- npm
- Yarn
- pnpm
npm install --save @growthbook/edge-fastly
yarn add @growthbook/edge-fastly
pnpm add @growthbook/edge-fastly
Implement the Edge App request handler
A basic implementation of our Edge App only requires a few lines of code:
/// <reference types="@fastly/js-compute" />
import { ConfigStore } from "fastly:config-store";
import { gbHandleRequest, getConfigEnvFromStore } from "@growthbook/edge-fastly";
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const envVarsStore = new ConfigStore("env_vars");
const env = getConfigEnvFromStore(envVarsStore);
const config = {
apiHostBackend: "api_host", // Name of Fastly backend pointing to your GrowthBook API Endpoint
backends: { "https://internal.mysite.io": "my_site" }, // Map of proxy origins to named Fastly backends
};
return await gbHandleRequest(event.request, env, config);
}
Notice a few references to backends. We will define these in the Fastly Dashboard in the subsequent doc section.
Set up backends (origins)
Unless your have requested open proxy behavior on your Fastly account, Fastly requires that you define backends for each origin server that your Compute application fetches from. In order to use our Edge app, you will need to create a backend for your GrowthBook API Host (which we'll call api_host
), and one or more backends for your origin site using the Fastly dashboard (called "Origins" in your Compute service configuration).
Each backend is defined as an origin URL. Example: https://internal.mysite.io
or https://internal.mobile.mysite.io
; but not full URLs like https//internal.mysite.io/features/widget
.
- In Fastly, create a backend called
api_host
pointing to your GrowthBook API Host. For GrowthBook Cloud customers, this will behttps://cdn.growthbook.io
. Link this backend to your Compute service. - In Fastly, create one or more backends pointing to your site origins. This includes both your main site origin URL as well as any origin URLs that you may redirect to in any URL Redirect experiments. Link these backends to your Compute service.
In your request handler, you must pass these backends via the config parameter, as shown in the example code in the previous doc section.
- The API Host backend should be set via
config.apiHostBackend = "api_host"
. - The site origin backends are defined as an object mapping each origin URL to its corresponding backend name. They should be set via
config.backends = { "https://internal.mysite.io": "my_site" }
.
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
We suggest using a Fastly Config store to set your environment variables. Create a Config store called env_vars
from the Fastly dashboard and link it to your Compute service. Then, at minimum, add these required key/value pairs:
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:
/// <reference types="@fastly/js-compute" />
import { ConfigStore } from "fastly:config-store";
import { gbHandleRequest, getConfigEnvFromStore } from "@growthbook/edge-fastly";
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const envVarsStore = new ConfigStore("env_vars");
const env = getConfigEnvFromStore(envVarsStore);
const cookie = parse(event.request.headers.get("Cookie") || "");
const config = {
// custom targeting attributes:
attributes: {
userType: cookie["userId"] ? "logged in" : "anonymous"
},
// backends:
apiHostBackend: "api_host", // Name of Fastly backend pointing to your GrowthBook API Endpoint
backends: { "https://internal.mysite.io": "my_site" }, // Map of proxy origins to named Fastly backends
};
return await gbHandleRequest(event.request, env, config);
}
More customization options
For a full list of customizations, view our vendor-agnostic Edge Utility repository .
Set up a Payload Cache
You can configure GrowthBook payload caching by using a Fastly KV store. This eliminates network requests from your edge to GrowthBook which speeds up page delivery while reducing network costs.
You may configure the Fastly Edge App to use either webhook-based or just-in-time payload caching (or both) depending on how you've set up your KV namespaces and SDK Webhooks.
More information about setting up your payload cache can be found in the Payload Caching with Fastly KV Store doc section below.
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):
/// <reference types="@fastly/js-compute" />
import { ConfigStore } from "fastly:config-store";
import { gbHandleRequest, getConfigEnvFromStore } from "@growthbook/edge-fastly";
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const envVarsStore = new ConfigStore("env_vars");
const env = getConfigEnvFromStore(envVarsStore);
const config = {
edgeTrackingCallback: (experiment, results) => {
// todo: replace with your tracking library
console.log('edge tracking callback', {experiment, results});
},
// backends:
apiHostBackend: "api_host", // Name of Fastly backend pointing to your GrowthBook API Endpoint
backends: { "https://internal.mysite.io": "my_site" }, // Map of proxy origins to named Fastly backends
};
return await gbHandleRequest(event.request, env, config);
}
Targeting Attributes
The following targeting attributes are set automatically by the Edge App.
id
- creates a long-livedgbuuid
cookie if it doesn't exist alreadyurl
path
host
query
pageTitle
deviceType
- eithermobile
ordesktop
browser
- one ofchrome
,edge
,firefox
,safari
, orunknown
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).
There may be situations when you will need to provide fine-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.
Cookie Policy and GDPR
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.
Delay Storing the Cookie Until Consent is Granted
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"));
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 thegbuuid
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). You will likely need to monkey-patch our SDK's built-in fetch calls in order to specify a Fastly backend.
In our @growthbook/edge-fastly
NPM package, we export a few Fastly-specific utility functions to simplify SDK payload caching (we discuss payload caching strategies in the subsequent doc section).
/// <reference types="@fastly/js-compute" />
import { KVStore } from "fastly:kv-store";
import { GrowthBook, setPolyfills, helpers } from "@growthbook/growthbook";
import { getPayloadFromKV, getKVLocalStoragePolyfill } from "@growthbook/edge-fastly";
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
// 1. Monkey-patch the GrowthBook SDK to support Fastly backends
helpers.fetchFeaturesCall = ({ host, clientKey, headers }) => fetch(
`${host}/api/features/${clientKey}`,
{ headers, backend: config.apiHostBackend }
);
// 2. 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 payloadKVStore = new KVStore("gb_payload");
const payload = await getPayloadFromKV(payloadKVStore);
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 cacheKVStore = new KVStore("gb_cache");
const localStoragePolyfill = getKVLocalStoragePolyfill(cacheKVStore);
setPolyfills({ localStorage: localStoragePolyfill });
await growthbook.init();
// 3. Start feature flagging
if (growthbook.isOn("my-feature")) {
return new Response("<h1>foo</h1>");
}
return new Response("<h1>bar</h1>");
}
Payload Caching with Fastly KV Store
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.
Convenient solutions this problem are realized through Fastly KV , an on-edge key-val store which we can leverage for persistent payload caching. There are 2 levels of KV integration available:
- You can either completely eliminate the blocking call to the GrowthBook API by implementing a GrowthBook-to-Fastly-KV push model via SDK Webhooks.
- Alternatively, you can eliminate most of these network requests by using Fastly KV as a just-in-time payload cache.
You can also use either of these strategies in your own manual SDK integration via the getPayloadFromKV
and getKVLocalStoragePolyfill
utility functions.
Configuring the KV store
Create a Fastly KV store for your worker to interface with.
Using the Fastly dashboard, create a Fastly KV store for either push-based or just-in-time payload cache (or use both if you like). By convention, we suggest naming a push-based KV store as gb_payload
and naming a just-in-time KV store as gb_cache
. Link your KV store(s) to your Compute service.
If you are using our turnkey Edge App, you simply need to instantiate your KVStore(s) and pass them into your request handler via the config parameter. The Edge App will automatically use these KV stores as persistent cache if present.
/// <reference types="@fastly/js-compute" />
import { ConfigStore } from "fastly:config-store";
import { KVStore } from "fastly:kv-store";
import { gbHandleRequest, getConfigEnvFromStore } from "@growthbook/edge-fastly";
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
const envVarsStore = new ConfigStore("env_vars");
const env = getConfigEnvFromStore(envVarsStore);
const config = {
apiHostBackend: "api_host",
backends: { "https://internal.mysite.io": "my_site" },
gbCacheStore: new KVStore("gb_cache"), // just-in-time payload cache
gbPayloadStore: new KVStore("gb_payload"), // push-based payload cache
};
return await gbHandleRequest(event.request, env, config);
}
Configuring a SDK Webhook
For KV stored payloads (1), we eliminate network requests from edge to GrowthBook by using a GrowthBook SDK Webhook to push the SDK payload to the KV store on change.
- Create an SDK Webhook on the same SDK Connection that you are using for edge integration. You do not need to worry about the receiving end of the webhook (verifying GrowthBook signatures, etc).
- Select Fastly KV as the Webhook Type and fill out the following fields:
- Store ID
- Key
- API Token
Now whenever feature and experiment values change, your Fastly 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.
Under the hood, the webhook is being configured with the following properties. If you need to change any of these settings for any reason, you can always edit the webhook.
- Endpoint URL is being set to
https://api.fastly.com/resources/stores/kv/{store_id}/keys/{key}
- Method is being set to
PUT
- A Fastly-Key Header is being added with your API Token
- The Payload format is being set to
SDK Payload only