/** * Cloudflare Worker: Pulse — Weekly KPI Report * * Collects metrics from GitHub, Discord, Supabase, npm, and Google Analytics, * then sends a consolidated report via Telegram every Sunday at 14:00 UTC. * * All source collectors are in this single file (no npm dependencies). * * Error tracking: set SENTRY_DSN (wrangler secret put SENTRY_DSN) to report * failed collectors to the "aitmpl-workers" Sentry project. Optional — the * worker degrades gracefully (console.error only) if it's not configured. */ import { reportError, checkIn } from './sentry.js'; const REPO = 'davila7/claude-code-templates'; const NPM_PACKAGE = 'claude-code-templates'; const GITHUB_API = 'https://api.github.com'; const DISCORD_API = 'https://discord.com/api/v10'; const NPM_API = 'https://api.npmjs.org'; const GA4_API = 'https://analyticsdata.googleapis.com/v1beta'; const TELEGRAM_API = 'https://api.telegram.org'; const MAX_MESSAGE_LENGTH = 4096; // ─── Entry Points ──────────────────────────────────────────────────────────── export default { async scheduled(event, env, ctx) { console.log('📊 Pulse: Starting weekly report (cron)...'); await runReport(env); }, async fetch(request, env, ctx) { const url = new URL(request.url); // Manual trigger if (url.pathname === '/trigger' && request.method === 'POST') { const authHeader = request.headers.get('Authorization'); if (!env.TRIGGER_SECRET || authHeader !== `Bearer ${env.TRIGGER_SECRET}`) { return jsonResponse({ error: 'Unauthorized' }, 401); } const onlySource = url.searchParams.get('source'); const sendTelegram = url.searchParams.get('send') !== 'false'; const result = await runReport(env, { onlySource, sendTelegram }); return jsonResponse(result); } // Status endpoint if (url.pathname === '/status') { return jsonResponse({ status: 'running', worker: 'pulse-weekly-report', schedule: 'Sundays 14:00 UTC', sources: ['github', 'discord', 'downloads', 'npm', 'analytics'] }); } return new Response( 'Pulse — Weekly KPI Report Worker\n\nEndpoints:\n- POST /trigger (requires auth)\n- GET /status', { headers: { 'Content-Type': 'text/plain' } } ); } }; // ─── Report Runner ─────────────────────────────────────────────────────────── async function runReport(env, opts = {}) { const { onlySource, sendTelegram = true } = opts; const checkInId = await checkIn(env, 'pulse-weekly-report', 'in_progress'); let results = {}; if (onlySource) { const collectors = { github: collectGitHub, discord: collectDiscord, downloads: collectDownloads, npm: collectNpm, analytics: collectAnalytics }; const collector = collectors[onlySource]; if (!collector) { return { error: `Unknown source: ${onlySource}`, available: Object.keys(collectors) }; } results[onlySource] = await collector(env); } else { const [github, discord, downloads, npm] = await Promise.all([ collectGitHub(env), collectDiscord(env), collectDownloads(env), collectNpm(env) ]); results = { github, discord, downloads, npm }; } // Report any failed collectors to Sentry (they still degrade gracefully // into "Unavailable" in the rendered report — this just makes failures visible). const failedSources = Object.entries(results).filter(([, v]) => v && v.error); for (const [source, value] of failedSources) { await reportError(env, `pulse collector "${source}" failed: ${value.error}`, { worker: 'pulse-weekly-report', source }); } const reportText = formatReport(results); let telegramResult = null; if (sendTelegram) { telegramResult = await sendToTelegram(env, reportText); } await checkIn(env, 'pulse-weekly-report', failedSources.length > 0 ? 'error' : 'ok', checkInId); return { success: true, sources: Object.fromEntries( Object.entries(results).map(([k, v]) => [k, v.error ? 'error' : 'ok']) ), report: reportText, telegram: sendTelegram ? telegramResult : 'skipped' }; } // ─── Source: GitHub ────────────────────────────────────────────────────────── async function collectGitHub(env) { try { if (!env.GITHUB_TOKEN) return { error: 'GITHUB_TOKEN not configured' }; const headers = { Accept: 'application/vnd.github.v3+json', Authorization: `Bearer ${env.GITHUB_TOKEN}`, 'User-Agent': 'Pulse-Weekly-Report/1.0' }; const since = weekAgo(); const sinceISO = since.toISOString(); const [repoRes, issuesRes, prsRes] = await Promise.all([ fetchJSON(`${GITHUB_API}/repos/${REPO}`, { headers }), fetchJSON(`${GITHUB_API}/repos/${REPO}/issues?state=all&since=${sinceISO}&per_page=100`, { headers }), fetchJSON(`${GITHUB_API}/repos/${REPO}/pulls?state=all&sort=updated&direction=desc&per_page=100`, { headers }) ]); const repo = repoRes; // Filter issues (exclude PRs) created this week const weekIssues = issuesRes.filter( i => !i.pull_request && new Date(i.created_at) >= since ); const closedThisWeek = issuesRes.filter( i => !i.pull_request && i.state === 'closed' && i.closed_at && new Date(i.closed_at) >= since ); // Filter PRs from this week const weekPRs = prsRes.filter(pr => new Date(pr.created_at) >= since); const mergedPRs = prsRes.filter(pr => pr.merged_at && new Date(pr.merged_at) >= since); // Count new stars this week — paginate to the last page to get most recent let starsWeek = null; try { const starHeaders = { ...headers, Accept: 'application/vnd.github.v3.star+json' }; // HEAD request to get total pages from Link header const headRes = await fetch(`${GITHUB_API}/repos/${REPO}/stargazers?per_page=100`, { method: 'HEAD', headers: starHeaders }); const linkHeader = headRes.headers.get('Link') || ''; const lastMatch = linkHeader.match(/page=(\d+)>; rel="last"/); const lastPage = lastMatch ? parseInt(lastMatch[1]) : 1; // Fetch last few pages to find stars from this week let recentStars = []; for (let page = lastPage; page >= Math.max(1, lastPage - 2); page--) { const pageRes = await fetchJSON( `${GITHUB_API}/repos/${REPO}/stargazers?per_page=100&page=${page}`, { headers: starHeaders } ); recentStars = recentStars.concat(pageRes); } starsWeek = recentStars.filter(s => new Date(s.starred_at) >= since).length; } catch { /* stargazers with timestamps may not always work */ } // Estimate new forks this week let forksWeek = null; try { const forkRes = await fetchJSON( `${GITHUB_API}/repos/${REPO}/forks?sort=newest&per_page=100`, { headers } ); forksWeek = forkRes.filter(f => new Date(f.created_at) >= since).length; } catch { /* ignore */ } return { stars: repo.stargazers_count, starsWeek, forks: repo.forks_count, forksWeek, openIssues: repo.open_issues_count - weekPRs.filter(pr => pr.state === 'open').length, issuesOpenedWeek: weekIssues.length, issuesClosedWeek: closedThisWeek.length, prsOpenedWeek: weekPRs.length, prsMergedWeek: mergedPRs.length }; } catch (error) { console.error('GitHub source error:', error.message); return { error: error.message }; } } // ─── Source: Discord ───────────────────────────────────────────────────────── async function collectDiscord(env) { try { if (!env.DISCORD_BOT_TOKEN || !env.DISCORD_GUILD_ID) { return { error: 'DISCORD_BOT_TOKEN or DISCORD_GUILD_ID not configured' }; } const headers = { Authorization: `Bot ${env.DISCORD_BOT_TOKEN}`, 'Content-Type': 'application/json' }; const guildId = env.DISCORD_GUILD_ID; // Fetch guild info with approximate counts const guild = await fetchJSON( `${DISCORD_API}/guilds/${guildId}?with_counts=true`, { headers } ); const totalMembers = guild.approximate_member_count || 0; const activeMembers = guild.approximate_presence_count || 0; // Estimate new members this week from audit log let newMembersWeek = null; try { const since = weekAgo(); const auditRes = await fetchJSON( `${DISCORD_API}/guilds/${guildId}/audit-logs?action_type=1&limit=100`, { headers } ); const entries = auditRes.audit_log_entries || []; newMembersWeek = entries.filter(e => { const timestamp = Number(BigInt(e.id) >> 22n) + 1420070400000; return timestamp >= since.getTime(); }).length; } catch { /* Audit log access may not be available */ } // Count messages in last 7 days across text channels let messagesWeek = null; try { const channels = await fetchJSON( `${DISCORD_API}/guilds/${guildId}/channels`, { headers } ); const textChannels = channels.filter(c => c.type === 0); const since = weekAgo(); const sinceSnowflake = String((BigInt(since.getTime()) - 1420070400000n) << 22n); let total = 0; // Sample up to 10 channels to avoid rate limits for (const ch of textChannels.slice(0, 10)) { try { const msgs = await fetchJSON( `${DISCORD_API}/channels/${ch.id}/messages?after=${sinceSnowflake}&limit=100`, { headers } ); total += msgs.length; } catch { /* Channel may not be accessible */ } } messagesWeek = total; } catch { /* ignore */ } return { totalMembers, activeMembers, newMembersWeek, messagesWeek }; } catch (error) { console.error('Discord source error:', error.message); return { error: error.message }; } } // ─── Source: Supabase Downloads (PostgREST) ────────────────────────────────── async function collectDownloads(env) { try { if (!env.SUPABASE_URL || !env.SUPABASE_SERVICE_ROLE_KEY) { return { error: 'SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY not configured' }; } const baseUrl = `${env.SUPABASE_URL}/rest/v1`; const headers = { apikey: env.SUPABASE_SERVICE_ROLE_KEY, Authorization: `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`, 'Content-Type': 'application/json', Prefer: 'count=exact' }; const since = weekAgo(); const sinceISO = since.toISOString(); // Total downloads all-time (HEAD request with count) const totalRes = await fetch(`${baseUrl}/component_downloads?select=*`, { method: 'HEAD', headers }); const totalAllTime = parseInt(totalRes.headers.get('content-range')?.split('/')[1]) || 0; // Downloads this week (HEAD request with count + filter) const weekRes = await fetch( `${baseUrl}/component_downloads?select=*&download_timestamp=gte.${sinceISO}`, { method: 'HEAD', headers } ); const totalWeek = parseInt(weekRes.headers.get('content-range')?.split('/')[1]) || 0; // Get weekly downloads with component info for top components + type breakdown const weekDownloads = await fetchJSON( `${baseUrl}/component_downloads?select=component_name,component_type&download_timestamp=gte.${sinceISO}&limit=10000`, { headers: { ...headers, Prefer: '' } } ); let topComponents = []; let byType = {}; if (weekDownloads && weekDownloads.length > 0) { const componentCounts = {}; const typeCounts = {}; for (const row of weekDownloads) { componentCounts[row.component_name] = (componentCounts[row.component_name] || 0) + 1; const t = row.component_type || 'unknown'; typeCounts[t] = (typeCounts[t] || 0) + 1; } topComponents = Object.entries(componentCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([name, count]) => ({ name, count })); const total = weekDownloads.length || 1; for (const [type, count] of Object.entries(typeCounts)) { byType[type] = (count / total) * 100; } } // Top 5 countries this week let topCountries = []; const countryData = await fetchJSON( `${baseUrl}/component_downloads?select=country&download_timestamp=gte.${sinceISO}&country=not.is.null&limit=10000`, { headers: { ...headers, Prefer: '' } } ); if (countryData && countryData.length > 0) { const countryCounts = {}; for (const row of countryData) { countryCounts[row.country] = (countryCounts[row.country] || 0) + 1; } const total = countryData.length || 1; topCountries = Object.entries(countryCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([country, count]) => ({ country, pct: (count / total) * 100 })); } return { totalAllTime, totalWeek, topComponents, byType, topCountries }; } catch (error) { console.error('Supabase downloads source error:', error.message); return { error: error.message }; } } // ─── Source: npm Downloads ─────────────────────────────────────────────────── async function collectNpm(env) { try { const headers = { 'User-Agent': 'Pulse-Weekly-Report/1.0' }; // Weekly downloads (last 7 days) const weekData = await fetchJSON( `${NPM_API}/downloads/point/last-week/${NPM_PACKAGE}`, { headers } ); // Monthly downloads (last 30 days) for context const monthData = await fetchJSON( `${NPM_API}/downloads/point/last-month/${NPM_PACKAGE}`, { headers } ); // Daily breakdown for the last week (to show trend) const end = new Date(); const start = weekAgo(); const startStr = start.toISOString().split('T')[0]; const endStr = end.toISOString().split('T')[0]; const rangeData = await fetchJSON( `${NPM_API}/downloads/range/${startStr}:${endStr}/${NPM_PACKAGE}`, { headers } ); // Find peak day let peakDay = null; let peakDownloads = 0; if (rangeData.downloads) { for (const day of rangeData.downloads) { if (day.downloads > peakDownloads) { peakDownloads = day.downloads; peakDay = day.day; } } } // Calculate daily average const dailyAvg = weekData.downloads > 0 ? Math.round(weekData.downloads / 7) : 0; return { weeklyDownloads: weekData.downloads || 0, monthlyDownloads: monthData.downloads || 0, dailyAvg, peakDay, peakDownloads }; } catch (error) { console.error('npm source error:', error.message); return { error: error.message }; } } // ─── Source: Google Analytics (GA4) ────────────────────────────────────────── async function collectAnalytics(env) { try { if (!env.GA_PROPERTY_ID || !env.GA_SERVICE_ACCOUNT_JSON) { return { error: 'GA_PROPERTY_ID or GA_SERVICE_ACCOUNT_JSON not configured' }; } const accessToken = await getGAAccessToken(env); const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }; const since = weekAgo(); const startDate = since.toISOString().split('T')[0]; const endDate = new Date().toISOString().split('T')[0]; const propertyId = env.GA_PROPERTY_ID; const [summaryRes, pagesRes, referrersRes] = await Promise.all([ fetchJSON(`${GA4_API}/properties/${propertyId}:runReport`, { method: 'POST', headers, body: JSON.stringify({ dateRanges: [{ startDate, endDate }], metrics: [{ name: 'activeUsers' }, { name: 'screenPageViews' }] }) }), fetchJSON(`${GA4_API}/properties/${propertyId}:runReport`, { method: 'POST', headers, body: JSON.stringify({ dateRanges: [{ startDate, endDate }], dimensions: [{ name: 'pagePath' }], metrics: [{ name: 'screenPageViews' }], orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }], limit: 10 }) }), fetchJSON(`${GA4_API}/properties/${propertyId}:runReport`, { method: 'POST', headers, body: JSON.stringify({ dateRanges: [{ startDate, endDate }], dimensions: [{ name: 'sessionSource' }], metrics: [{ name: 'sessions' }], orderBys: [{ metric: { metricName: 'sessions' }, desc: true }], limit: 10 }) }) ]); const summaryRow = summaryRes.rows?.[0]; const visitors = summaryRow ? parseInt(summaryRow.metricValues[0].value) : 0; const pageviews = summaryRow ? parseInt(summaryRow.metricValues[1].value) : 0; const topPages = (pagesRes.rows || []).map(row => ({ path: row.dimensionValues[0].value, views: parseInt(row.metricValues[0].value) })); const referrerRows = referrersRes.rows || []; const totalSessions = referrerRows.reduce((sum, r) => sum + parseInt(r.metricValues[0].value), 0) || 1; const topReferrers = referrerRows.map(row => ({ source: row.dimensionValues[0].value, sessions: parseInt(row.metricValues[0].value), pct: (parseInt(row.metricValues[0].value) / totalSessions) * 100 })); return { visitors, pageviews, topPages, topReferrers }; } catch (error) { console.error('Google Analytics source error:', error.message); return { error: error.message }; } } // GA4 JWT auth — build and sign JWT for service account async function getGAAccessToken(env) { const saJson = atob(env.GA_SERVICE_ACCOUNT_JSON); const sa = JSON.parse(saJson); const now = Math.floor(Date.now() / 1000); const headerB64 = base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' })); const payloadB64 = base64url(JSON.stringify({ iss: sa.client_email, scope: 'https://www.googleapis.com/auth/analytics.readonly', aud: 'https://oauth2.googleapis.com/token', iat: now, exp: now + 3600 })); const signingInput = `${headerB64}.${payloadB64}`; // Import RSA private key and sign const pemContents = sa.private_key .replace(/-----BEGIN PRIVATE KEY-----/, '') .replace(/-----END PRIVATE KEY-----/, '') .replace(/\n/g, ''); const keyData = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0)); const cryptoKey = await crypto.subtle.importKey( 'pkcs8', keyData.buffer, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign'] ); const signatureBuffer = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', cryptoKey, new TextEncoder().encode(signingInput)); const signatureB64 = base64url(signatureBuffer); const jwt = `${signingInput}.${signatureB64}`; // Exchange JWT for access token const tokenRes = await fetchJSON('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${jwt}` }); return tokenRes.access_token; } // ─── Report Formatter ──────────────────────────────────────────────────────── function formatReport(results) { const sections = []; sections.push(`📊 Claude Code Templates - Weekly Report\n📅 ${getWeekRange()}`); // Downloads if (results.downloads?.error) { sections.push(`DOWNLOADS\n└ Unavailable: ${results.downloads.error}`); } else if (results.downloads) { const dl = results.downloads; const lines = [ `DOWNLOADS (aitmpl.com)`, `├ Total: ${fmt(dl.totalAllTime)}${fmtDelta(dl.totalWeek)}` ]; if (dl.topComponents?.length > 0) { lines.push(`├ Top: ${dl.topComponents[0].name} (${fmt(dl.topComponents[0].count)})`); } if (dl.byType && Object.keys(dl.byType).length > 0) { const types = Object.entries(dl.byType).sort((a, b) => b[1] - a[1]).filter(([, pct]) => pct >= 1); for (let i = 0; i < types.length; i++) { const prefix = (i === types.length - 1 && dl.topComponents?.length === 0) ? '└' : '├'; lines.push(`${prefix} ${types[i][0]}: ${fmtPct(types[i][1])}`); } } // fix last item to └ if (lines.length > 1) { lines[lines.length - 1] = lines[lines.length - 1].replace('├', '└'); } sections.push(lines.join('\n')); } // npm if (results.npm?.error) { sections.push(`NPM (npmjs.com)\n└ ⚠️ Unavailable: ${results.npm.error}`); } else if (results.npm) { const n = results.npm; const lines = [ `NPM (npmjs.com)`, `├ Weekly: ${fmt(n.weeklyDownloads)}`, `├ Monthly: ${fmt(n.monthlyDownloads)}`, `├ Daily avg: ${fmt(n.dailyAvg)}` ]; if (n.peakDay) { const peakDate = new Date(n.peakDay); const dayName = peakDate.toLocaleDateString('en-US', { weekday: 'short' }); lines.push(`└ Peak: ${dayName} (${fmt(n.peakDownloads)})`); } else { lines[lines.length - 1] = lines[lines.length - 1].replace('├', '└'); } sections.push(lines.join('\n')); } // GitHub if (results.github?.error) { sections.push(`GITHUB\n└ Unavailable: ${results.github.error}`); } else if (results.github) { const g = results.github; sections.push([ `GITHUB`, `├ Stars: ${fmt(g.stars)}${fmtDelta(g.starsWeek)}`, `├ Forks: ${fmt(g.forks)}${fmtDelta(g.forksWeek)}`, `├ Issues: ${fmt(g.openIssues)} open (${fmt(g.issuesOpenedWeek)} new, ${fmt(g.issuesClosedWeek)} closed)`, `└ PRs: ${fmt(g.prsOpenedWeek)} opened, ${fmt(g.prsMergedWeek)} merged` ].join('\n')); } // Discord if (results.discord?.error) { sections.push(`DISCORD\n└ Unavailable: ${results.discord.error}`); } else if (results.discord) { const d = results.discord; sections.push([ `DISCORD`, `├ Members: ${fmt(d.totalMembers)}${fmtDelta(d.newMembersWeek)}`, `├ Active: ~${fmt(d.activeMembers)}`, `└ Messages: ${fmt(d.messagesWeek)}` ].join('\n')); } return sections.join('\n\n'); } // ─── Telegram Sender ───────────────────────────────────────────────────────── async function sendToTelegram(env, text) { const botToken = env.TELEGRAM_BOT_TOKEN; const chatId = env.TELEGRAM_CHAT_ID; if (!botToken || !chatId) { throw new Error('Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID'); } const messages = splitMessage(text); let sent = 0; for (const msg of messages) { const response = await fetch(`${TELEGRAM_API}/bot${botToken}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text: msg, parse_mode: 'HTML', disable_web_page_preview: true }) }); const result = await response.json(); if (!result.ok) { console.error('Telegram API error:', JSON.stringify(result)); return { success: false, error: result.description }; } sent++; } console.log(`✅ Telegram: sent ${sent} message(s)`); return { success: true, messagesSent: sent }; } function splitMessage(text) { if (text.length <= MAX_MESSAGE_LENGTH) return [text]; const messages = []; const sections = text.split('\n\n'); let current = ''; for (const section of sections) { if ((current + '\n\n' + section).length > MAX_MESSAGE_LENGTH) { if (current) messages.push(current.trim()); current = section; } else { current = current ? current + '\n\n' + section : section; } } if (current) messages.push(current.trim()); return messages; } // ─── Helpers ───────────────────────────────────────────────────────────────── async function fetchJSON(url, opts = {}) { const response = await fetch(url, opts); if (!response.ok) { const body = await response.text(); throw new Error(`HTTP ${response.status}: ${body.substring(0, 200)}`); } return response.json(); } function jsonResponse(data, status = 200) { return new Response(JSON.stringify(data, null, 2), { status, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } }); } function weekAgo() { const d = new Date(); d.setDate(d.getDate() - 7); return d; } function getWeekRange() { const end = new Date(); const start = weekAgo(); const opts = { month: 'short', day: 'numeric', year: 'numeric' }; return `${start.toLocaleDateString('en-US', opts)} - ${end.toLocaleDateString('en-US', opts)}`; } function fmt(n) { if (n == null) return '—'; return Number(n).toLocaleString('en-US'); } function fmtDelta(n) { if (n == null) return ''; const sign = n >= 0 ? '+' : ''; return ` (${sign}${fmt(n)})`; } function fmtPct(n) { if (n == null) return '—'; return `${Math.round(n)}%`; } function base64url(input) { let str; if (input instanceof ArrayBuffer) { str = btoa(String.fromCharCode(...new Uint8Array(input))); } else { str = btoa(input); } return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }