Flagship + Fastly Compute@Edge Integration
This example will guide you to integrate Flagship feature flags with Fastly Compute@Edge, enabling feature flagging and A/B testing at the edge.
Github Repository
https://github.com/flagship-io/flagship-fastly-worker-example
Overview
This guide shows how to:
- Use KV storage or direct integration for caching bucketing data to improve performance
- Initialize the Flagship SDK in a Fastly Compute@Edge application
- Create a visitor object with context data from request headers or any other source
- Fetch feature flags assigned to this visitor
- Retrieve specific flag values for use in the application
- Send analytics data back to Flagship
- Ensure analytics are sent before the worker terminates
Prerequisites
- Node.js (v18 or later)
- Yarn (v4 or later)
- A Fastly account with Compute@Edge access
- A Flagship account with API credentials
Setup
- Create a Fastly Compute@Edge project:
Follow this link to setup a Fastly Compute@Edge project
- Install dependencies:
yarn add @flagship.io/js-sdk
- Configure your Flagship credentials as Fastly secrets:
# Create a secret store
fastly compute secret-store create --name=MY_APP_SECRET
# Add your configuration as a secret
fastly compute secret create --store=MY_APP_SECRET --name=FLAGSHIP_CONFIG --file=./FLAGSHIP_CONFIG.json
Where FLAGSHIP_CONFIG.json contains:
{
"envId": "your-env-id",
"apiKey": "your-api-key"
}
- Create a KV store for caching:
fastly compute kv-store create --name=MY_APP_KV
- Update the fastly.toml file with your service configuration
Use KV storage or direct integration for bucketing data
Bucketing data contains information about your Flagship campaigns and variations, allowing the worker to make flag decisions at the edge without calling the Flagship API for every request.
Development Approach
Option 1: KV Storage
- Fetch bucketing data directly from the Flagship CDN:
# Replace YOUR_ENV_ID with your Flagship Environment ID
curl -s https://cdn.flagship.io/YOUR_ENV_ID/bucketing.json > bucketing-data.json
- Configure your local development environment to use this data by adding the following to your fastly.toml file:
[local_server.kv_stores]
[[local_server.kv_stores.MY_APP_KV]]
key = "initialBucketing"
file = "./bucketing-data.json"
Option 2: Direct Integration
For direct integration, you'll need to:
- Fetch the bucketing data during your build process
- Save it as a JSON file in your project
- Import it directly in your edge application code
# During build/deployment:
curl -s https://cdn.flagship.io/YOUR_ENV_ID/bucketing.json > src/bucketing-data.json
Then import in your code:
import bucketingData from './bucketing-data.json';
// Use this data when initializing Flagship
Production Approach
For production environments, there are two recommended approaches. Both require setting up webhooks in the Flagship platform that trigger your CI/CD pipeline when campaigns are updated.
Find more details here.
Initialize the Flagship SDK in a Fastly Compute@Edge application
The first step to using Flagship in your Fastly Compute@Edge application is to initialize the SDK. This sets up the connection with your Flagship project and configures how feature flags will be delivered.
With KV Storage
The KV storage approach involves retrieving the bucketing data from Fastly KV at runtime:
import { SecretStore } from "fastly:secret-store";
import { KVStore } from "fastly:kv-store";
import {
BucketingDTO,
DecisionMode,
Flagship,
LogLevel,
} from "@flagship.io/js-sdk/dist/edge.js";
async function initFlagship(event: FetchEvent) {
// Access Flagship credentials from a single consolidated secret
const secretStore = new SecretStore("MY_APP_SECRET");
const configSecret = await secretStore.get("FLAGSHIP_CONFIG");
const config = JSON.parse(configSecret?.plaintext() || "{}");
const FLAGSHIP_ENV_ID = config.envId;
const FLAGSHIP_API_KEY = config.apiKey;
// Retrieve cached bucketing data from KV storage
const store = new KVStore("MY_APP_KV");
const initialBucketingItem = await store.get("initialBucketing");
const initialBucketing = initialBucketingItem
? await initialBucketingItem.json()
: undefined;
// Initialize Flagship SDK with credentials and configuration
await Flagship.start(FLAGSHIP_ENV_ID, FLAGSHIP_API_KEY, {
// Use decision API mode for optimal performance
decisionMode: DecisionMode.DECISION_API,
// Pass cached bucketing data
initialBucketing: initialBucketing as BucketingDTO,
// Defer fetching campaign data until explicitly needed
fetchNow: false,
logLevel: LogLevel.DEBUG,
});
}
With Direct Integration
The direct integration approach involves importing the bucketing data directly:
import { SecretStore } from "fastly:secret-store";
import {
DecisionMode,
Flagship,
LogLevel,
} from "@flagship.io/js-sdk/dist/edge.js";
// Import bucketing data directly
import initialBucketing from './bucketing-data.json';
async function initFlagship(event: FetchEvent) {
// Access Flagship credentials from a single consolidated secret
const secretStore = new SecretStore("MY_APP_SECRET");
const configSecret = await secretStore.get("FLAGSHIP_CONFIG");
const config = JSON.parse(configSecret?.plaintext() || "{}");
const FLAGSHIP_ENV_ID = config.envId;
const FLAGSHIP_API_KEY = config.apiKey;
// Initialize Flagship SDK with credentials and embedded bucketing data
await Flagship.start(FLAGSHIP_ENV_ID, FLAGSHIP_API_KEY, {
// Use decision API mode for optimal performance
decisionMode: DecisionMode.DECISION_API,
// Use the imported bucketing data
initialBucketing,
// Defer fetching campaign data until explicitly needed
fetchNow: false,
logLevel: LogLevel.DEBUG,
});
}
Configuration Options
-
decisionMode:
BUCKETING_EDGE
is recommended for Workers as it makes decisions locally using bucketing dataAPI
mode would call Flagship servers for each decision (not recommended for Workers)
-
initialBucketing:
- Pre-loaded campaign data to make local decisions without API calls
- Retrieved from KV storage or embedded in your code
-
fetchNow:
false
Defer fetching campaign data until explicitly needed
Create a visitor object with context data from request headers or any other source
The visitor object represents a user of your application. You need to create one for each request, providing a unique ID and relevant context data that can be used for targeting.
// From the worker fetch handler
const { searchParams } = new URL(event.request.url);
// Get visitor ID from query params or let SDK generate one
const visitorId = (searchParams.get('visitorId') as string) || undefined;
// Create a visitor with context data extracted from request headers
// This context can be used for targeting rules in Flagship campaigns
const visitor = Flagship.newVisitor({
visitorId,
// Set GDPR consent status for data collection
hasConsented: true,
context: {
userAgent: event.request.headers.get('user-agent') || 'unknown',
country: event.client.geo?.country_name || 'unknown',
path: event.request.url,
referrer: event.request.headers.get('referer') || 'unknown',
// You can add any additional context data that's relevant for your targeting
// For example:
// isPremiumUser: searchParams.get('premium') === 'true',
// deviceType: detectDeviceType(request.headers.get('user-agent')),
},
});
You can include any information in the context object that might be useful for targeting. Fastly Compute@Edge provides access to geolocation information, request headers, and more. Common examples include:
- Demographics: age, gender, location
- Technical: device, browser, OS, screen size
- Behavioral: account type, subscription status
- Custom: any application-specific attributes
This context is used by Flagship for targeting rules, so include any attributes that might be useful for segmenting your users.
Fetch feature flags assigned to this visitor
Once you have a visitor object, you need to fetch the feature flags assigned to them based on targeting rules:
// Fetch feature flags assigned to this visitor
// This applies all targeting rules based on visitor context
await visitor.fetchFlags();
// ... Continue with the rest of your worker logic
This operation evaluates all campaign rules against the visitor's context and assigns flag variations accordingly.
Retrieve specific flag values for use in the application
After fetching flags, you can retrieve specific flag values for use in your application. The SDK provides a type-safe way to access flag values with default fallbacks.
// Retrieve specific flag values with default fallbacks if flags aren't defined
const welcomeMessage = visitor
.getFlag("welcome_message")
.getValue("Welcome to our site!");
const isFeatureEnabled = visitor
.getFlag("new_feature_enabled")
.getValue(false);
// You can get different types of values:
// Strings
const title = visitor.getFlag('page_title').getValue('Default Title');
// Numbers
const discountPercent = visitor.getFlag('discount_percentage').getValue(0);
// Objects
const uiConfig = visitor.getFlag('ui_config').getValue({
theme: 'light',
showBanner: false,
menuItems: ['home', 'products', 'contact'],
});
// Arrays
const items = visitor.getFlag('menu_items').getValue(['home', 'about']);
Always provide a default value that matches the expected type. This ensures your application works even if the flag isn't defined or there's an issue fetching flags.
Note: calling
getValue
automatically activates the flag, meaning it will be counted in the reporting.
Send analytics data back to Flagship
To measure the impact of your feature flags, you need to send analytics data back to Flagship. This includes page views, conversions, transactions, and custom events.
import { EventCategory, HitType } from "@flagship.io/js-sdk/dist/edge.js";
// Send analytics data back to Flagship for campaign reporting
visitor.sendHits([
{
type: HitType.PAGE_VIEW,
documentLocation: event.request.url,
},
{
type: HitType.EVENT,
category: EventCategory.ACTION_TRACKING,
action: 'feature_view',
label: 'new_feature',
value: isFeatureEnabled ? 1 : 0,
},
]);
Analytics data is crucial for measuring the impact of your feature flags in A/B testing scenarios. You can track page views, events, transactions, and more.
Ensure analytics are sent before the worker terminates
Fastly Compute@Edge applications can terminate quickly, potentially before analytics data is sent. To prevent this, use waitUntil:
// Ensure analytics are sent before the worker terminates
event.waitUntil(Flagship.close());
// Return feature flag values as JSON response
return new Response(
JSON.stringify({
message: welcomeMessage,
features: {
newFeatureEnabled: isFeatureEnabled,
},
}),
{
status: 200,
headers: new Headers({ "Content-Type": "application/json" }),
}
);
This ensures that all pending analytics are sent before the worker terminates, giving you accurate reporting data.
Production Approach to retrieve and update bucketing data
For production environments, there are two recommended approaches. Both require setting up webhooks in the Flagship platform that trigger your CI/CD pipeline when campaigns are updated:
Common Setup for Both Approaches
- Set up a webhook in the Flagship Platform that triggers whenever a campaign is updated
- Configure the webhook to call your CI/CD pipeline or serverless function
The primary difference between the approaches is where the bucketing data is stored:
Option 1: Webhook + KV Storage
This approach stores bucketing data in Fastly KV:
name: Update Flagship Bucketing Data
on:
repository_dispatch:
types: [flagship-campaign-updated]
jobs:
update-bucketing:
runs-on: ubuntu-latest
steps:
- name: Install Fastly CLI
run: |
curl -fsSL https://developer.fastly.com/install.sh | bash
- name: Fetch latest bucketing data
run: |
curl -s https://cdn.flagship.io/${{ secrets.FLAGSHIP_ENV_ID }}/bucketing.json > bucketing.json
- name: Update Fastly KV
run: |
fastly compute kv-store entry put --store-id=${{ secrets.FASTLY_KV_STORE_ID }} \
--key=initialBucketing [email protected] \
--service-id=${{ secrets.FASTLY_SERVICE_ID }} \
--token=${{ secrets.FASTLY_API_TOKEN }}
Option 2: Direct Integration via Deployment
This approach embeds bucketing data directly in your worker code:
name: Deploy Worker with Latest Bucketing Data
on:
repository_dispatch:
types: [flagship-campaign-updated]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Fetch latest bucketing data
run: |
curl -s https://cdn.flagship.io/${{ secrets.FLAGSHIP_ENV_ID }}/bucketing.json > src/bucketing-data.json
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: yarn install
- name: Deploy to Fastly
run: yarn deploy
env:
FASTLY_API_TOKEN: ${{ secrets.FASTLY_API_TOKEN }}
Trade-offs between approaches:
KV Storage Approach:
- Performance: Adds KV read latency to each request
- Flexibility: Allows updating flags without redeploying code
- Reliability: If KV is unavailable, flags might not work correctly
- Cost: Incurs KV read costs for each worker invocation
- Debugging: Easier to inspect current bucketing data separately from code
- Isolation: Clearer separation between code and configuration
Direct Integration Approach:
- Performance: Faster initialization with no external calls during startup
- Deployment: Requires redeployment for each flag configuration change
- Reliability: Fewer runtime dependencies, more predictable behavior
- Bundle size: Larger worker bundle due to embedded bucketing data
- Caching: Better cold start performance since data is bundled
Choose the approach that best fits your deployment frequency and performance requirements.
Learn More
Updated about 8 hours ago