Install and configure the Spinlink Cloudflare Worker to capture HTTP request metadata and track AI-referred traffic to your property.
The Cloudflare Analytics Worker runs as transparent middleware on your Cloudflare zone. It intercepts every HTTP request, collects metadata, and forwards it asynchronously to Spinlink for analytics — without affecting your site's performance or functionality.
The Worker captures the following request metadata per request:
| Field | Description |
|---|---|
| Timestamp | When the request was made |
| Host | The hostname requested |
| Method | HTTP method (GET, POST, etc.) |
| Pathname | The URL path |
| Query parameters | Parsed query strings |
| Status | HTTP response status code |
| IP address | Client IP (from cf-connecting-ip header) |
| User agent | Browser or client information |
| Referer | HTTP referer header |
| Bytes | Response size in bytes |
| Metric | Value |
|---|---|
| Added latency | <5ms per request |
| Blocking | No (uses waitUntil() for async logging) |
| Origin load | Zero additional requests |
| Bandwidth | ~500 bytes per request to Spinlink |
npm create cloudflare@latest -- spinlink-worker
When prompted, select the “Hello World” Worker template, choose TypeScript, enable Git version control, and skip deploying for now.
cd spinlink-worker
Update your wrangler.json with your domain configuration. Replace example.com with your actual domain.
{
"name": "spinlink-worker",
"main": "src/index.ts",
"compatibility_date": "2024-01-01",
"vars": {
"SPINLINK_API_URL": "https://api.spinlink.io/api/ingestion/logs/cloudflare-worker"
},
"route": {
"pattern": "example.com/*",
"zone_name": "example.com"
}
}To include www or multiple domains, use the routes array format:
{
"name": "spinlink-worker",
"main": "src/index.ts",
"compatibility_date": "2024-01-01",
"vars": {
"SPINLINK_API_URL": "https://api.spinlink.io/api/ingestion/logs/cloudflare-worker"
},
"routes": [
{ "pattern": "example.com/*", "zone_name": "example.com" },
{ "pattern": "www.example.com/*", "zone_name": "example.com" }
]
}Replace the contents of src/index.ts with the following:
export interface Env {
SPINLINK_API_URL: string;
SPINLINK_API_KEY: string;
}
interface LogEntry {
timestamp: number;
host: string;
method: string;
pathname: string;
query_params: Record<string, string>;
status: number;
ip: string;
user_agent: string;
referer: string;
bytes: number;
}
function parseQueryParams(url: URL): Record<string, string> {
const params: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
params[key] = value;
});
return params;
}
async function sendToSpinlink(env: Env, logs: LogEntry[]): Promise<void> {
if (!env.SPINLINK_API_KEY || !env.SPINLINK_API_URL) {
console.error('Spinlink API key or URL not configured');
return;
}
try {
const response = await fetch(env.SPINLINK_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': env.SPINLINK_API_KEY,
},
body: JSON.stringify(logs),
});
if (!response.ok) {
console.error(`Spinlink API error: ${response.status} ${response.statusText}`);
}
} catch (error) {
console.error('Failed to send logs to Spinlink:', error);
}
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url);
// Pass through the request to the origin
const response = await fetch(request);
// Clone the response to read its size
const responseClone = response.clone();
const responseBody = await responseClone.arrayBuffer();
const responseSize = responseBody.byteLength;
// Build log entry
const logEntry: LogEntry = {
timestamp: Date.now(),
host: url.hostname,
method: request.method,
pathname: url.pathname,
query_params: parseQueryParams(url),
status: response.status,
ip: request.headers.get('cf-connecting-ip') || '',
user_agent: request.headers.get('user-agent') || '',
referer: request.headers.get('referer') || '',
bytes: responseSize,
};
// Send log asynchronously (don't block response)
ctx.waitUntil(sendToSpinlink(env, [logEntry]));
// Return the original response
return response;
},
};Store your Spinlink API key as a secret. Never commit it to code.
npx wrangler secret put SPINLINK_API_KEY
When prompted, paste your Spinlink API key and press Enter.
npx wrangler login npx wrangler deploy
You should see output confirming the deployment with your Worker URL.
| Variable | Type | Description |
|---|---|---|
| SPINLINK_API_URL | Variable (wrangler.json) | Analytics ingestion endpoint |
| SPINLINK_API_KEY | Secret (wrangler secret) | Your Spinlink API credentials |
The API key is sent as an X-API-Key header with each request to the Spinlink ingestion endpoint. Always store it as a Wrangler secret, never in your configuration file.
For hotel groups with multiple properties, configure routes for each domain:
{
"routes": [
{ "pattern": "hotel-downtown.com/*", "zone_name": "hotel-downtown.com" },
{ "pattern": "hotel-beach.com/*", "zone_name": "hotel-beach.com" },
{ "pattern": "*.hotelgroup.com/*", "zone_name": "hotelgroup.com" }
]
}curl -I https://yourdomain.com
npx wrangler tail
This shows real-time logs from your Worker. Look for any error messages related to the Spinlink API.
Log in to your Spinlink dashboard and navigate to the Analytics section. Requests should appear within a few seconds.
| Issue | Solution |
|---|---|
| Worker not running | Verify route configuration in Cloudflare Dashboard (Triggers tab). Ensure your domain is proxied through Cloudflare (orange cloud enabled). |
| No logs in Spinlink | Check API key with npx wrangler secret list. Verify the endpoint in wrangler.json. Check Worker logs with npx wrangler tail. |
| 401 Unauthorized | API key is invalid or expired. Generate a new key from your Spinlink dashboard and update with npx wrangler secret put SPINLINK_API_KEY. |
| Timeout errors | Optimize your origin server response time or enable Cloudflare caching to reduce origin requests. |
resource "cloudflare_worker_script" "spinlink" {
name = "spinlink-analytics"
content = file("worker.js")
}- name: Deploy Spinlink Worker
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_TOKEN }}After making changes to your code, redeploy:
npx wrangler deploy
npx wrangler delete spinlink-worker
For analytics on non-Cloudflare sites, we offer an Nginx log forwarding option. See the Nginx Plugin documentation.
No. The Worker uses async processing via waitUntil() and adds less than 5ms of latency. Your visitor receives the response immediately while logging happens in the background.
Yes. The API key is stored as a Cloudflare Worker secret and is never exposed in your configuration files or source code. It is only sent server-side to the Spinlink ingestion endpoint.
For information, see the Security & Compliance documentation.
Need help? Contact support