Daily Analytics Collection #83
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
| name: Daily Analytics Collection | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| include_lighthouse: | |
| description: 'Run Lighthouse audit (normally weekly only)' | |
| required: false | |
| type: boolean | |
| default: false | |
| schedule: | |
| # GA4 and Search Console run Mon-Sat at 6 AM UTC | |
| - cron: '0 6 * * 1-6' | |
| # Full run with Lighthouse on Sundays at 6 AM UTC (a11y/SEO focus) | |
| - cron: '0 6 * * 0' | |
| jobs: | |
| lighthouse: | |
| # Run weekly on Sundays, or on manual dispatch with include_lighthouse=true | |
| if: github.event.schedule == '0 6 * * 0' || github.event.inputs.include_lighthouse == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run Lighthouse on production (multi-page) | |
| run: node scripts/lighthouse-multi-page.mjs | |
| - name: Save Lighthouse reports | |
| run: | | |
| mkdir -p docs/metrics | |
| cp lighthouse-reports/summary.json docs/metrics/latest-multi-page.json | |
| echo "{\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"commit\": \"${{ github.sha }}\", \"trigger\": \"${{ github.event_name == 'schedule' && 'scheduled' || 'manual' }}\"}" > docs/metrics/last-run.json | |
| - name: Upload Lighthouse reports | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: lighthouse-reports | |
| path: lighthouse-reports/ | |
| retention-days: 7 | |
| outputs: | |
| completed: ${{ job.status == 'success' }} | |
| github-billing: | |
| # Run weekly on Sundays alongside Lighthouse | |
| if: github.event.schedule == '0 6 * * 0' || github.event.inputs.include_lighthouse == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Fetch GitHub billing data | |
| env: | |
| GH_BILLING_TOKEN: ${{ secrets.DEPLOY_TOKEN }} | |
| run: node scripts/fetch-github-billing.js | |
| - name: Upload billing data | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: github-billing-data | |
| path: docs/metrics/github-billing-history.json | |
| retention-days: 7 | |
| search-console: | |
| # Run daily - no dependency on lighthouse (which is weekly) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Fetch Search Console data | |
| env: | |
| SEARCH_CONSOLE_CREDENTIALS: ${{ secrets.SEARCH_CONSOLE_CREDENTIALS }} | |
| run: node scripts/fetch-search-console-data.js | |
| - name: Upload search data | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: search-console-data | |
| path: docs/metrics/search-console-history.json | |
| retention-days: 7 | |
| ga4: | |
| # Run daily - depends on search-console for sequential execution | |
| needs: search-console | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Fetch GA4 analytics data | |
| env: | |
| GA4_CREDENTIALS: ${{ secrets.GA4_CREDENTIALS }} | |
| GA4_PROPERTY_ID: ${{ secrets.GA4_PROPERTY_ID }} | |
| run: node scripts/fetch-ga4-data.js | |
| - name: Upload GA4 data | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ga4-data | |
| path: docs/metrics/ga4-history.json | |
| retention-days: 7 | |
| commit-all: | |
| # Wait for data collection jobs; lighthouse and github-billing are optional (weekly only) | |
| needs: [search-console, ga4, github-billing] | |
| if: always() && (needs.search-console.result == 'success' || needs.ga4.result == 'success') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: write | |
| issues: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts | |
| continue-on-error: true | |
| - name: Merge artifacts into docs/metrics | |
| run: | | |
| mkdir -p docs/metrics | |
| # Copy Lighthouse data (only available on weekly runs) | |
| if [ -f artifacts/lighthouse-reports/summary.json ]; then | |
| cp artifacts/lighthouse-reports/summary.json docs/metrics/latest-multi-page.json | |
| echo "✅ Lighthouse data updated (weekly run)" | |
| else | |
| echo "ℹ️ No Lighthouse data this run (weekly only)" | |
| fi | |
| # Copy Search Console data | |
| if [ -f artifacts/search-console-data/search-console-history.json ]; then | |
| cp artifacts/search-console-data/search-console-history.json docs/metrics/ | |
| echo "✅ Search Console data updated" | |
| fi | |
| # Copy GA4 data | |
| if [ -f artifacts/ga4-data/ga4-history.json ]; then | |
| cp artifacts/ga4-data/ga4-history.json docs/metrics/ | |
| echo "✅ GA4 data updated" | |
| fi | |
| # Copy GitHub billing data (only available on weekly runs) | |
| if [ -f artifacts/github-billing-data/github-billing-history.json ]; then | |
| cp artifacts/github-billing-data/github-billing-history.json docs/metrics/ | |
| echo "✅ GitHub billing data updated (weekly run)" | |
| else | |
| echo "ℹ️ No GitHub billing data this run (weekly only)" | |
| fi | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Regenerate latest.json | |
| env: | |
| GA4_CREDENTIALS: ${{ secrets.GA4_CREDENTIALS }} | |
| GA4_PROPERTY_ID: ${{ secrets.GA4_PROPERTY_ID }} | |
| SEARCH_CONSOLE_CREDENTIALS: ${{ secrets.SEARCH_CONSOLE_CREDENTIALS }} | |
| run: | | |
| # Re-run GA4 script to regenerate latest.json with all data | |
| node scripts/fetch-ga4-data.js | |
| - name: Commit all metrics | |
| run: | | |
| git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git config --local user.name "github-actions[bot]" | |
| git add docs/metrics/ | |
| git diff --staged --quiet || git commit -m "chore: daily analytics update" | |
| git push | |
| - name: Check for anomalies | |
| id: anomaly_check | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const failures = []; | |
| // Check GA4 data using 7-day rolling average to avoid false positives from traffic spikes | |
| const ga4File = 'docs/metrics/ga4-history.json'; | |
| if (fs.existsSync(ga4File)) { | |
| const history = JSON.parse(fs.readFileSync(ga4File, 'utf8')); | |
| if (history.length >= 8) { | |
| const latest = history[history.length - 1]; | |
| const past7 = history.slice(-8, -1); | |
| const avgSessions = past7.reduce((sum, e) => sum + e.summary.sessions, 0) / past7.length; | |
| const sessionsChange = ((latest.summary.sessions - avgSessions) / avgSessions) * 100; | |
| if (sessionsChange < -40) { | |
| failures.push(\`GA4: Sessions dropped by \${Math.abs(Math.round(sessionsChange))}% vs 7-day avg (\${latest.summary.sessions} vs \${Math.round(avgSessions)})\`); | |
| } | |
| } | |
| } | |
| // Check Search Console data | |
| const scFile = 'docs/metrics/search-console-history.json'; | |
| if (fs.existsSync(scFile)) { | |
| const history = JSON.parse(fs.readFileSync(scFile, 'utf8')); | |
| if (history.length >= 2) { | |
| const latest = history[history.length - 1]; | |
| const previous = history[history.length - 2]; | |
| const positionChange = latest.summary.averagePosition - previous.summary.averagePosition; | |
| if (positionChange > 5) { | |
| failures.push(\`Search Console: Position worsened by \${Math.round(positionChange * 10) / 10}\`); | |
| } | |
| } | |
| } | |
| // Check Lighthouse data | |
| const lhFile = 'docs/metrics/latest-multi-page.json'; | |
| if (fs.existsSync(lhFile)) { | |
| const summary = JSON.parse(fs.readFileSync(lhFile, 'utf8')); | |
| summary.forEach(page => { | |
| if (page.performance < 50) { | |
| failures.push(\`Lighthouse: \${page.page} performance at \${page.performance}\`); | |
| } | |
| if (page.accessibility < 95) { | |
| failures.push(\`Lighthouse: \${page.page} accessibility at \${page.accessibility}\`); | |
| } | |
| }); | |
| } | |
| if (failures.length > 0) { | |
| console.log('::set-output name=has_anomalies::true'); | |
| console.log('Anomalies detected:'); | |
| failures.forEach(f => console.log(\` - \${f}\`)); | |
| // Write failures to file for issue creation | |
| fs.writeFileSync('/tmp/anomalies.json', JSON.stringify(failures)); | |
| process.exit(1); | |
| } else { | |
| console.log('✅ No anomalies detected'); | |
| } | |
| " | |
| - name: Create issue for anomalies | |
| if: failure() && steps.anomaly_check.conclusion == 'failure' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const date = new Date().toISOString().split('T')[0]; | |
| let anomalies = []; | |
| try { | |
| anomalies = JSON.parse(fs.readFileSync('/tmp/anomalies.json', 'utf8')); | |
| } catch (e) { | |
| anomalies = ['Unknown anomaly - check workflow logs']; | |
| } | |
| // Suppress duplicate alerts: if an open analytics anomaly issue exists | |
| // from the last 3 days, comment on it instead of creating a new one | |
| const recentIssues = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: 'analytics,automated', | |
| state: 'open', | |
| sort: 'created', | |
| direction: 'desc', | |
| per_page: 1 | |
| }); | |
| const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); | |
| const recentOpen = recentIssues.data.find( | |
| i => new Date(i.created_at) > threeDaysAgo | |
| ); | |
| if (recentOpen) { | |
| // Append today's data as a comment on the existing issue | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: recentOpen.number, | |
| body: `### Update (${date})\n${anomalies.map(a => '- ' + a).join('\n')}\n\n[Workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId})` | |
| }); | |
| core.info(`Appended to existing issue #${recentOpen.number} instead of creating a new one`); | |
| return; | |
| } | |
| const body = [ | |
| '## 📉 Daily Analytics Anomaly Alert', | |
| '', | |
| `**Date:** ${date}`, | |
| '', | |
| '### Detected Issues', | |
| ...anomalies.map(a => `- ${a}`), | |
| '', | |
| '### Actions Required', | |
| `1. Review the [workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId})`, | |
| '2. Check individual data artifacts for details', | |
| '3. Investigate root causes', | |
| '', | |
| '---', | |
| '*This issue was automatically created by the Daily Analytics Collection workflow.*' | |
| ].join('\n'); | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `🚨 Analytics Anomaly Detected (${date})`, | |
| body: body, | |
| labels: ['analytics', 'automated'] | |
| }); |