Skip to content

feat/cloudflare-workers#363

Open
abbaseya wants to merge 3 commits intomain-convertfrom
feat/cloudflare-workers
Open

feat/cloudflare-workers#363
abbaseya wants to merge 3 commits intomain-convertfrom
feat/cloudflare-workers

Conversation

@abbaseya
Copy link
Contributor

@abbaseya abbaseya commented Feb 24, 2026

Edge Experimentation with Cloudflare Workers

Run Convert A/B tests, feature flags, and personalisation at the Cloudflare edge. Visitors receive a fully modified page with zero flicker and sub-millisecond bucketing latency because all decisions happen server-side inside a Cloudflare Worker, before the response reaches the browser.

sequenceDiagram
    participant V as Visitor
    participant W as Cloudflare Worker
    participant O as Origin Server
    participant C as Convert CDN

    V->>W: GET /page
    W->>C: Fetch config (edge-cached)
    C-->>W: SDK config data
    W->>W: SDK: createContext → runExperience
    W->>O: Fetch origin page
    O-->>W: HTML response
    W->>W: HTMLRewriter: apply variation
    W-->>V: Modified page (zero flicker)
    W--)C: Send tracking event (waitUntil)
Loading

Table of Contents

  1. Why Edge Experimentation?
  2. Architecture Overview
  3. Prerequisites
  4. Setup
  5. Utility Package Reference
  6. Usage Patterns
  7. Caching Strategies
  8. Performance Considerations
  9. Troubleshooting
  10. FAQ

1. Why Edge Experimentation?

Traditional client-side A/B testing has three fundamental problems:

Problem Client-Side Cloudflare Edge
Flicker JavaScript loads, evaluates, then modifies DOM → visible flash HTML is modified before delivery → no flash
Ad-blocker susceptibility Testing scripts blocked by ad-blockers Changes are server-side, invisible to ad-blockers
Performance overhead Extra JS download + parse + execute on every page Zero client-side overhead; Worker adds ~5ms at edge

The Convert FullStack SDK already runs in Cloudflare Workers without modification. Its HTTP client auto-detects the Workers runtime (server-with-fetch) and uses the native fetch API. What this guide provides is the Cloudflare-specific integration layer: edge-cached config, cookie management, HTMLRewriter patterns, and tracking event handling.


2. Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                   Cloudflare Edge PoP                   │
│                                                         │
│  ┌────────────────────────────────────────────────────┐ │
│  │              Cloudflare Worker                     │ │
│  │                                                    │ │
│  │  1. Parse visitor cookie                           │ │
│  │  2. Fetch config (edge-cached via cf.cacheTtl)     │ │
│  │  3. Init SDK with cached config                    │ │
│  │  4. createContext → runExperience (MurmurHash)     │ │
│  │  5. Fetch origin page                              │ │
│  │  6. HTMLRewriter: apply variation                  │ │
│  │  7. Set cookie + respond                           │ │
│  │  8. waitUntil: releaseQueues (flush tracking)      │ │
│  │                                                    │ │
│  └──────────────────┬──────────────┬──────────────────┘ │
│                     │              │                    │
└─────────────────────┼──────────────┼────────────────────┘
                      │              │
       ┌──────────────▼──┐    ┌──────┴────────────────────┐
       │  Origin Server  │    │  Convert CDN              │
       │  (your site)    │    │  (config + tracking;      │
       └─────────────────┘    │   cached at edge)         │
                              └───────────────────────────┘

Key principles:

  • No KV required. Config is cached using Cloudflare's built-in cf.cacheTtl fetch option, which is free and works on all plans.
  • No persistence store required. The SDK uses deterministic MurmurHash bucketing — the same visitor ID always produces the same variation assignment. A cookie identifies the visitor; no server-side state is needed.
  • Tracking events must be flushed explicitly. The SDK's internal event queue uses setTimeout to batch and release tracking events. In Workers, the isolate may finish before that timer fires. Always call context.releaseQueues() inside ctx.waitUntil() to flush events before the Worker completes.
  • The SDK singleton persists across requests within the same Worker isolate. Config is fetched from the CDN only on cold start or when the edge cache entry expires.

3. Prerequisites

  • Cloudflare account with Workers enabled (free tier works for development)
  • Convert account with a FullStack project and at least one active experience
  • Node.js ≥ 18 and Wrangler CLI (npm install -g wrangler)
  • Your SDK key in the format ACCOUNT_ID/PROJECT_ID (found in Convert app settings)

4. Setup

4.1 Create the Worker Project

# Option A: From the demo
cd javascript-sdk/demo/cloudflare-workers
yarn install

# Option B: From scratch
mkdir convert-edge-worker && cd convert-edge-worker
npm init -y
npm install @convertcom/js-sdk @convertcom/js-sdk-cloudflare
npm install -D wrangler @cloudflare/workers-types typescript

4.2 Configure wrangler.toml

name = "convert-edge-experiments"
main = "src/index.ts"
compatibility_date = "2024-12-01"

[vars]
CONVERT_SDK_KEY = "YOUR_ACCOUNT_ID/YOUR_PROJECT_ID"

That's it — no KV namespace needed for the basic setup. The SDK config is cached automatically at the Cloudflare edge using the native fetch cache.

4.3 Development

# Start local development
wrangler dev

# Deploy to production
wrangler deploy

5. Utility Package Reference

Install: npm install @convertcom/js-sdk-cloudflare (or yarn add)

EdgeConfigCache

Caches Convert SDK configuration using Cloudflare's built-in cf.cacheTtl fetch option. No KV namespace required — the response is cached at the edge automatically.

import {EdgeConfigCache} from '@convertcom/js-sdk-cloudflare';

const configCache = new EdgeConfigCache(
  'ACCOUNT/PROJECT', // SDK key
  300                // cache TTL in seconds (default: 300)
);

// Serves from edge cache when available, fetches from CDN on cache miss
const configData = await configCache.getConfig();

// Force refresh (e.g. triggered by a webhook or cron)
await configCache.refreshConfig();

KVDataStore (Optional)

Bridges the SDK's synchronous DataStore interface with Cloudflare's async KV API. This is optional — the SDK uses deterministic MurmurHash bucketing, so the same visitor ID always gets the same variation. You only need KV persistence if you want to:

  • Preserve bucketing across experience config changes
  • Store custom visitor attributes between requests
  • Share state across multiple Workers or routes

Note: Cloudflare KV is a paid add-on and may not be available on all plans.

import {KVDataStore} from '@convertcom/js-sdk-cloudflare';

const dataStore = new KVDataStore(
  env.CONVERT_KV, // KV namespace binding
  'visitor'       // key prefix (default: 'visitor')
);

// Phase 1: Load from KV (async, before SDK)
await dataStore.load(visitorId);

// Phase 2: SDK uses get/set synchronously
const sdk = new ConvertSDK({data: configData, dataStore});
const context = sdk.createContext(visitorId);
// ... run experiments ...

// Phase 3: Save back to KV (async, after SDK)
await dataStore.save(visitorId, 86400); // TTL: 24 hours

Cookie Helpers

Parse and set visitor ID cookies on Workers Request/Response objects.

import {
  getVisitorId,
  setVisitorIdCookie,
  generateVisitorId
} from '@convertcom/js-sdk-cloudflare';

// Read visitor ID from request cookies
const visitorId = getVisitorId(request, 'convert_visitor_id');

// Generate a new one if missing
const newId = generateVisitorId(); // crypto.randomUUID()

// Set the cookie on the response
const headers = new Headers(response.headers);
setVisitorIdCookie(headers, newId, 'convert_visitor_id', 31536000);

Cache Helpers

Build variation-aware cache keys and set appropriate headers.

import {buildCacheKey, setCacheHeaders} from '@convertcom/js-sdk-cloudflare';

// Create a cache key that separates entries by variation
const cacheKey = buildCacheKey(request, variation.key);
// Internally: appends ?_conv_v=variation-1 to the URL

// Set Vary: Cookie and Cache-Control on the response
setCacheHeaders(headers, 300); // 5-minute edge cache

6. Usage Patterns

Pattern 1: Page-Level A/B Test (HTMLRewriter)

This is the core pattern. The Worker fetches the origin page, then uses Cloudflare's HTMLRewriter to modify elements before delivery.

import ConvertSDK from '@convertcom/js-sdk';
import {
  EdgeConfigCache,
  getVisitorId, setVisitorIdCookie, generateVisitorId,
  setCacheHeaders
} from '@convertcom/js-sdk-cloudflare';

let sdk: InstanceType<typeof ConvertSDK> | null = null;
let sdkReadyPromise: Promise<InstanceType<typeof ConvertSDK>> | null = null;

async function getSDK(env: Env) {
  if (sdk) return sdk;
  if (sdkReadyPromise) return sdkReadyPromise;
  sdkReadyPromise = (async () => {
    const config = await new EdgeConfigCache(env.SDK_KEY).getConfig();
    sdk = new ConvertSDK({data: config, network: {tracking: true}});
    await sdk.onReady();
    return sdk;
  })();
  return sdkReadyPromise;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const convert = await getSDK(env);

    // Identify visitor
    let visitorId = getVisitorId(request) || generateVisitorId();

    // Run experiment (deterministic bucketing — no persistence needed)
    const context = convert.createContext(visitorId);
    const variation = context.runExperience('homepage-hero-test', {
      locationProperties: {url: new URL(request.url).pathname}
    });

    // Fetch origin
    const originResponse = await fetch(request);

    // Apply variation via HTMLRewriter
    let response = originResponse;
    if (variation && typeof variation !== 'string' && variation.key === 'bold-headline') {
      response = new HTMLRewriter()
        .on('h1.hero', {
          element(el) { el.setInnerContent('Ship Faster, Test Smarter'); }
        })
        .on('.hero-subtitle', {
          element(el) { el.setInnerContent('Zero-flicker A/B testing at the edge.'); }
        })
        .transform(originResponse);
    }

    // Response headers
    const headers = new Headers(response.headers);
    setVisitorIdCookie(headers, visitorId);
    setCacheHeaders(headers);

    // IMPORTANT: Flush tracking events before the Worker finishes.
    // The SDK queues events internally with a setTimeout-based timer.
    // In Workers that timer may never fire, so you must release manually.
    ctx.waitUntil(context.releaseQueues('edge'));

    return new Response(response.body, {status: response.status, headers});
  }
};

Pattern 2: Asset / Image Swap

Replace images, stylesheets, or scripts per variation:

if (variation.key === 'modern-assets') {
  response = new HTMLRewriter()
    .on('link[rel="stylesheet"][href*="main.css"]', {
      element(el) {
        el.setAttribute('href', '/css/main-v2.css');
      }
    })
    .on('img.product-hero', {
      element(el) {
        const src = el.getAttribute('src') || '';
        el.setAttribute('src', src.replace('/images/', '/images/v2/'));
      }
    })
    .on('script[src*="analytics.js"]', {
      element(el) {
        el.setAttribute('src', '/js/analytics-v2.js');
      }
    })
    .transform(originResponse);
}

Pattern 3: Split URL Redirect

Serve a completely different origin page without the visitor seeing a redirect:

if (variation.key === 'new-checkout') {
  const url = new URL(request.url);
  url.pathname = '/checkout-v2' + url.pathname.replace('/checkout', '');
  // Fetch the alternate page transparently (URL in browser doesn't change)
  return fetch(new Request(url.toString(), request));
}

Pattern 4: SPA Injection

For single-page applications, inject bucketing decisions as a global JS variable so the client-side framework can apply them:

const variations = context.runExperiences({
  locationProperties: {url: new URL(request.url).pathname}
});

const decisions = JSON.stringify(
  variations.filter(v => v && typeof v !== 'string')
);

response = new HTMLRewriter()
  .on('head', {
    element(el) {
      el.append(
        `<script>window.__CONVERT_DECISIONS__=${decisions};</script>`,
        {html: true}
      );
    }
  })
  .transform(originResponse);

Then in your SPA (React, Vue, etc.):

// Read decisions injected at the edge
const decisions = window.__CONVERT_DECISIONS__ || [];
const heroVariation = decisions.find(d => d.experienceKey === 'homepage-hero-test');
if (heroVariation?.key === 'bold-headline') {
  // Apply variation in your component
}

Pattern 5: Feature Flags at the Edge

Use feature flags to gate entire page sections:

const feature = context.runFeature('new-pricing-table', {
  locationProperties: {url: '/pricing'}
});

if (feature && typeof feature !== 'string' && feature.status === 'enabled') {
  response = new HTMLRewriter()
    .on('#pricing-table', {
      element(el) {
        el.replace('<div id="pricing-table"><!-- new pricing HTML --></div>', {html: true});
      }
    })
    .transform(originResponse);
}

7. Caching Strategies

SDK Config Caching

The EdgeConfigCache class uses Cloudflare's native cf.cacheTtl fetch option to cache the SDK config response at the edge. This is:

  • Free — no KV reads/writes, works on all Cloudflare plans
  • Automatic — Cloudflare manages the cache lifecycle
  • Fast — cached responses are served from the same PoP, no network hop to KV
// Under the hood, EdgeConfigCache does:
const response = await fetch(configUrl, {
  cf: { cacheTtl: 300, cacheEverything: true }
});

To force a refresh (e.g. after updating your Convert project config):

await configCache.refreshConfig(); // fetches with cacheTtl: 0

Edge Cache Per Variation

Cache origin responses per variation to avoid redundant origin fetches:

import {buildCacheKey} from '@convertcom/js-sdk-cloudflare';

async function fetchWithEdgeCache(
  request: Request,
  variationKey: string,
  ctx: ExecutionContext
): Promise<Response> {
  const cache = caches.default;
  const cacheKey = buildCacheKey(request, variationKey);

  // Try edge cache first
  let response = await cache.match(cacheKey);
  if (response) return response;

  // Cache miss: fetch from origin
  response = await fetch(request);
  const cloned = new Response(response.body, response);
  cloned.headers.set('Cache-Control', 'public, max-age=300');

  // Store in edge cache (non-blocking)
  ctx.waitUntil(cache.put(cacheKey, cloned.clone()));

  return cloned;
}

Recommended Cache-Control Headers

Resource Type Header Reason
A/B tested HTML pages public, max-age=300 + Vary: Cookie Short cache, respects visitor bucketing
Static assets (CSS/JS/images) public, max-age=31536000, immutable Long cache, fingerprinted filenames
SDK config (edge cache) cf.cacheTtl: 300 Matches SDK's default 5-minute refresh

When to Bypass Cache

// Don't cache personalised or authenticated pages
if (request.headers.get('Authorization') || url.pathname.startsWith('/account/')) {
  return fetch(request);
}

8. Performance Considerations

What This Adds

Operation Latency When
Config fetch (edge-cached) ~1-5 ms Every request (Cloudflare edge cache hit)
Config fetch (CDN, cache miss) ~50-100 ms Cold start or cache expiry
SDK bucketing (MurmurHash) < 0.1 ms Every request (CPU)
HTMLRewriter transform ~1-3 ms Every request (streaming)
Tracking POST ~50-100 ms Background (waitUntil)
Total added to response ~5-8 ms

SDK Behaviour in Workers

Two SDK patterns behave differently in Workers than in long-lived servers:

  1. Config refresh timer (setTimeout in core.ts) — In a long-lived Node.js server, this refreshes config every 5 minutes. In Workers, the isolate may not persist that long. Solution: Pass config data directly via EdgeConfigCache and let the edge cache TTL handle refresh. The setTimeout is harmless (it just won't fire).

  2. Event batching (ApiManager.enqueue()) — The SDK queues tracking events and releases them after a setTimeout delay (default: 1-10 seconds). In Workers, the isolate finishes before that timer fires, so events would be lost. Solution: Always call context.releaseQueues() inside ctx.waitUntil() at the end of each request. This immediately flushes all pending events without blocking the response:

// This is REQUIRED — without it, tracking events will be lost
ctx.waitUntil(context.releaseQueues('edge-request-complete'));

Cloudflare Worker Limits

Limit Free Plan Paid Plan
Script size 1 MB 10 MB
CPU time per request 10 ms 30 ms (50 ms burst)
Subrequests per request 50 50

The Convert SDK at 24 KB gzipped (136 KB uncompressed) fits well within script size limits.


9. Troubleshooting

Visitor Gets Different Variations Across Requests

Cause: Visitor cookie not being set or read correctly.

Check:

# Verify the cookie is set
curl -v https://your-site.com/page 2>&1 | grep -i 'set-cookie.*convert'

# Verify the cookie is sent back
curl -v -b "convert_visitor_id=test-123" https://your-site.com/page

Fix: Ensure setVisitorIdCookie() is called and the response headers are forwarded. Check that the cookie domain matches your site.

SDK Returns Null Context

Cause: Config data is empty or invalid.

Check:

# Verify config fetch
curl https://cdn-4.convertexperiments.com/api/v1/config/YOUR_ACCOUNT_ID/YOUR_PROJECT_ID

Fix: Verify your SDK key. Check that the experience is active (not paused/draft) in the Convert dashboard.

HTMLRewriter Changes Not Appearing

Cause: Selectors don't match the origin HTML, or the response is not HTML.

Check:

  • View the origin HTML source and verify your CSS selectors match
  • Add console.log in the Worker and check with wrangler tail
  • Verify the Content-Type is text/html

Experience Returns a RuleError

Cause: The visitor doesn't match the experience's audience/location rules.

Check: The locationProperties and visitorProperties passed to runExperience() must match the rules configured in Convert.

const variation = context.runExperience('my-test', {
  locationProperties: {url: '/page'},    // must match site area rules
  visitorProperties: {country: 'US'}     // must match audience rules
});
console.log('Result:', variation); // Will show the RuleError string if rules don't match

Tracking Events Not Appearing in Convert Dashboard

Cause: releaseQueues() not called before the Worker finishes.

Fix: Always call context.releaseQueues() inside ctx.waitUntil():

// This flushes the SDK's internal event queue immediately
ctx.waitUntil(context.releaseQueues('edge-request-complete'));

Without this, the SDK's internal setTimeout-based release timer will never fire because the Worker isolate finishes first.

Edge Cache Serving Stale Config

Fix: Call configCache.refreshConfig() programmatically (e.g. via a cron trigger or webhook endpoint). This fetches with cacheTtl: 0 which bypasses the edge cache.


10. FAQ

Can I use this with any website, or only Cloudflare-hosted sites?

Any website that has Cloudflare as its DNS/proxy provider (orange cloud enabled). The origin server can be hosted anywhere (AWS, Vercel, your own server). Cloudflare Workers intercept requests at the edge regardless of where the origin sits.

How does this compare to client-side Convert tracking scripts?

They serve different purposes. The client-side tracking script is a drop-in solution for marketing teams. Edge experimentation via Workers is for developers who want full control, zero flicker, and server-side experimentation on their own infrastructure.

Do I need Cloudflare KV?

No. The basic setup requires no KV at all:

  • Config caching uses Cloudflare's native fetch cache (cf.cacheTtl) — free on all plans
  • Visitor bucketing is deterministic (MurmurHash) — the same visitor ID always gets the same variation, no server-side state needed
  • Visitor identity is stored in a cookie

KV is only needed if you want to persist bucketing decisions across experience config changes, or store custom visitor attributes. See the KVDataStore section.

Does the SDK work in Cloudflare Pages Functions?

Yes. Pages Functions are Cloudflare Workers under the hood. The same patterns apply. Place your function in functions/[[path]].ts to intercept all routes.

Can I run multiple experiments on the same page?

Yes. Use context.runExperiences() to get all variation decisions at once, then chain multiple HTMLRewriter handlers:

const variations = context.runExperiences({
  locationProperties: {url: url.pathname}
});

let rewriter = new HTMLRewriter();
for (const v of variations) {
  if (v && typeof v !== 'string') {
    // Add handlers per experiment
    if (v.experienceKey === 'hero-test' && v.key === 'v1') {
      rewriter = rewriter.on('h1', {element(el) { el.setInnerContent('New Hero'); }});
    }
    if (v.experienceKey === 'cta-test' && v.key === 'v1') {
      rewriter = rewriter.on('.cta', {element(el) { el.setInnerContent('Buy Now'); }});
    }
  }
}
response = rewriter.transform(originResponse);

How do I track conversions?

Call context.trackConversion() in the Worker (for server-side events like form submissions) or from the client-side SDK (for click events):

// Server-side (in Worker)
if (request.method === 'POST' && url.pathname === '/api/purchase') {
  context.trackConversion('purchase-goal', {
    conversionData: [{key: 'amount', value: 49.99}]
  });
  ctx.waitUntil(context.releaseQueues('conversion'));
}

What happens if the Worker throws an error?

The demo wraps everything in a try/catch that falls back to fetch(request) — the unmodified origin page. Visitors always get a working page; experiments degrade gracefully.

Why must I call releaseQueues() manually?

The SDK internally batches tracking events and releases them after a setTimeout delay. This works in long-lived Node.js servers but not in Cloudflare Workers where the isolate finishes before the timer fires. releaseQueues() immediately flushes all pending events. Wrapping it in ctx.waitUntil() ensures the tracking POST completes without blocking the response to the visitor.


Additional Resources

Add @convertcom/js-sdk-cloudflare with edge-specific helpers for running
Convert experiments inside Cloudflare Workers: KV-backed DataStore adapter,
edge config cache, cookie helpers, and variation-aware cache utilities.

Includes a demo Worker (demo/cloudflare-workers/) demonstrating page-level
A/B testing with HTMLRewriter, asset swaps, split URL redirects, and SPA
injection patterns.

Updates release-please config, publish workflows, and root build scripts
to support the new package.
@abbaseya abbaseya requested a review from a team February 24, 2026 21:03
@abbaseya abbaseya self-assigned this Feb 24, 2026
@gemini-code-assist
Copy link

Summary of Changes

Hello @abbaseya, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement by enabling the Convert JavaScript SDK to run experiments directly on the Cloudflare edge. This integration allows for A/B testing, feature flagging, and personalization with superior performance, eliminating visual flicker and reducing latency by executing all decision-making and content modification server-side within Cloudflare Workers. The changes include a new utility package, a detailed demo, and robust mechanisms for configuration caching and visitor data persistence using Cloudflare KV.

Highlights

  • New Cloudflare Workers Utility Package: Introduced @convertcom/js-sdk-cloudflare, a new utility package designed to integrate the Convert JavaScript SDK with Cloudflare Workers, enabling edge experimentation capabilities.
  • Edge Experimentation Demo: Added a comprehensive demo project (demo/cloudflare-workers) showcasing various patterns for running A/B tests, feature flags, and personalization directly at the Cloudflare edge, including HTMLRewriter transformations, asset swaps, split URL redirects, and SPA injection.
  • Zero Flicker A/B Testing: Enabled server-side modification of HTML via Cloudflare's HTMLRewriter, ensuring that visitors receive fully modified pages with zero flicker and sub-millisecond bucketing latency.
  • KV-backed Persistence and Caching: Implemented EdgeConfigCache for caching SDK configuration in Cloudflare KV and KVDataStore for persisting visitor bucketing decisions, significantly reducing latency and reliance on CDN fetches.
  • Cookie and Cache Management: Provided helper functions for managing visitor ID cookies (getVisitorId, setVisitorIdCookie, generateVisitorId) and for building variation-aware cache keys and setting appropriate Cache-Control headers (buildCacheKey, setCacheHeaders) for Cloudflare's edge cache.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .release-please-manifest.json
    • Added packages/cloudflare to the release manifest with an initial version of 1.0.0.
  • demo/cloudflare-workers/README.md
    • Added a new README file detailing the purpose, setup, and working principles of the Cloudflare Workers demo.
  • demo/cloudflare-workers/package.json
    • Added a new package.json file for the Cloudflare Workers demo, specifying project metadata, scripts, and dependencies including @convertcom/js-sdk and @convertcom/js-sdk-cloudflare.
  • demo/cloudflare-workers/src/index.ts
    • Added the main TypeScript file for the Cloudflare Worker demo, implementing SDK initialization, visitor identification, experiment execution, HTML modification via HTMLRewriter, and background persistence/tracking.
  • demo/cloudflare-workers/tsconfig.json
    • Added a new tsconfig.json file for the Cloudflare Workers demo, configuring TypeScript compilation options for the Worker environment.
  • demo/cloudflare-workers/wrangler.toml
    • Added a new wrangler.toml file for the Cloudflare Workers demo, defining the Worker's name, main script, compatibility date, KV namespace bindings, and environment variables.
  • package.json
    • Updated the root package.json to include linting and build scripts for the new cloudflare package.
    • Added a new demo:cloudflare:start script to facilitate local development of the Cloudflare Workers demo.
  • packages/cloudflare/README.md
    • Added a new README file for the @convertcom/js-sdk-cloudflare package, outlining its purpose, installation instructions, included exports, and a quick start example.
  • packages/cloudflare/index.ts
    • Added the main entry point for the @convertcom/js-sdk-cloudflare package, exporting KVDataStore, EdgeConfigCache, cookie helpers, and cache helpers.
  • packages/cloudflare/package.json
    • Added a new package.json file for the @convertcom/js-sdk-cloudflare package, defining its name, version, description, main files, author, license, scripts, and peer dependencies.
  • packages/cloudflare/src/cache-helpers.ts
    • Added cache-helpers.ts containing utility functions buildCacheKey to create variation-aware cache keys and setCacheHeaders to manage Cache-Control and Vary headers for A/B tested responses.
  • packages/cloudflare/src/cookie-helpers.ts
    • Added cookie-helpers.ts containing utility functions getVisitorId, setVisitorIdCookie, and generateVisitorId for managing visitor identification via cookies in Cloudflare Workers.
  • packages/cloudflare/src/edge-config-cache.ts
    • Added edge-config-cache.ts defining the EdgeConfigCache class, which caches Convert SDK configuration in Cloudflare KV to reduce latency and provides methods to get and refresh the configuration.
  • packages/cloudflare/src/kv-data-store.ts
    • Added kv-data-store.ts defining the KVDataStore class, an adapter for Cloudflare Workers KV that enables asynchronous persistence of visitor data for the Convert SDK.
  • release-please-config.json
    • Added configuration for the new packages/cloudflare to the release-please-config.json, enabling automated release management for the package.
Ignored Files
  • Ignored by pattern: .github/workflows/** (2)
    • .github/workflows/publish-package.yml
    • .github/workflows/release-please.yml
Activity
  • No human activity (comments, reviews, approvals) has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new package @convertcom/js-sdk-cloudflare and a demo to enable running Convert experiments on Cloudflare's edge. The implementation is well-structured and thoroughly documented. My review focuses on improving robustness and type safety. I've pointed out potential race conditions in singleton initialization and areas where JSON parsing could fail without error handling. I've also suggested improvements to TypeScript typings for better code clarity and safety. Overall, this is a great addition.

Fix SDK singleton race condition by caching the initialization promise
so concurrent cold-start requests share one init. Guard JSON.parse calls
in KVDataStore and EdgeConfigCache against corrupted KV data. Type the
applyVariation parameter as BucketedVariation instead of any.
Copy link
Contributor

@clllaur clllaur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abbaseya I took a quick look and some comments:

  1. for caching fetch response (our project data) is no need to use KW, that's more like a database, more expensive and It might be that not all plans have it. Instead the basic caching setting for fetch can be used:
    let response = await fetch(url, { cf: { cacheTtl: 500000, cacheEverything: true, } }) https://developers.cloudflare.com/workers/examples/cache-using-fetch/
  2. Persistence store is not really required as usual, maybe make that clear?
  3. you mentioned that tracking events are released on a schedule base, how will that work here? You need to make sure the script finishes fast and does not wait on the queue of events to be released X seconds later. IF thats the case, a setting for this would be needed and than have tracking queue release right before finishing worker's job

…tional persistence

Address code review on Cloudflare Workers integration:

- Replace KV-based EdgeConfigCache with Cloudflare built-in fetch cache
  (cf: { cacheTtl, cacheEverything }) — simpler, free on all plans, no KV
  dependency needed for basic setup
- Mark KVDataStore as optional — deterministic MurmurHash bucketing means
  the same visitor ID always gets the same variation without persistence
- Document that releaseQueues() must be called in waitUntil() before the
  Worker finishes, since the SDK setTimeout-based event timer will not fire
  in stateless Workers
- Update demo, README, and wrangler.toml to reflect KV-free default setup
@abbaseya abbaseya force-pushed the feat/cloudflare-workers branch from 65a4e88 to a24523c Compare February 27, 2026 03:02
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants