Created
July 4, 2025 11:10
-
-
Save maesoser/eca1849f1d96c0c3393f9b49721de1ff to your computer and use it in GitHub Desktop.
Cloudflare Worker for Domain Traffic Analytics API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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