码桶

发现社区成员的开源项目

analytics.js3.9 KB
export async function onRequestGet(context) {
    const { cfHeaders } = context.data;
    const { zoneId } = context.params;
    const url = new URL(context.request.url);
    const sinceMinutes = parseInt(url.searchParams.get('since') || '-1440');

    const now = new Date();
    const sinceDate = new Date(now.getTime() + sinceMinutes * 60 * 1000);

    // Use daily groups for 30d, hourly for shorter ranges
    const useDaily = sinceMinutes <= -10080;
    const node = useDaily ? 'httpRequests1dGroups' : 'httpRequests1hGroups';
    const dateFilter = useDaily
        ? { date_geq: sinceDate.toISOString().split('T')[0], date_lt: now.toISOString().split('T')[0] }
        : { datetime_geq: sinceDate.toISOString(), datetime_lt: now.toISOString() };

    const query = `query {
        viewer {
            zones(filter: { zoneTag: "${zoneId}" }) {
                ${node}(filter: ${JSON.stringify(dateFilter).replace(/"([^"]+)":/g, '$1:')}, limit: 10000) {
                    sum {
                        requests
                        cachedRequests
                        bytes
                        cachedBytes
                        threats
                        pageViews
                        countryMap { clientCountryName requests threats }
                        responseStatusMap { edgeResponseStatus requests }
                    }
                    uniq { uniques }
                }
            }
        }
    }`;

    try {
        const res = await fetch('https://api.cloudflare.com/client/v4/graphql', {
            method: 'POST',
            headers: { ...cfHeaders, 'Content-Type': 'application/json' },
            body: JSON.stringify({ query })
        });
        const result = await res.json();

        if (result.errors && result.errors.length > 0) {
            return new Response(JSON.stringify({ success: false, errors: result.errors.map(e => ({ message: e.message })) }), {
                status: 400, headers: { 'Content-Type': 'application/json' }
            });
        }

        const groups = result.data?.viewer?.zones?.[0]?.[node] || [];

        // Aggregate all groups into totals matching the old dashboard API format
        const totals = {
            requests: { all: 0, cached: 0, http_status: {}, country: {} },
            bandwidth: { all: 0, cached: 0 },
            threats: { all: 0, country: {} },
            uniques: { all: 0 },
            pageviews: { all: 0 }
        };

        for (const g of groups) {
            const s = g.sum || {};
            totals.requests.all += s.requests || 0;
            totals.requests.cached += s.cachedRequests || 0;
            totals.bandwidth.all += s.bytes || 0;
            totals.bandwidth.cached += s.cachedBytes || 0;
            totals.threats.all += s.threats || 0;
            totals.pageviews.all += s.pageViews || 0;
            totals.uniques.all += g.uniq?.uniques || 0;

            for (const c of (s.countryMap || [])) {
                totals.requests.country[c.clientCountryName] = (totals.requests.country[c.clientCountryName] || 0) + c.requests;
                if (c.threats > 0) {
                    totals.threats.country[c.clientCountryName] = (totals.threats.country[c.clientCountryName] || 0) + c.threats;
                }
            }
            for (const r of (s.responseStatusMap || [])) {
                const status = String(r.edgeResponseStatus);
                totals.requests.http_status[status] = (totals.requests.http_status[status] || 0) + r.requests;
            }
        }

        return new Response(JSON.stringify({ success: true, data: { totals }, errors: [] }), {
            headers: { 'Content-Type': 'application/json' }
        });
    } catch (e) {
        return new Response(JSON.stringify({ success: false, errors: [{ message: e.message || 'Failed to fetch analytics' }] }), {
            status: 500, headers: { 'Content-Type': 'application/json' }
        });
    }
}