The read half of the Medblocks analytics stack: an Express API over Postgres plus a
React/Vite dashboard that visualizes the funnels. It reads the data and SQL functions produced
by analytics_script_2.
Big picture: see
ANALYTICS_MASTER_FLOW.mdfor how this repo and the scraper fit together.
This repo only reads from Postgres; the scraper only writes. Postgres is the contract.
Postgres ──► server/index.js (Express API) ──► src/components/*Tab.tsx (React + Recharts)
(funnels) ~16 /api/* endpoints one tab per channel
- API:
server/index.js, a single Express 5 server. The channel endpoints build on the inlineATTRIBUTED_SIGNUPS_CTES(signup attribution computed in SQL);/api/brevostill uses the scraper'sf_brevo_funnel; the raw and exploratory endpoints queryumami_*tables directly. - UI:
src/— React 18 + Vite. One*Tab.tsxper channel, all driven by the shareduseDateRangeanduseFetchDatahooks.
| Endpoint | Purpose |
|---|---|
/api/totals, /api/total-users, /api/user-growth |
Top-level KPIs (LinkedIn / YouTube / Google / Google Ads / Other) and the all-time growth chart |
/api/google |
Organic Google search landing-page funnel |
/api/google-ads |
Paid Google funnel (utm_source=google, cpc / demand_gen), kept separate from organic |
/api/source-growth?source= |
Per-channel 30-day growth (daily bars + all-time cumulative), for the channel tabs |
/api/search-queries, /api/keywords |
Google Search Console queries and keyword editor |
/api/brevo |
Email campaign funnel (f_brevo_funnel) |
/api/linkedin-raw, /api/youtube-raw, /api/youtube-rankings |
LinkedIn / YouTube funnels and rankings |
/api/umami-raw |
Raw traffic bucketed by utm_source / referrer_domain |
/api/other, /api/contact-us |
Other-source drill-down and contact-form data |
OverviewTab, GoogleTab, GoogleAdsTab, BrevoTab, LinkedInRawTab, YouTubeRawTab,
YTSearchRankingTab, SearchQueriesTab, RawUmamiTab, ContactUsTab, OtherTab.
Signups are attributed with a first-touch model. A signup's source is resolved in priority order, then mapped to a channel:
- First-touch cookie carried on the SignUp event (
first_utm_*/first_referrer_url, written by the website from itsmb_utm_datacookie). session_any_source: the earliest source-bearing event of the converting session (any event type), skipping sourceless events and auth-callback / internal referrers. This recovers the real source when it sits on a later pageview or a custom event, not the first pageview.- First pageview of the converting session (last-resort fallback).
A signup is "Direct" only when none of the above carries a usable source.
Channel mapping (SOURCE_CASE_SQL in server/index.js). The resolved utm_source /
utm_medium / referrer_domain map to a channel:
- LinkedIn / YouTube:
utm_source,utm_medium, or the platform's referrer domains. - Google Ads:
utm_source=google(paid: cpc / demand_gen), checked before organic so a paid click stays paid even when its referrer isgoogle.com. - Google: organic search referrers (
google.comand country variants,search.google.com, Android quick-search), with noutm_source=google. - Other: everything else. The
/api/otherdrill-down sub-labels it (Direct, Brevo / Email, Google OAuth callback, AI chat, Other search engine, No entry pageview, and so on).
Channel tabs reconcile with the Overview. Each channel tab classifies sessions with the same
resolution (classified_sessions) and FULL OUTER joins redirects to conversions, so a tab's
conversion total equals the Overview's count for that channel. Page-less signups fold into a
single (no entry page) row.
The
/api/brevoemail funnel still matches the landing URL to the campaignfull_linkbyte-for-byte (in the scraper'sf_brevo_funnel), so it can drop clicks when query params differ. The raw/api/umami-rawendpoint never loses events. SeeANALYTICS_MASTER_FLOW.md.
npm install
cp .env.example .env # point at the SAME Postgres the scraper writes to
npm start # runs Vite UI + Express API concurrently
DB_*must point at theanalyticswarehouse (db=analytics, port5432) — the same database the scraper writes to. This repo never connects to the upstreamumamisource DB; that's the scraper's job. SeeANALYTICS_MASTER_FLOW.md.
| Var | Purpose |
|---|---|
DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT |
analytics warehouse connection (required) |
PORT |
API port (default 4000) |
NODE_ENV |
dev / production (serves built UI in production) |
| Command | Purpose |
|---|---|
npm start |
Vite dev server + Express API together (concurrently) |
npm run dev |
Vite UI only |
npm run server |
Express API only |
npm run build |
Type-check + production build |
npm run lint |
ESLint |