Install and configure the Nuno 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 Nuno 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 Nuno |
npm create cloudflare@latest -- nuno-worker
When prompted, select the “Hello World” Worker template, choose TypeScript, enable Git version control, and skip deploying for now.
cd nuno-worker
Rename wrangler.json to wrangler.jsonc (to allow comments), then update it with your domain configuration. Replace yourdomain.com with your actual domain.
{
"name": "nuno-worker",
"main": "src/index.ts",
"compatibility_date": "2026-03-10",
"observability": {
"enabled": true
},
"upload_source_maps": true,
"compatibility_flags": [
"nodejs_compat"
],
"vars": {
"NUNO_API_URL": "https://api.nuno.io/api/ingestion/logs/cloudflare-worker"
},
"routes": [
{ "pattern": "yourdomain.com/*", "zone_name": "yourdomain.com" },
{ "pattern": "www.yourdomain.com/*", "zone_name": "yourdomain.com" }
]
}The observability and upload_source_maps settings enable real-time logging and source maps in the Cloudflare dashboard, making debugging easier. The nodejs_compat flag enables Node.js API compatibility.
Replace the contents of src/index.ts with the following. The Worker proxies all requests to your origin, filters out static assets and CMS internals to reduce noise, and forwards relevant request metadata to Nuno.
interface NunoEnv extends Env {
NUNO_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;
}
// Paths that should not be logged (CMS internals, admin panels)
const IGNORED_PREFIXES = [
"/wp-admin/",
"/wp-includes/",
"/wp-login",
"/wp-cron",
"/wp-content/themes/",
"/wp-content/uploads/",
];
// Static file extensions that should not be logged
const IGNORED_EXTENSIONS = new Set([
".js", ".css", ".woff", ".ttf",
".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico",
".mp4",
]);
function shouldLog(pathname: string): boolean {
const lower = pathname.toLowerCase();
if (IGNORED_PREFIXES.some((prefix) => lower.startsWith(prefix))) {
return false;
}
const dotIndex = lower.lastIndexOf(".");
if (dotIndex !== -1 && IGNORED_EXTENSIONS.has(lower.slice(dotIndex))) {
return false;
}
return true;
}
function parseQueryParams(url: URL): Record<string, string> {
const params: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
params[key] = value;
});
return params;
}
async function sendToNuno(
env: NunoEnv,
logs: LogEntry[]
): Promise<void> {
if (!env.NUNO_API_KEY || !env.NUNO_API_URL) {
console.error("Nuno API key or URL not configured");
return;
}
try {
const response = await fetch(env.NUNO_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": env.NUNO_API_KEY,
},
body: JSON.stringify(logs),
});
if (!response.ok) {
console.error(
`Nuno API error: ${response.status} ${response.statusText}`
);
}
} catch (error) {
console.error("Failed to send logs to Nuno:", error);
}
}
export default {
async fetch(request, env: NunoEnv, ctx): Promise<Response> {
const url = new URL(request.url);
// Pass through the request to the origin
const response = await fetch(request);
// Skip logging for static assets and CMS internals
if (!shouldLog(url.pathname)) {
return response;
}
// Use Content-Length header for response size (no body clone needed)
let responseSize = 0;
const contentLength = response.headers.get("content-length");
if (contentLength) {
responseSize = parseInt(contentLength, 10) || 0;
}
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 the response
ctx.waitUntil(sendToNuno(env, [logEntry]));
return response;
},
} satisfies ExportedHandler<NunoEnv>;Store your Nuno API key as a secret. Never commit it to code.
npx wrangler secret put NUNO_API_KEY
When prompted, paste your Nuno API key and press Enter.
npx wrangler login npx wrangler deploy
You should see output confirming the deployment with your Worker URL and configured routes.
The Worker automatically filters out requests that add noise to your analytics. Only meaningful page and API requests are logged.
You can customize the IGNORED_PREFIXES and IGNORED_EXTENSIONS lists in src/index.ts to match your site's structure. For example, add "/admin/" if you use a different CMS, or add ".woff2" for additional font formats.
The code-level filtering above prevents logging, but the Worker still executes on every request. For high-traffic sites you can skip Worker execution entirely for certain path prefixes by creating routes with no worker attached. A route without a worker takes priority over more general patterns.
In the Cloudflare dashboard, go to your zone > Workers Routes and add routes like:
yourdomain.com/wp-content/* → (no worker) yourdomain.com/wp-admin/* → (no worker) yourdomain.com/wp-includes/* → (no worker) yourdomain.com/* → nuno-worker
This way Cloudflare never invokes the Worker for those paths, saving execution time and Worker invocations on your plan. Note that bypass routes only work with path prefixes — you cannot exclude by file extension (e.g. *.jpg). For extension-based filtering, the code-level filtering handles it.
| Variable | Type | Description |
|---|---|---|
| NUNO_API_URL | Variable (wrangler.jsonc) | Analytics ingestion endpoint |
| NUNO_API_KEY | Secret (wrangler secret) | Your Nuno API credentials |
The API key is sent as an X-API-Key header with each request to the Nuno 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 Nuno API.
Log in to your Nuno 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 Nuno | Check API key with npx wrangler secret list. Verify the endpoint in wrangler.jsonc. Check Worker logs with npx wrangler tail. |
| Request not being logged | The Worker filters static assets (.js, .css, images) and CMS paths (/wp-admin/, etc.). Test with a page URL like / or /about instead. |
| 401 Unauthorized | API key is invalid or expired. Generate a new key from your Nuno dashboard and update with npx wrangler secret put NUNO_API_KEY. |
| Timeout errors | Optimize your origin server response time or enable Cloudflare caching to reduce origin requests. |
resource "cloudflare_worker_script" "nuno" {
name = "nuno-worker"
content = file("dist/index.js")
}- name: Deploy Nuno 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
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 Nuno ingestion endpoint.
For information, see the Security & Compliance documentation.
Need help? Contact support