Dashboard · Analytics

Analytics Dashboard

One page for Google Analytics and Search Console across five sites

GA4 Data APISearch Console APIGitHub ActionsGitHub PagesCloudflare WorkersVanilla JS
GA4Search Console

01 · What I Built

I run five sites: this portfolio, streamerOS, FitRank, DefendIt and SpotAI. Each has a different audience and a different goal, and I could not answer the plain questions about any of them. Who visited, and from where? Did anyone come from ChatGPT or LinkedIn? Which button turned into a lead? What do people type into Google before they land on us?

Google Analytics and Search Console can answer all of it, but in two products, one site at a time. So I built a single page that reads both for all five sites, shows the answer in plain words, and works on a phone. Along with it came the tracking that makes the numbers worth reading: tagged events, first-touch attribution on every lead, and a UTM link builder.

Who it is for: anyone who runs several small sites and wants one honest view of them, without a paid analytics tool and without a server to look after.

02 · What It Does

Tracking
One GA4 property with five streams. Eight key events (WhatsApp, email, phone, app download, template download, lead, calculator, agreement) and seven custom fields, created through the Admin API.
First-touch attribution
Every lead carries where that person first arrived from: source, medium and campaign. Old ?ref= tags become real campaigns.
Sources in plain words
Visits grouped into search engines, social, AI assistants (ChatGPT, Perplexity, Claude, Gemini), communities, email and direct, with the exact pages that linked.
Search Console
Queries, pages, countries and devices with clicks, impressions, CTR and position, plus a Google-style chart and compare-to-previous-period.
Date ranges
Seven days to sixteen months, each loaded only when you open it. Tables sort and filter; on a phone they become cards.
Live now
Visitors in the last 30 minutes, by site, page and country.

03 · The Stack

Data sources
GA4 Data and Admin APIs, and the Search Console API, through a read-only Google service account.
Build
A GitHub Actions workflow runs a dependency-free Node script every two hours.
Hosting
GitHub Pages on analytics.streamerosai.com. Only aggregate numbers are published; the Google key stays in repository secrets.
Live layer
One Cloudflare Worker (free plan) that holds the key as a secret, answers only the dashboard's own origin, and caches for 45 seconds to 15 minutes.
Front end
One HTML file, vanilla JavaScript, no framework and no build step. Charts are SVG drawn at the real width, so text stays readable on phones.
Cost
$0 a month at this volume. Everything runs on free tiers.

04 · How Data Flows

  1. 1Tag on every page

    A plain Google tag in the head of all five sites sends page views and tagged clicks and leads to one GA4 property.

  2. 2GitHub Action

    Every two hours a Node script signs in with a read-only service account and pulls GA4 and Search Console for six date ranges.

  3. 3Static JSON files

    Each range becomes its own small file (ga-28d.json, gsc-28d.json, live.json), published to GitHub Pages with the page.

  4. 4Page opens

    The browser draws the published copy straight away and loads only the date range you picked.

  5. 5Live Worker

    A free Cloudflare Worker answers the same files fresh from Google, cached for 45 seconds to 15 minutes.

  6. 6One swap

    Live answers are batched and swapped in once, and only when something changed. If the Worker is down, the published copy stays.

05 · Deep Technical Breakdown

First-touch attribution on every event

GA4 tells you how a session arrived, but a lead often comes on the third visit, from a different source than the first. The tag stores the first source, medium and campaign in the browser once, then attaches them to every key event. A shared link tagged ?ref=whatsapp is read as a campaign, so the old tagging habit still works.

var ft = JSON.parse(localStorage.getItem('ga_ft') || 'null');
if (!ft) {
  ft = { s: utm_source || ref || referrerHost || 'direct',
         m: utm_medium || (ref ? 'share' : referrerHost ? 'referral' : 'none'),
         c: utm_campaign || (ref ? 'ref-tag' : '(not set)') };
  localStorage.setItem('ga_ft', JSON.stringify(ft));
}
window.gaEvent = function (name, p) {
  p = p || {};
  p.first_source = ft.s; p.first_medium = ft.m; p.first_campaign = ft.c;
  gtag('event', name, p);
};

Two copies of the data, one swap

The Action publishes the data every two hours, so the page can draw something immediately from GitHub's CDN. The Worker then fetches the same files fresh from Google. Early on, each live answer rebuilt the page on its own, so the Overview redrew five times while loading. Now the answers are collected, compared with what is already on screen, and swapped in once after 600 ms, only if something changed.

function sig(j) {                       // ignore the timestamp when comparing
  return JSON.stringify(j, (k, v) => k === 'generatedAt' ? undefined : v);
}
function scheduleSwap() {
  clearTimeout(swapT);
  swapT = setTimeout(() => render(true), 600);
}

Date ranges as separate files

A sixteen-month report is much bigger than a seven-day one, so each range is its own file and a tab loads only the sources it needs. Search Console is a separate file from Google Analytics, loaded only on the tabs that use it.

Grouping sources, including AI assistants

GA4 reports a visit from ChatGPT as a referral from a domain. A small rule list names the source (ChatGPT, Perplexity, Claude, Gemini, LinkedIn, GitHub and so on) and files it under search engines, social, AI assistants, communities, email or direct, so the answer to "did anyone find us through an AI assistant" is one line.

06 · What Broke, and What I Changed

  • The page redrew itself four or five times while loading. Published data, live visitors, live Analytics and live Search Console each rebuilt it. I now batch them into one swap and skip it when nothing changed.
  • Google's tag checker could not find the tag. It was injected with JavaScript after the page loaded. A plain script tag in the page head fixed it on all five sites.
  • Old links lost their tracking. Redirect pages from earlier site moves dropped ?utm_ and ?ref=. They now pass the query string through.
  • One site had no /sitemap.xml. SpotAI serves sitemap-index.xml only, and my index-status checker assumed the other. It now follows sitemap indexes.

Honest limits

Google Analytics reports take hours to appear after a tag goes live, and Search Console runs about two days behind, so only the "online now" numbers are truly live. The dashboard is public and shows aggregate numbers only, with no login. And I tested the click events against the live pages, but I have not yet sent a real form on every site to see each lead event arrive.

See it live

The dashboard is running now. The source is public.

Want a system like this built?

I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo: RAG pipelines, agent orchestration, real-time and edge architectures. Open to senior / lead roles and contract work, remote or on-site.