Running on the edge with Flagship
How to use Flagship on the edge
Javascript SDK - Engine compatibility
Our Javascript SDK is compatible with :
- V8 Engine
- Node.js runtime
- Deno
Flagship JS SDK can be used in most edge environments provided by cloud platforms and CDNs
This example will guide you to implement Flagship JS SDK in CloudFlare Edge worker, but the rule is the same for any other Edge service using Javascript runtime.
Follow this link to setup a cloudFlare worker project
Once your project is set up, you need to install and initialize the Flagship JS SDK
yarn install @flagship.io/js-sdk
To properly use Flagship in an Edge service, you must follow these steps:
- Download the bucketing file from the the Flagship CDN
- Start the SDK at the beginning of your function and Play with it
- Close the SDK before to return a response
- Managing visitor cache
1. Download the bucketing file
The bucketing file is a JSON file containing all the data used by SDK to make a decision.
You need to download and import it in your script to initialize the SDK with.
To know how and when to download the bucketing file, use this following steps here
2. Start the SDK
You must start the Flagship SDK at the beginning of your function with decisionMode
property to BUCKETING_EDGE
. initialBucketing
property must be set with the data from the bucketing file, the advantage is that no network calls will be made when you call FetchFlags because all the flags are already there.
Here we use the cookie to store the visitorID, if there is no cookie, the SDK will generate one for the visitor.
We recommend using Flagship JS SDK
lightweight bundle @flagship.io/js-sdk/dist/index.lite
, it has small size and is optimized for Edge service.
3. Close the SDK
Flagship.close()
must be called before the end of the script, this function will batch and send all hits generated when running the script. We recommend calling it in the background if possible
//src/index.ts
import {
DecisionMode,
Flagship,
HitType,
} from "@flagship.io/js-sdk/dist/index.browser.lite";
import cookie from "cookie";
//import bucketing file
import bucketingData from "./bucketing.json";
export interface Env {
VISITOR_CACHE_KV: KVNamespace;
API_KEY: string;
ENV_ID: string;
}
const html = (flagValue: unknown, visitorId: string) => `<!DOCTYPE html>
<body>
<h1>Hello World</h1>
<p>This is my Cloudflare Worker using Flagship for the visitorID : <span style="color: red;">${visitorId}</span> assigned on flag <span style="color: red;">${flagValue}</span>.</p>
</body>`;
const FS_VISITOR_ID_COOKIE_NAME = "fs_visitor_id";
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
// Start the SDK
Flagship.start(env.ENV_ID, env.API_KEY, {
decisionMode: DecisionMode.BUCKETING_EDGE, // Set decisionMode to BUCKETING_EDGE
initialBucketing: bucketingData, // Set bucketing data fetched from flagship CDN
});
const cookies = cookie.parse(request.headers.get("Cookie") || "");
//Get visitor Id from cookies
const visitorId = cookies[FS_VISITOR_ID_COOKIE_NAME];
const visitor = Flagship.newVisitor({
visitorId, // if no visitor id exists from the cookie, the SDK will generate one
hasConsented: true,
});
await visitor.fetchFlags();
const flag = visitor.getFlag("my_flag_key");
const flagValue = flag.getValue("default-value");
// Send a hit
await visitor.sendHit({
type: HitType.PAGE,
documentLocation: "page",
});
// Call Flagship.close() in the background to batch and send all hit
ctx.waitUntil(Flagship.close());
return new Response(html(flagValue, visitor.visitorId), {
headers: {
"content-type": "text/html;charset=UTF-8",
"Set-Cookie": cookie.serialize(
FS_VISITOR_ID_COOKIE_NAME,
visitor.visitorId
),
},
});
},
};
import { DecisionMode, Flagship, HitType } from "@flagship.io/js-sdk/dist/index.browser.lite";
import cookie from "cookie";
// import bucketing file
import bucketingData from "./bucketing.json";
const html = (
flagValue,
visitorId,
region
) => `<!DOCTYPE html>
<body>
<h1>Hello World from ${region}</h1>
<p>This is my Cloudflare Worker using Flagship for the visitorID : <span style="color: red;">${visitorId}</span> assigned on flag <span style="color: red;">${flagValue}</span>.</p>
</body>`;
const FS_VISITOR_ID_COOKIE_NAME = "fs_visitor_id";
export default {
async fetch(request, env, ctx) {
// Start the SDK
Flagship.start(env.ENV_ID, env.API_KEY, {
decisionMode: DecisionMode.BUCKETING_EDGE, // Set decisionMode to BUCKETING_EDGE
initialBucketing: bucketingData, // Set bucketing data fetched from flagship CDN
});
const cookies = cookie.parse(request.headers.get("Cookie") || "");
//Get visitor Id from cookies
const visitorId = cookies[FS_VISITOR_ID_COOKIE_NAME];
const visitor = Flagship.newVisitor({
visitorId, // if no visitor id exists from the cookie, the SDK will generate one
hasConsented: true
});
await visitor.fetchFlags();
const flag = visitor.getFlag("my_flag_key");
const flagValue = flag.getValue("default-value");
await visitor.sendHit({
type: HitType.PAGE,
documentLocation: "page",
});
// close the SDK to batch and send all hits
ctx.waitUntil(Flagship.close());
return new Response(
html(flagValue, visitor.visitorId, request?.cf?.region),
{
headers: {
"content-type": "text/html;charset=UTF-8",
"Set-Cookie": cookie.serialize(
FS_VISITOR_ID_COOKIE_NAME,
visitor.visitorId
),
},
}
);
},
};
4. Managing visitor cache
On BUCKETING_EDGE
mode, assignation is made on local so changing campaigns allocation in the platform would make the visitors to see different campaigns. Visitor cache will help you to always keep visitor in variation where he was allocated first, in the case of traffic allocation modifications (made by you or by the dynamic allocation). See more
Here we use Workers KV to store visitor cache
const visitorCacheImplementation: IVisitorCacheImplementation = {
cacheVisitor: async (
visitorId: string,
data: VisitorCacheDTO
): Promise<void> => {
await env.VISITOR_CACHE_KV.put(visitorId, JSON.stringify(data));
},
lookupVisitor: async (visitorId: string): Promise<VisitorCacheDTO> => {
const caches = await env.VISITOR_CACHE_KV.get(visitorId);
return caches ? JSON.parse(caches) : caches;
},
flushVisitor: async (visitorId: string): Promise<void> => {
await env.VISITOR_CACHE_KV.delete(visitorId);
},
};
const visitorCacheImplementation = {
cacheVisitor: async (visitorId, data) => {
await env.VISITOR_CACHE_KV.put(visitorId, JSON.stringify(data));
},
lookupVisitor: async (visitorId) => {
const caches = await env.VISITOR_CACHE_KV.get(visitorId);
return caches ? JSON.parse(caches) : caches;
},
flushVisitor: async (visitorId) => {
await env.VISITOR_CACHE_KV.delete(visitorId);
},
}
Full code
//src/index.ts
import {
DecisionMode,
Flagship,
HitType,
IVisitorCacheImplementation,
VisitorCacheDTO,
} from "@flagship.io/js-sdk/dist/index.browser.lite";
import cookie from "cookie";
import bucketingData from "./bucketing.json";
export interface Env {
VISITOR_CACHE_KV: KVNamespace;
API_KEY: string;
ENV_ID: string;
}
const html = (flagValue: unknown, visitorId: string) => `<!DOCTYPE html>
<body>
<h1>Hello World</h1>
<p>This is my Cloudflare Worker using Flagship for the visitorID : <span style="color: red;">${visitorId}</span> assigned on flag <span style="color: red;">${flagValue}</span>.</p>
</body>`;
const FS_VISITOR_ID_COOKIE_NAME = "fs_visitor_id";
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const visitorCacheImplementation: IVisitorCacheImplementation = {
cacheVisitor: async (
visitorId: string,
data: VisitorCacheDTO
): Promise<void> => {
await env.VISITOR_CACHE_KV.put(visitorId, JSON.stringify(data));
},
lookupVisitor: async (visitorId: string): Promise<VisitorCacheDTO> => {
const caches = await env.VISITOR_CACHE_KV.get(visitorId);
return caches ? JSON.parse(caches) : caches;
},
flushVisitor: async (visitorId: string): Promise<void> => {
await env.VISITOR_CACHE_KV.delete(visitorId);
},
};
// Start the SDK
Flagship.start(env.ENV_ID, env.API_KEY, {
decisionMode: DecisionMode.BUCKETING_EDGE,
visitorCacheImplementation,
initialBucketing: bucketingData, // Set bucketing data fetched from flagship CDN
});
const cookies = cookie.parse(request.headers.get("Cookie") || "");
//Get visitor Id from cookies
const visitorId = cookies[FS_VISITOR_ID_COOKIE_NAME];
const visitor = Flagship.newVisitor({
visitorId, // if no visitor id exists from the cookie, the SDK will generate one
hasConsented: true
});
await visitor.fetchFlags();
const flag = visitor.getFlag("my_flag_key");
const flagValue = flag.getValue("default-value");
await visitor.sendHit({
type: HitType.PAGE,
documentLocation: "page",
});
// close the SDK to batch and send all hits
ctx.waitUntil(Flagship.close());
return new Response(html(flagValue, visitor.visitorId), {
headers: {
"content-type": "text/html;charset=UTF-8",
"Set-Cookie": cookie.serialize(
FS_VISITOR_ID_COOKIE_NAME,
visitor.visitorId
),
},
});
},
};
//src/index.js
import { DecisionMode, Flagship, HitType } from "@flagship.io/js-sdk/dist/index.browser.lite";
import cookie from "cookie";
// import bucketing file
import bucketingData from "./bucketing.json";
const html = (
flagValue,
visitorId,
region
) => `<!DOCTYPE html>
<body>
<h1>Hello World from ${region}</h1>
<p>This is my Cloudflare Worker using Flagship for the visitorID : <span style="color: red;">${visitorId}</span> assigned on flag <span style="color: red;">${flagValue}</span>.</p>
</body>`;
const FS_VISITOR_ID_COOKIE_NAME = "fs_visitor_id";
export default {
async fetch(request, env, ctx) {
const visitorCacheImplementation = {
cacheVisitor: async (visitorId, data) => {
await env.VISITOR_CACHE_KV.put(visitorId, JSON.stringify(data));
},
lookupVisitor: async (visitorId) => {
const caches = await env.VISITOR_CACHE_KV.get(visitorId);
return caches ? JSON.parse(caches) : caches;
},
flushVisitor: async (visitorId) => {
await env.VISITOR_CACHE_KV.delete(visitorId);
},
}
// Start the SDK
Flagship.start(env.ENV_ID, env.API_KEY, {
decisionMode: DecisionMode.BUCKETING_EDGE, // Set decisionMode to BUCKETING_EDGE
visitorCacheImplementation, // set visitorCacheImplementation
initialBucketing: bucketingData, // Set bucketing data fetched from flagship CDN
});
const cookies = cookie.parse(request.headers.get("Cookie") || "");
//Get visitor Id from cookies
const visitorId = cookies[FS_VISITOR_ID_COOKIE_NAME];
const visitor = Flagship.newVisitor({
visitorId, // if no visitor id exists from the cookie, the SDK will generate one
hasConsented: true
});
await visitor.fetchFlags();
const flag = visitor.getFlag("my_flag_key");
const flagValue = flag.getValue("default-value");
await visitor.sendHit({
type: HitType.PAGE,
documentLocation: "page",
});
// close the SDK to batch and send all hits
ctx.waitUntil(Flagship.close());
return new Response(
html(flagValue, visitor.visitorId, request?.cf?.region),
{
headers: {
"content-type": "text/html;charset=UTF-8",
"Set-Cookie": cookie.serialize(
FS_VISITOR_ID_COOKIE_NAME,
visitor.visitorId
),
},
}
);
},
};
How to download bucketing file
Downloading The bucketing file is done by calling the Flagship CDN https://cdn.flagship.io/{FLAGSHIP_ENV_ID}/bucketing.json
replacing FLAGSHIP_ENV_ID
with your Environment ID
from Flagship and place the JSON file where you can import it easily in your script.
You can manually download it in your code and deploy them together or you can use your CI/CD pipeline.
Here is an example of Github action workflow that downloads bucketing file and deploys application.
#.github/workflows/deploy.yml
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "main" ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
environment: cloudflare
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
# Runs a single command using the runners shell
- uses: actions/setup-node@v2
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- run: yarn install
- name: Download bucketing file
# Downloads bucketing file and stores it in src/bucketing.json
run: curl https://cdn.flagship.io/${{secrets.FLAGSHIP_ENV_ID}}/bucketing.json -o src/bucketing.json
- name: Publish
run: env CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }} yarn deploy
When does the bucketing file is required ?
The bucketing file must be downloaded the first time you deploy your app and each time you update your campaigns from Flagship.
Flagship has Webhooks Integrations that can help us to trigger our CI/CD pipeline when a campaing is updated.
When your campaign is updated, a hook called [environment] synchronized
will be triggered. and we will use this event to trigger our CI/CD pipeline which will download the updated bucketing file and deploy our app.
Follow this link to know how to manually trigger a GitHub Actions workflow.
From Flagship plateforme go to settings -> integrations and webhooks tab
Choose [environment] synchronized event and set your Github api
Any feedback?
Want another tutorial?
Contact us
See full code here : github
Updated 5 months ago