Skip to content

Instantly share code, notes, and snippets.

@maesoser
Created July 4, 2025 11:10
Show Gist options
  • Save maesoser/eca1849f1d96c0c3393f9b49721de1ff to your computer and use it in GitHub Desktop.
Save maesoser/eca1849f1d96c0c3393f9b49721de1ff to your computer and use it in GitHub Desktop.
Cloudflare Worker for Domain Traffic Analytics API
// Cloudflare Worker for Domain Traffic Analytics API
// Example wrangler.toml configuration:
/*
name = "domain-traffic-api"
main = "src/index.js"
compatibility_date = "2024-01-01"
[env.production.vars]
CLOUDFLARE_ACCOUNT_TAG = "your-actual-account-tag"
CLOUDFLARE_API_TOKEN = "your-actual-api-token"
# Usage example:
# POST to your worker URL with:
# {
# "domains": ["example.com", "another-domain.com", "third-domain.org"]
# }
*/
export default {
async fetch(request, env, ctx) {
// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
});
}
// Only allow POST requests
if (request.method !== 'POST') {
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
status: 405,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
try {
// Parse the incoming JSON request
const requestBody = await request.json();
if (!requestBody.domains || !Array.isArray(requestBody.domains)) {
return new Response(JSON.stringify({
error: 'Invalid request: domains array is required'
}), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
// Configuration - Replace these with your actual values
const ACCOUNT_TAG = env.CLOUDFLARE_ACCOUNT_TAG ;
const API_TOKEN = env.CLOUDFLARE_API_TOKEN ;
const GRAPHQL_ENDPOINT = 'https://api.cloudflare.com/client/v4/graphql';
// Calculate date range (current day minus 30 to current day)
const until = new Date().toISOString().split('T')[0];
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
// GraphQL query
const query = `
query GetTrafficData($account: String!, $since: Date!, $until: Date!, $domains: [ZoneHttpRequestsAdaptiveGroupsFilter!]!) {
viewer {
accounts(filter: { accountTag: $account }) {
requests: httpRequestsAdaptiveGroups(
limit: 10000,
filter: { date_geq: $since, date_leq: $until, AND: $domains }
) {
sum { trafficBytes: edgeResponseBytes }
trafficRequests: count
}
}
}
}
`;
// Process each domain and collect results
const results = await Promise.allSettled(
requestBody.domains.map(async (domain) => {
// Build domain filter
const domainFilter = [
{ requestSource: "eyeball" },
{ clientRequestHTTPHost_like: domain }
];
const variables = {
account: ACCOUNT_TAG,
since: since,
until: until,
domains: domainFilter
};
// Make GraphQL request
const response = await fetch(GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
variables: variables
})
});
if (!response.ok) {
throw new Error(`GraphQL request failed: ${response.statusText}`);
}
const data = await response.json();
if (data.errors && data.errors.length > 0) {
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
}
return {
domain: domain,
data: data
};
})
);
// Process results and handle any failures
const successfulResults = [];
const failedResults = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successfulResults.push(result.value);
} else {
failedResults.push({
domain: requestBody.domains[index],
error: result.reason.message
});
}
});
// Normalize and aggregate the response
const normalizedResponse = {
stats: successfulResults.map(result => {
const accountData = result.data.data?.viewer?.accounts?.[0];
const requests = accountData?.requests || [];
// Aggregate all requests for this domain
const totalBytes = requests.reduce((sum, req) => sum + (req.sum?.trafficBytes || 0), 0 );
const totalRequests = requests.reduce((sum, req) => sum + (req.trafficRequests || 0), 0 );
return {
domain: result.domain,
trafficBytes: totalBytes,
trafficRequests: totalRequests,
};
}),
errors: failedResults.length > 0 ? failedResults : null,
since: since,
until: until,
totalDomains: requestBody.domains.length,
successfulQueries: successfulResults.length,
failedQueries: failedResults.length
};
// Calculate totalTrafficRequest and totalTrafficBytes
const totalTrafficRequest = normalizedResponse.stats.reduce((sum, stat) => sum + stat.trafficRequests, 0);
const totalTrafficBytes = normalizedResponse.stats.reduce((sum, stat) => sum + stat.trafficBytes, 0);
// Add them to the normalizedResponse object
normalizedResponse.totalTrafficRequest = totalTrafficRequest;
normalizedResponse.totalTrafficBytes = totalTrafficBytes;
return new Response(JSON.stringify(normalizedResponse, null, 2), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
} catch (error) {
console.error('Worker error:', error);
return new Response(JSON.stringify({
error: 'Internal server error',
message: error.message
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment