diff --git a/.github/scripts/accumulate-traffic.py b/.github/scripts/accumulate-traffic.py index 13525f8..fbf5eaa 100644 --- a/.github/scripts/accumulate-traffic.py +++ b/.github/scripts/accumulate-traffic.py @@ -19,6 +19,13 @@ TRAFFIC_DIR = f"traffic-{REPO_SLUG}" HISTORY_REFERRERS = "traffic_referrers_history.json" HISTORY_PATHS = "traffic_paths_history.json" HISTORY_STARS = "traffic_stars_history.json" +HISTORY_TRENDING = "traffic_trending_history.json" + +# One-time backfill of github.com/trending history, scraped from Trendshift +# (trendshift.io/repositories/14577) which retained it before this tracker +# existed. Used only to seed the very first run; after that the accumulated +# branch history is authoritative and this is never consulted again. +TRENDING_SEED = [{"date":"2025-08-13","javascript":5},{"date":"2025-08-14","javascript":2},{"date":"2025-08-15","javascript":14},{"date":"2025-08-16","javascript":8},{"date":"2025-08-17","javascript":2},{"date":"2025-08-24","javascript":6},{"date":"2025-08-25","javascript":2,"all":10},{"date":"2025-08-26","all":1,"javascript":1},{"date":"2025-08-27","all":1,"javascript":1},{"date":"2025-08-28","all":1,"javascript":1},{"date":"2025-08-29","all":11,"javascript":1},{"date":"2025-09-16","javascript":2},{"date":"2025-09-24","javascript":6},{"date":"2025-10-04","javascript":5},{"date":"2025-10-13","javascript":1,"all":4},{"date":"2025-10-14","javascript":1,"all":6},{"date":"2025-10-15","javascript":1},{"date":"2025-10-16","javascript":15},{"date":"2025-10-26","javascript":12},{"date":"2026-01-27","javascript":1},{"date":"2026-01-28","javascript":1,"all":3},{"date":"2026-01-29","javascript":1,"all":2},{"date":"2026-01-30","javascript":1,"all":2},{"date":"2026-01-31","javascript":2},{"date":"2026-04-02","all":3},{"date":"2026-06-11","javascript":9},{"date":"2026-06-12","javascript":2},{"date":"2026-06-13","javascript":1},{"date":"2026-06-14","javascript":1},{"date":"2026-06-15","javascript":3},{"date":"2026-06-16","javascript":6},{"date":"2026-06-17","javascript":4},{"date":"2026-06-18","javascript":2},{"date":"2026-06-19","javascript":9},{"date":"2026-06-20","javascript":5},{"date":"2026-06-21","javascript":1,"all":16},{"date":"2026-06-22","javascript":3},{"date":"2026-06-23","javascript":2},{"date":"2026-07-04","all":7,"javascript":3},{"date":"2026-07-05","all":3,"javascript":2},{"date":"2026-07-06","all":1,"javascript":1}] def load_json(path): try: @@ -155,7 +162,64 @@ def accumulate_stars(publish_dir): print(f"Accumulated: {len(star_history)} star snapshots (latest={star_history[-1]['stars'] if star_history else 'n/a'})") return star_history, fetch_star_windows() -def build_dashboard(publish_dir, ref_history, path_history, star_history, star_windows): +def fetch_trending_rank(url): + """Rank of this repo on a github.com/trending page (1-based article + position), or None if not listed / fetch failed. Trending has no API, + but the page is server-rendered HTML: one

+ per entry, in rank order.""" + import re + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (traffic-dashboard)"}) + try: + with urllib.request.urlopen(req, timeout=30) as r: + html = r.read().decode("utf-8", errors="replace") + except Exception as e: + print(f"Trending fetch failed ({url}): {e}") + return None + repos = re.findall(r'

.*?href="/([^"]+)"', html, re.S) + try: + return repos.index(REPO_FULL) + 1 + except ValueError: + return None + +def accumulate_trending(publish_dir): + """One entry per day: {"date", "all": rank?, "javascript": rank?}. + Ranks move all day, so when a day already has an entry keep the BEST + (lowest) rank seen — the workflow runs several times a day.""" + today = datetime.now(timezone.utc).strftime("%Y-%m-%d") + data_dir = os.path.join(publish_dir, TRAFFIC_DIR) + + history = ( + load_json(os.path.join(data_dir, HISTORY_TRENDING)) + or fetch_from_branch(publish_dir, HISTORY_TRENDING) + or [dict(e) for e in TRENDING_SEED] # first run only; deep-copied so the constant is never mutated + ) + + ranks = {} + for lang, url in [ + ("all", "https://github.com/trending?since=daily"), + ("javascript", "https://github.com/trending/javascript?since=daily"), + ]: + rank = fetch_trending_rank(url) + if rank is not None: + ranks[lang] = rank + + if ranks: + entry = next((e for e in history if e["date"] == today), None) + if entry is None: + history.append({"date": today, **ranks}) + history.sort(key=lambda x: x["date"]) + else: + for lang, rank in ranks.items(): + if lang not in entry or rank < entry[lang]: + entry[lang] = rank + + with open(os.path.join(data_dir, HISTORY_TRENDING), "w") as f: + json.dump(history, f, separators=(",", ":")) + + print(f"Accumulated: {len(history)} trending snapshots (today={ranks or 'not trending'})") + return history + +def build_dashboard(publish_dir, ref_history, path_history, star_history, star_windows, trending_history): data_dir = os.path.join(publish_dir, TRAFFIC_DIR) views = load_json(os.path.join(data_dir, "traffic_views.json")) clones = load_json(os.path.join(data_dir, "traffic_clones.json")) @@ -171,6 +235,7 @@ def build_dashboard(publish_dir, ref_history, path_history, star_history, star_w "paths_series": path_history, "stars_series": star_history, "stars_windows": star_windows, + "trending_series": trending_history, }, separators=(",", ":")) html = DASHBOARD_TEMPLATE.replace("__DATA_PLACEHOLDER__", embedded) @@ -250,6 +315,7 @@ DASHBOARD_TEMPLATE = r"""
Daily Views

Drag to zoom — click Reset to restore

GitHub Stars (cumulative)

Bars show stars gained per day · drag to zoom

+

Page Trends — daily 14-day view count for the top pages

@@ -413,6 +479,51 @@ if (starSeries.length) { }).render(); } +// GitHub Trending rank. No official API — scraped daily from the trending +// page's article order. Y axis is reversed so #1 sits at the top; gaps mean +// the repo wasn't on that day's list. Two series: "All languages" and +// "JavaScript" (the language bucket GitHub files this repo under). +const trendSeries = (DATA.trending_series||[]).slice().sort((a,b)=>a.date.localeCompare(b.date)); +if (trendSeries.length) { + document.getElementById('trendingCard').style.display = ''; + const mkSeries = key => trendSeries.map(d=>[new Date(d.date).getTime(), d[key]!=null?d[key]:null]); + const latest = trendSeries[trendSeries.length-1]; + const best = key => { const v = trendSeries.map(d=>d[key]).filter(x=>x!=null); return v.length?Math.min(...v):null; }; + const bestAll = best('all'), bestJs = best('javascript'); + const bits = []; + if (latest.all != null) bits.push(`#${latest.all} all today`); + if (latest.javascript != null) bits.push(`#${latest.javascript} JS today`); + if (bestAll != null) bits.push(`best #${bestAll} all`); + if (bestJs != null) bits.push(`best #${bestJs} JS`); + document.getElementById('trendingReadout').innerHTML = bits.join('  ·  '); + + new ApexCharts(document.getElementById('trendingChart'), { + ...baseOpts, + chart:{...baseOpts.chart, type:'line', height:280, id:'trending', zoom:{enabled:true,type:'x'}}, + series:[ + {name:'All languages', data:mkSeries('all')}, + {name:'JavaScript', data:mkSeries('javascript')}, + ], + colors:['#6366f1','#f59e0b'], + stroke:{curve:'straight',width:2.5}, + xaxis:{type:'datetime',labels:{datetimeUTC:false}}, + yaxis:{reversed:true,min:1,forceNiceScale:true,labels:{formatter:v=>'#'+Math.round(v)}}, + dataLabels:{enabled:false}, + markers:{size:4,hover:{size:6}}, + legend:{position:'top',horizontalAlign:'left',fontSize:'12px'}, + tooltip:{shared:true,x:{format:'MMM d, yyyy'},y:{formatter:v=>v!=null?'#'+v:'not listed'}}, + }).render(); +} + +if (curStars != null && trendSeries.length) { + const latest = trendSeries[trendSeries.length-1]; + const bestRank = Math.min(...trendSeries.flatMap(d=>[d.all,d.javascript].filter(x=>x!=null))); + const cur = latest.all != null ? latest.all : latest.javascript; + const lbl = latest.all != null ? 'all langs' : 'JavaScript'; + if (cur != null) statCards.splice(statCards.length, 0, { l:'Trending Rank', v:'#'+cur, s:`${lbl} today · best #${bestRank}` }); + document.getElementById('stats').innerHTML = statCards.map(s=>`
${s.l}
${s.v}
${s.s}
${s.t?`
${s.t}
`:''}
`).join(''); +} + new ApexCharts(document.getElementById('clonesChart'), { ...baseOpts, chart:{...baseOpts.chart, type:'bar', height:240, stacked:false}, @@ -613,4 +724,5 @@ if __name__ == "__main__": print(f"Processing traffic data in {publish_dir}") ref_history, path_history = accumulate(publish_dir) star_history, star_windows = accumulate_stars(publish_dir) - build_dashboard(publish_dir, ref_history, path_history, star_history, star_windows) + trending_history = accumulate_trending(publish_dir) + build_dashboard(publish_dir, ref_history, path_history, star_history, star_windows, trending_history)