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:

  1. Download the bucketing file from the the Flagship CDN
  2. Start the SDK at the beginning of your function and Play with it
  3. Close the SDK before to return a response
  4. 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.

882

Webhooks Integrations

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