Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# BrowserStack credentails
BROWSER_STACK_USERNAME=
BROWSER_STACK_ACCESS=
# Convert Staging SDK Key
CONVERT_STAGING_SDK_KEY=
CONVERT_STAGING_SDK_KEY2=
CONVERT_STAGING_SDK_KEY2_SECRET=

# Logger
LOG_LEVEL=2
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/publish-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ jobs:
(cd packages/experience && npm publish --access public)
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-cloudflare-v') }}
run: |
yarn cloudflare:build
(cd packages/cloudflare && npm publish --access public)
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-v') }}
run: |
yarn sdk:build
Expand Down
36 changes: 22 additions & 14 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,37 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18]
node: [22]
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
with:
# The Node.js version to configure
node-version: ${{ matrix.node }}
fetch-depth: 2

# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v4
with:
fetch-depth: 2
- name: Install needed libraries and packages
node-version: ${{ matrix.node }}

- name: Setup Yarn
run: |
corepack enable
corepack prepare yarn@stable --activate

- name: Install Playwright browsers
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
sudo apt-get update
sudo apt-get install -y google-chrome-stable
cd packages/js-sdk
npx playwright install --with-deps chromium

- name: Runs the SDK QA checks
env:
CONVERT_STAGING_SDK_KEY: ${{ secrets.CONVERT_STAGING_SDK_KEY }}
CONVERT_STAGING_SDK_KEY2: ${{ secrets.CONVERT_STAGING_SDK_KEY2 }}
CONVERT_STAGING_SDK_KEY2_SECRET: ${{ secrets.CONVERT_STAGING_SDK_KEY2_SECRET }}
run: |
yarn set version berry
yarn
cd packages/js-sdk
yarn lint
yarn test
yarn build
yarn test:mocha
yarn test:browser
1 change: 1 addition & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
branches: [main]

permissions:
contents: write

Check warning on line 8 in .github/workflows/release-please.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this write permission from workflow level to job level.

See more on https://sonarcloud.io/project/issues?id=convertcom_javascript-sdk&issues=AZ1kI4CtqzjtoCksAzE4&open=AZ1kI4CtqzjtoCksAzE4&pullRequest=372
pull-requests: write

Check warning on line 9 in .github/workflows/release-please.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this write permission from workflow level to job level.

See more on https://sonarcloud.io/project/issues?id=convertcom_javascript-sdk&issues=AZ1kI4CtqzjtoCksAzE5&open=AZ1kI4CtqzjtoCksAzE5&pullRequest=372

jobs:
release:
Expand Down Expand Up @@ -56,5 +56,6 @@
packages/data) yarn data:build && (cd packages/data && npm publish --access public) ;;
packages/experience) yarn experience:build && (cd packages/experience && npm publish --access public) ;;
packages/js-sdk) yarn sdk:build && (cd packages/js-sdk && npm publish --access public) ;;
packages/cloudflare) yarn cloudflare:build && (cd packages/cloudflare && npm publish --access public) ;;
*) echo "Unknown path: $REL_PATH" && exit 1 ;;
esac
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ lib/
dist/
docs/
coverage/
test-results/
packages/demo-*
.next
demo/remixjs-server-side/build
Expand Down
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"packages/segments": "2.1.2",
"packages/api": "2.1.4",
"packages/data": "3.3.3",
"packages/experience": "2.3.2"
"packages/experience": "2.3.2",
"packages/cloudflare": "1.0.0"
}
252 changes: 252 additions & 0 deletions demo/cloudflare-workers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# Convert SDK — Cloudflare Workers Demo

Run Convert A/B tests at the Cloudflare edge with zero client-side flicker. This demo works out of the box with the Convert staging project (`10035569/10034190`) — the same credentials used by all other SDK demos.

## Routes

| Route | Location | Experiments | Features |
|-------|----------|-------------|----------|
| `/` | — | None (home page) | — |
| `/events` | `events` | `test-experience-ab-fullstack-1` | — |
| `/statistics` | `statistics` | All matching | `feature-4` |
| `/pricing` | `pricing` | All matching | `feature-5` |

## How It Works

```
Visitor → Cloudflare Worker (localhost:8787)
├── Fetch config from Convert CDN (edge-cached, ~1-5ms)
├── Map URL path to location property (e.g. /events → "events")
├── SDK: createContext → runExperience/runExperiences
├── Fetch origin page (localhost:8888)
├── HTMLRewriter: inject experiment results into HTML
├── Set visitor cookie + cache headers
└── Respond (total overhead: ~5-8ms)

└── Background (waitUntil):
└── Flush tracking events to Convert
```

No KV namespace is required. Config is cached using Cloudflare's native `cf.cacheTtl` fetch option (free, all plans). Visitor bucketing is deterministic — the same visitor ID always gets the same variation via a cookie.

## Setup

### 1. Install Dependencies

From the monorepo root:

```bash
yarn install
```

If the cloudflare package hasn't been built yet:

```bash
yarn cloudflare:build
```

You don't need to publish `@convertcom/js-sdk-cloudflare` to npm — Yarn workspaces symlinks it to the local `packages/cloudflare/` directory, and Wrangler's bundler follows symlinks.

### 2. Start the Origin Server

The demo includes a simple origin server (`origin/`) that serves HTML pages for each route. The Worker proxies to this origin and modifies the response with experiment results.

```bash
# Terminal 1
cd demo/cloudflare-workers
yarn origin
```

This starts on http://localhost:8888 with pages for `/`, `/events`, `/statistics`, and `/pricing`.

### 3. Start the Worker

```bash
# Terminal 2
cd demo/cloudflare-workers
yarn dev
```

This starts on http://localhost:8787.

### 4. Open in Browser

Visit http://localhost:8787/ and navigate to the different routes:

- **Events** — Shows the bucketed variation for `test-experience-ab-fullstack-1`
- **Statistics** — Shows all matching variations + `feature-4` status
- **Pricing** — Shows all matching variations (both experiences) + `feature-5` status

## Verification

### Config Fetch

Confirm the staging project config is accessible:

```bash
curl -s "https://cdn-4.convertexperiments.com/api/v1/config/10035569/10034190" | head -c 200
```

Should return JSON starting with `{"account_id":"10035569"...`.

### Visitor Cookie

```bash
curl -v http://localhost:8787/events 2>&1 | grep -i set-cookie
```

Expected: `Set-Cookie: convert_visitor_id=<uuid>; Path=/; ...`

### Deterministic Bucketing

```bash
# Get a visitor ID
VISITOR_ID=$(curl -s -D- http://localhost:8787/events 2>&1 | grep -io 'convert_visitor_id=[^;]*' | cut -d= -f2)
echo "Visitor: $VISITOR_ID"

# Same cookie → same variation every time
curl -s -b "convert_visitor_id=$VISITOR_ID" http://localhost:8787/events
curl -s -b "convert_visitor_id=$VISITOR_ID" http://localhost:8787/events
```

Both responses should show identical experiment results.

### HTMLRewriter

Compare the origin page vs the Worker-modified page:

```bash
# Origin (unmodified)
curl -s http://localhost:8888/events | grep experiment-results

# Worker (modified with bucketing results)
curl -s http://localhost:8787/events | grep experiment-results
```

The Worker response should contain variation names/keys instead of the placeholder text.

### Cache Headers

```bash
curl -s -D- http://localhost:8787/events 2>&1 | grep -iE 'cache-control|vary'
```

Expected:

```
Cache-Control: public, max-age=300
Vary: Cookie
```

### Tracking Events

Watch the Worker logs while making requests:

```bash
# Terminal 3
cd demo/cloudflare-workers
yarn tail
```

After visiting a page, you should see the SDK POST tracking data to Convert. In the Convert dashboard, the visitor count for the experience should increment.

### Error Fallback

Temporarily set an invalid SDK key in `wrangler.toml`:

```toml
CONVERT_SDK_KEY = "invalid/key"
```

Restart `yarn dev` and visit http://localhost:8787/events — should return the unmodified origin page (graceful degradation via try/catch).

### Bundle Validation

```bash
cd demo/cloudflare-workers
npx wrangler deploy --dry-run --outdir /tmp/cf-bundle
```

Should succeed with bundle size ~296 KiB / ~58 KiB gzipped.

## Checklist

| Test | What to Verify |
|------|---------------|
| Config fetch | `curl` to CDN config endpoint returns JSON |
| Wrangler bundle | `--dry-run` succeeds |
| Worker starts | `yarn dev` runs without errors |
| Origin server | `yarn origin` serves pages on port 8888 |
| New visitor cookie | `Set-Cookie: convert_visitor_id=<uuid>` in response |
| Deterministic bucketing | Same cookie → same variation on repeated requests |
| HTMLRewriter | Experiment results injected into HTML |
| Cache headers | `Cache-Control: public, max-age=300` + `Vary: Cookie` |
| Tracking events | Visitor count increments in Convert dashboard |
| Error fallback | Invalid SDK key → unmodified origin page returned |
| No KV required | All above works without any KV namespace configured |

## Configuration

The demo is pre-configured in `wrangler.toml`:

```toml
CONVERT_SDK_KEY = "10035569/10034190" # Staging project
ORIGIN_URL = "http://localhost:8888" # Local origin server
```

To use your own project, update these values and adjust the experience/feature keys in `src/index.ts`.

## Iterating on Package Changes

If you modify `packages/cloudflare/src/` files:

```bash
# Rebuild the package
yarn cloudflare:build

# Restart the Worker (Ctrl+C, then yarn dev again)
```

## Production Deployment

**Important:** The default `ORIGIN_URL` is `http://localhost:8888` which only works for local development. Deploying with this value will fail because `localhost` is not reachable from Cloudflare's network (you'll see a Cloudflare "Error 1003: Direct IP access not allowed" page).

For production, update `ORIGIN_URL` in `wrangler.toml` to a publicly accessible origin:

```toml
ORIGIN_URL = "https://your-site.com"
```

Then deploy:

```bash
yarn deploy
```

## Optional: KV-Backed Persistence

If you need to preserve bucketing across experience config changes or store custom visitor attributes, you can optionally add KV support. See the commented section at the bottom of `src/index.ts` for setup instructions.

## Troubleshooting

**"Cannot find module @convertcom/js-sdk-cloudflare"**
→ Run `yarn install` at the monorepo root to ensure workspace symlinks exist, then `yarn cloudflare:build`.

**"Convert config fetch failed: 404"**
→ Verify your SDK key. Test directly: `curl https://cdn-4.convertexperiments.com/api/v1/config/ACCOUNT_ID/PROJECT_ID`

**Experience returns null / string (RuleError)**
→ The experience key doesn't match, or location/audience rules don't match. Check that the experience is **Active** (not Draft/Paused) in Convert.

**HTMLRewriter changes not visible**
→ Add `console.log('variation:', variation)` in the Worker and check Wrangler logs to confirm bucketing.

**Tracking events not appearing in dashboard**
→ Confirm `ctx.waitUntil(context.releaseQueues(...))` is called. Check Wrangler logs for outbound POST requests.

**Page hangs / infinite loop**
→ All `fetch(request)` calls must go through `fetchOrigin()` which rewrites the URL to `ORIGIN_URL`. Direct `fetch(request)` loops back to the Worker.

## Documentation

Full guide: [Cloudflare Workers Edge Experimentation](https://github.com/convertcom/javascript-sdk/wiki/CloudflareWorkers)
Loading
Loading