#!/usr/bin/env python3 """ GitHub Traffic Statistics Script Author: Roman Tsisyk Date: 2025-02-16 Description: This script collects detailed traffic statistics for all your GitHub repositories. It uses the GitHub API to gather data on: - Views (last 14 days), including daily breakdown and unique visitors. - Clones (last 14 days), including daily breakdown and unique cloners. - Releases and downloads (if available). The script generates: - A text report saved to "github_traffic_report.txt". - An HTML dashboard ("dashboard.html") with interactive charts powered by Chart.js. - A modern UI with a professional look. Usage: 1. Copy your GitHub Personal Access Token (PAT) to your clipboard. 2. Run this script and simply press Enter when prompted. 3. Enjoy your report and interactive dashboard! ================================================================================ Enter your GitHub PAT (or press Enter to use clipboard): """ import os import requests import json from collections import defaultdict import getpass # Optional: try to import pyperclip for clipboard support. try: import pyperclip except ImportError: print("pyperclip not found. Install it with 'pip install pyperclip' for clipboard support.") pyperclip = None # --- Display Script Information --- print("\n" + "=" * 80) print("GitHub Traffic Statistics Script") print("Author: Roman Tsisyk") print("Date: 2025-02-16\n") print("Description:") print(" This script collects detailed traffic statistics for all your GitHub repositories.") print(" It uses the GitHub API to gather data on:") print(" - Views (last 14 days), including daily breakdown and unique visitors.") print(" - Clones (last 14 days), including daily breakdown and unique cloners.") print(" - Releases and downloads (if available).\n") print(" The script generates:") print(' - A text report saved to "github_traffic_report.txt".') print(' - An HTML dashboard ("dashboard.html") with interactive charts powered by Chart.js.\n') print("Usage:") print(" 1. Copy your GitHub Personal Access Token (PAT) to your clipboard.") print(" 2. Run this script and simply press Enter when prompted.") print(" 3. Enjoy your report and interactive dashboard!") print("=" * 80 + "\n") # --- Configuration via Console Input --- token_input = getpass.getpass("Enter your GitHub PAT (or press Enter to use clipboard): ").strip() if not token_input and pyperclip: token_input = pyperclip.paste().strip() if not token_input: raise ValueError("No token provided. Please copy your GitHub PAT into the clipboard and try again.") GITHUB_TOKEN = token_input # Auto-detect GitHub username using the API (no manual input) BASE_API_URL = "https://api.github.com" HEADERS = { "Authorization": f"Bearer {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json" } try: resp = requests.get(f"{BASE_API_URL}/user", headers=HEADERS) resp.raise_for_status() user_info = resp.json() GITHUB_USER = user_info["login"] print(f"\nDetected GitHub username: {GITHUB_USER}\n") except Exception as e: print(f"Error detecting user: {e}") exit(1) # --- Helper Functions for Report Generation --- def save_report_to_file(report, filename="github_traffic_report.txt"): """Saves the full report to a file (append mode).""" with open(filename, "a", encoding="utf-8") as file: file.write(report) file.write("\n\n") print(f"Report saved to '{filename}'.") def get_user_repos(user, per_page=100): """Retrieves all repositories for the given user using simple pagination.""" all_repos = [] page = 1 while True: url = f"{BASE_API_URL}/users/{user}/repos" params = {"per_page": per_page, "page": page} resp = requests.get(url, headers=HEADERS, params=params) resp.raise_for_status() repos = resp.json() if not repos: break all_repos.extend(repos) page += 1 return all_repos def get_repo_traffic_views(owner, repo): """Retrieves view statistics (last 14 days) for a repository.""" url = f"{BASE_API_URL}/repos/{owner}/{repo}/traffic/views" resp = requests.get(url, headers=HEADERS) resp.raise_for_status() return resp.json() def get_repo_traffic_clones(owner, repo): """Retrieves clone statistics (last 14 days) for a repository.""" url = f"{BASE_API_URL}/repos/{owner}/{repo}/traffic/clones" resp = requests.get(url, headers=HEADERS) resp.raise_for_status() return resp.json() def get_repo_releases(owner, repo): """Retrieves the list of releases for a repository.""" url = f"{BASE_API_URL}/repos/{owner}/{repo}/releases" resp = requests.get(url, headers=HEADERS) if resp.status_code == 404: return [] resp.raise_for_status() return resp.json() def print_views_details(views_data): """Returns a string with daily view details (removes 'T00:00:00Z').""" report = "" for view in views_data.get("views", []): date = view["timestamp"].replace("T00:00:00Z", "") daily_views = view["count"] report += f" * {date}: {daily_views} views\n" if not report: report = " (No data available)\n" return report def print_clones_details(clones_data): """Returns a string with daily clone details (removes 'T00:00:00Z').""" report = "" for clone in clones_data.get("clones", []): date = clone["timestamp"].replace("T00:00:00Z", "") daily_clones = clone["count"] report += f" * {date}: {daily_clones} clones\n" if not report: report = " (No data available)\n" return report # --- Formatting for Console Output using Rich --- from rich.console import Console from rich.panel import Panel from rich.progress import track from rich.box import ROUNDED from rich.text import Text console = Console() def format_repo_panel(repo_name, panel_text): """Formats a repository panel with rounded borders, word wrapping, and consistent padding.""" wrapped_text = Text(panel_text, overflow="fold", no_wrap=False) return Panel(wrapped_text, title=f"[bold blue]{repo_name}[/bold blue]", border_style="bright_blue", padding=(1, 2), expand=False, box=ROUNDED) # --- Main Function --- def main(): console.rule("[bold blue]GitHub Traffic Report[/bold blue]") console.print(f"[bold]User:[/bold] {GITHUB_USER}\n", style="green") repos = get_user_repos(GITHUB_USER) if not repos: console.print("[red]No repositories found.[/red]") input("Press Enter to exit...") return full_report = "" # Containers for HTML dashboard data views_by_repo = {} # { repo_name: { date: view_count } } clones_by_repo = {} # { repo_name: { date: clone_count } } downloads_by_repo = {} # { repo_name: total downloads } for r in track(repos, description="Processing repositories..."): repo_name = r["name"] full_name = r["full_name"] panel_text = f"[bold]Repository:[/bold] {full_name}\n" views_by_repo[repo_name] = {} clones_by_repo[repo_name] = {} downloads_by_repo[repo_name] = 0 try: views_data = get_repo_traffic_views(GITHUB_USER, repo_name) clones_data = get_repo_traffic_clones(GITHUB_USER, repo_name) total_views = views_data.get("count", 0) unique_visitors = views_data.get("uniques", 0) total_clones = clones_data.get("count", 0) unique_cloners = clones_data.get("uniques", 0) panel_text += f"[cyan]Views (last 14 days):[/cyan] {total_views}\n" panel_text += f"[cyan]Unique Visitors:[/cyan] {unique_visitors}\n" panel_text += f"[magenta]Clones (last 14 days):[/magenta] {total_clones}\n" panel_text += f"[magenta]Unique Cloners:[/magenta] {unique_cloners}\n\n" panel_text += "[bold]Daily Views:[/bold]\n" + print_views_details(views_data) panel_text += "\n[bold]Daily Clones:[/bold]\n" + print_clones_details(clones_data) for view in views_data.get("views", []): date = view["timestamp"].replace("T00:00:00Z", "") views_by_repo[repo_name][date] = view["count"] for clone in clones_data.get("clones", []): date = clone["timestamp"].replace("T00:00:00Z", "") clones_by_repo[repo_name][date] = clone["count"] except requests.exceptions.HTTPError as e: panel_text += f"[red]Error retrieving traffic data: {e}[/red]\n" try: releases = get_repo_releases(GITHUB_USER, repo_name) if releases: panel_text += f"\n[bold]Releases:[/bold] {len(releases)}\n" for rel in releases: rel_name = rel.get("name") or rel.get("tag_name") assets = rel.get("assets", []) if assets: for asset in assets: download_count = asset["download_count"] panel_text += f" * {rel_name} — Downloads: {download_count}\n" downloads_by_repo[repo_name] += download_count else: panel_text += f" * {rel_name} (no assets)\n" else: panel_text += "\n[italic]No releases or releases are hidden (404).[/italic]\n" except requests.exceptions.HTTPError as e: panel_text += f"[red]Could not retrieve releases: {e}[/red]\n" full_report += panel_text + "\n" # Insert a newline before printing the panel so the progress bar output is separated. console.print("") console.print(format_repo_panel(repo_name, panel_text)) save_report_to_file(full_report) console.rule("[bold blue]Report Complete[/bold blue]") # --- Prepare Data for HTML Dashboard --- # Process Views Data all_view_dates = set() for repo_data in views_by_repo.values(): all_view_dates.update(repo_data.keys()) all_view_dates = sorted(all_view_dates) views_chart_data = {} for repo, data in views_by_repo.items(): views_chart_data[repo] = [data.get(date, 0) for date in all_view_dates] overall_views = [0] * len(all_view_dates) for repo_data in views_chart_data.values(): for i, count in enumerate(repo_data): overall_views[i] += count # Process Clones Data all_clone_dates = set() for repo_data in clones_by_repo.values(): all_clone_dates.update(repo_data.keys()) all_clone_dates = sorted(all_clone_dates) clones_chart_data = {} for repo, data in clones_by_repo.items(): clones_chart_data[repo] = [data.get(date, 0) for date in all_clone_dates] overall_clones = [0] * len(all_clone_dates) for repo_data in clones_chart_data.values(): for i, count in enumerate(repo_data): overall_clones[i] += count # --- Generate HTML Dashboard --- html_template = f"""<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>GitHub Traffic Dashboard</title> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> body {{ font-family: 'Roboto', sans-serif; background: linear-gradient(135deg, #ece9e6, #ffffff); margin: 0; padding: 20px; color: #444; }} .container {{ max-width: 1200px; margin: auto; background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); animation: fadeIn 1s ease-in-out; }} header {{ text-align: center; margin-bottom: 40px; }} h1 {{ font-size: 2.5em; margin-bottom: 10px; color: #222; }} h2 {{ font-size: 1.8em; color: #333; margin-bottom: 20px; }} .chart-box {{ position: relative; margin: 40px auto; height: 400px; width: 100%; max-width: 1000px; }} .section {{ margin-bottom: 60px; }} @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(10px); }} to {{ opacity: 1; transform: translateY(0); }} }} </style> </head> <body> <div class="container"> <header> <h1>GitHub Traffic Dashboard</h1> <p style="font-size: 1.2em; color: #666;">User: {GITHUB_USER}</p> </header> <div class="section"> <h2>Daily Views per Repository</h2> <div class="chart-box"> <canvas id="viewsCombinedChart"></canvas> </div> <h2 style="text-align:center; margin-top: 40px;">Overall Daily Views</h2> <div class="chart-box"> <canvas id="viewsOverallChart"></canvas> </div> </div> <div class="section"> <h2>Daily Clones per Repository</h2> <div class="chart-box"> <canvas id="clonesCombinedChart"></canvas> </div> <h2 style="text-align:center; margin-top: 40px;">Overall Daily Clones</h2> <div class="chart-box"> <canvas id="clonesOverallChart"></canvas> </div> </div> </div> <script> // Colors for datasets const colors = [ "rgba(255, 99, 132, 1)", "rgba(54, 162, 235, 1)", "rgba(255, 206, 86, 1)", "rgba(75, 192, 192, 1)", "rgba(153, 102, 255, 1)", "rgba(255, 159, 64, 1)", "rgba(199, 199, 199, 1)", "rgba(83, 102, 255, 1)", "rgba(255, 99, 71, 1)", "rgba(60, 179, 113, 1)" ]; // --- Views Charts --- const viewLabels = {json.dumps(all_view_dates)}; const viewsChartData = {json.dumps(views_chart_data)}; let viewsDatasets = []; let idx = 0; for (const repo in viewsChartData) {{ viewsDatasets.push({{ label: repo, data: viewsChartData[repo], fill: false, borderColor: colors[idx % colors.length], backgroundColor: colors[idx % colors.length], tension: 0.3, pointRadius: 4 }}); idx++; }} const ctxViewsCombined = document.getElementById('viewsCombinedChart').getContext('2d'); new Chart(ctxViewsCombined, {{ type: 'line', data: {{ labels: viewLabels, datasets: viewsDatasets }}, options: {{ responsive: true, plugins: {{ title: {{ display: true, text: 'Daily Views per Repository' }}, tooltip: {{ mode: 'index', intersect: false }}, legend: {{ position: 'bottom' }} }}, scales: {{ x: {{ title: {{ display: true, text: 'Date' }} }}, y: {{ beginAtZero: true, title: {{ display: true, text: 'Views' }} }} }} }} }}); const overallViews = {json.dumps(overall_views)}; const ctxViewsOverall = document.getElementById('viewsOverallChart').getContext('2d'); new Chart(ctxViewsOverall, {{ type: 'line', data: {{ labels: viewLabels, datasets: [{{ label: 'Overall Views', data: overallViews, fill: true, borderColor: 'rgba(0, 122, 204, 1)', backgroundColor: 'rgba(0, 122, 204, 0.2)', tension: 0.4, pointRadius: 6, pointBackgroundColor: 'rgba(0, 122, 204, 1)' }}] }}, options: {{ responsive: true, plugins: {{ title: {{ display: true, text: 'Overall Daily Views' }}, tooltip: {{ callbacks: {{ label: function(context) {{ return context.parsed.y + " views"; }} }} }}, legend: {{ display: false }} }}, scales: {{ x: {{ title: {{ display: true, text: 'Date' }} }}, y: {{ beginAtZero: true, title: {{ display: true, text: 'Views' }} }} }} }} }}); // --- Clones Charts --- const cloneLabels = {json.dumps(all_clone_dates)}; const clonesChartData = {json.dumps(clones_chart_data)}; let clonesDatasets = []; idx = 0; for (const repo in clonesChartData) {{ clonesDatasets.push({{ label: repo, data: clonesChartData[repo], fill: false, borderColor: colors[idx % colors.length], backgroundColor: colors[idx % colors.length], tension: 0.3, pointRadius: 4 }}); idx++; }} const ctxClonesCombined = document.getElementById('clonesCombinedChart').getContext('2d'); new Chart(ctxClonesCombined, {{ type: 'line', data: {{ labels: cloneLabels, datasets: clonesDatasets }}, options: {{ responsive: true, plugins: {{ title: {{ display: true, text: 'Daily Clones per Repository' }}, tooltip: {{ mode: 'index', intersect: false }}, legend: {{ position: 'bottom' }} }}, scales: {{ x: {{ title: {{ display: true, text: 'Date' }} }}, y: {{ beginAtZero: true, title: {{ display: true, text: 'Clones' }} }} }} }} }}); const overallClones = {json.dumps(overall_clones)}; const ctxClonesOverall = document.getElementById('clonesOverallChart').getContext('2d'); new Chart(ctxClonesOverall, {{ type: 'line', data: {{ labels: cloneLabels, datasets: [{{ label: 'Overall Clones', data: overallClones, fill: true, borderColor: 'rgba(0, 122, 204, 1)', backgroundColor: 'rgba(0, 122, 204, 0.2)', tension: 0.4, pointRadius: 6, pointBackgroundColor: 'rgba(0, 122, 204, 1)' }}] }}, options: {{ responsive: true, plugins: {{ title: {{ display: true, text: 'Overall Daily Clones' }}, tooltip: {{ callbacks: {{ label: function(context) {{ return context.parsed.y + " clones"; }} }} }}, legend: {{ display: false }} }}, scales: {{ x: {{ title: {{ display: true, text: 'Date' }} }}, y: {{ beginAtZero: true, title: {{ display: true, text: 'Clones' }} }} }} }} }}); </script> </body> </html> """ try: with open("dashboard.html", "w", encoding="utf-8") as html_file: html_file.write(html_template) print("Dashboard generated as 'dashboard.html'.") except Exception as e: print(f"Error generating dashboard: {e}") if __name__ == "__main__": try: main() except Exception as err: print(f"Unexpected error: {err}") input("Press Enter to exit...")