Inspect site analytics
import { Aside, Steps, Tabs, TabItem } from ‘@astrojs/starlight/components’;
This guide is the practical companion to Concepts → Analytics. Use it when you want to:
- Pull a fresh analytics snapshot from your CI / dashboard / Slack bot.
- Embed the first-party tracker in a non-template site.
- Wire the dashboards into a custom UI.
Prerequisites
Section titled “Prerequisites”-
A Melbora Website with
status: "active". -
A
vc_pat_*Personal Access Token. Either an admin token (sees every site) or a client token (sees only the websites it owns). -
The SDK installed:
```sh npm install @vantageconnections/sdk ``` ```sh pnpm add @vantageconnections/sdk ```
Read SEO / GEO analytics via SDK
Section titled “Read SEO / GEO analytics via SDK”vc.analytics.getSeo(websiteId, range) returns the composed SEO
dashboard payload — health-scan snapshot, GEO citation rate, Search
Console state, citation checklist, competitor diffs, AI quota usage.
import { VantageClient } from "@vantageconnections/sdk";
const vc = new VantageClient({ token: process.env.VANTAGE_TOKEN! });
// Default range is "30d". Valid: "7d" | "30d" | "90d" | "all".const seo = await vc.analytics.getSeo("wb_01J7XB4MQH", "30d");
console.log("Tier:", seo.tier); // "essentials" | "growth" | "premium" | "premium_plus" | nullconsole.log("Health score:", seo.healthScan?.score ?? null); // null = no scan yetconsole.log( "GEO citation rate:", seo.geo.citationRate, // 0..1, or null when totalProbes === 0 `(${seo.geo.citedProbes}/${seo.geo.totalProbes})`);
if (seo.searchConsole.connected) { console.log("GSC property:", seo.searchConsole.property); console.log("GSC last fetched:", seo.searchConsole.fetchedAt);} else { console.log("Search Console not connected.");}
// AI quota — null when the site has no SEO tier set.if (seo.aiUsage) { console.log( "Blog drafts this month:", `${seo.aiUsage.blogDraft.used} / ${seo.aiUsage.blogDraft.limit}` );}Field guide (most-asked)
Section titled “Field guide (most-asked)”| Field | What it is | When it’s null |
|---|---|---|
tier | SEO tier — essentials / growth / premium / premium_plus. | Null when SEO isn’t enabled on the site. |
healthScan | Most recent scan result: score, issues, scannedAt. | Null when no scan has been run. |
geo.citationRate | Fraction of in-range probes that cited the primary domain. | Null when totalProbes === 0. |
searchConsole.metrics | Most recent GSC pull. | Null when GSC isn’t connected. |
competitors | URLs tracked + diff activity in the range. | Null on tiers below Premium, or when no URLs configured. |
aiUsage | Monthly counters vs tier quotas. | Null when SEO isn’t enabled. |
Read traffic analytics via SDK
Section titled “Read traffic analytics via SDK”vc.analytics.getTraffic(websiteId, range, interval) returns the
traffic dashboard — KPIs, timeline, top breakdowns, bot section.
import { VantageClient } from "@vantageconnections/sdk";
const vc = new VantageClient({ token: process.env.VANTAGE_TOKEN! });
// interval "auto" picks based on range:// 7d → hourly// 30d → daily// 90d, all → weeklyconst traffic = await vc.analytics.getTraffic("wb_01J7XB4MQH", "30d", "auto");
if (traffic.empty) { // Tracker hasn't been installed yet, or no events recorded in range. console.log("No traffic data yet — embed the tracker first.");} else { console.log("Pageviews:", traffic.pageviews); console.log("Sessions:", traffic.sessions); console.log("Avg dwell (ms):", traffic.avgDwellMs); console.log("Bounce rate:", traffic.bounceRate); // 0..1
console.log("Top 5 pages:"); for (const p of traffic.topPages.slice(0, 5)) { console.log(` ${p.path} ${p.pageviews} views ${p.avgDwellMs}ms avg`); }
console.log("Top referrers:"); for (const r of traffic.topReferrers.slice(0, 5)) { console.log(` ${r.referrer} ${r.sessions} sessions`); }
console.log("Bot pageviews:", traffic.bots.pageviews);}Range / interval combinations
Section titled “Range / interval combinations”| Range | Auto-resolved interval | Buckets returned |
|---|---|---|
7d | hourly | up to 168 |
30d | daily | up to 30 |
90d | weekly | up to ~13 |
all | weekly | bounded by the 90-day event TTL |
You can override interval explicitly (e.g. daily over a 7d
window if you want a coarser view). The server returns the
interval it actually applied in the response so the UI can format
the timeline axis correctly.
Embed the tracker in a site
Section titled “Embed the tracker in a site”The Melbora website-template ships with the tracker pre-installed
— if you’re porting an existing site (see Port an existing repo)
or running the tracker on a non-Vantage-hosted page, install it
manually.
The minimum tracker
Section titled “The minimum tracker”The tracker POSTs AnalyticsIngestPayload events via navigator.sendBeacon()
to https://<your-api-host>/public/v1/c. The payload shape is
documented in Concepts → Analytics;
the only required fields are websiteId, sessionId, path,
referrer, dwell, and event.
<!-- Drop into the <head> of every page you want to track. --><script>(function () { var API = "https://qpcjiogcm2.execute-api.us-east-1.amazonaws.com"; var WEBSITE_ID = "wb_01J7XB4MQH"; // your websiteId var INGEST = API + "/public/v1/c";
// Daily-rotated session id. Stored in sessionStorage so the same // tab keeps one id across navigations; rotates on the next UTC day. function getSessionId() { var key = "vc_sid"; var today = new Date().toISOString().slice(0, 10); var raw = sessionStorage.getItem(key); if (raw) { try { var p = JSON.parse(raw); if (p.day === today) return p.id; } catch (e) {} } var id = crypto.randomUUID(); sessionStorage.setItem(key, JSON.stringify({ id: id, day: today })); return id; }
// Best-effort 2-letter country from browser locale. function getCountry() { try { var r = new Intl.Locale(navigator.language).region; if (r && /^[A-Z]{2}$/.test(r)) return r; } catch (e) {} return undefined; }
function send(event, dwell) { var body = JSON.stringify({ websiteId: WEBSITE_ID, sessionId: getSessionId(), path: location.pathname, referrer: document.referrer || null, dwell: dwell || 0, event: event, country: getCountry(), }); // sendBeacon survives page-unload; falls back to fetch for older browsers. if (navigator.sendBeacon) { navigator.sendBeacon(INGEST, body); } else { fetch(INGEST, { method: "POST", body: body, keepalive: true }); } }
var pageviewAt = Date.now(); send("pageview", 0);
// Fire "pageend" with dwell on tab-hide / unload. pagehide is the // bfcache-safe choice; visibilitychange catches tab switches. function flush() { send("pageend", Date.now() - pageviewAt); } addEventListener("pagehide", flush); document.addEventListener("visibilitychange", function () { if (document.visibilityState === "hidden") flush(); });})();</script>Replace WEBSITE_ID with the wb_* id of the Website you want to
attribute traffic to. The API host is the production Melbora API; for
a custom deployment, point at your own.
Verifying the tracker works
Section titled “Verifying the tracker works”After embedding, open the site in a browser, navigate around, then:
const traffic = await vc.analytics.getTraffic("wb_01J7XB4MQH", "7d");console.log(traffic.empty, traffic.pageviews);// → false, <count> if the tracker is firingIf empty: true persists after a real pageview, check the browser’s
network tab for /public/v1/c. A net::ERR_BLOCKED_BY_CLIENT means
an ad blocker is dropping it — the tracker is working, the blocker
is just doing its job.
Call the REST endpoints directly
Section titled “Call the REST endpoints directly”If you’re not on Node, the SDK methods map 1:1 to:
# SEO / GEO snapshotcurl -H "Authorization: Bearer vc_pat_..." \ "https://qpcjiogcm2.execute-api.us-east-1.amazonaws.com/v1/websites/wb_01J7XB4MQH/analytics/seo?range=30d"
# Traffic dashboardcurl -H "Authorization: Bearer vc_pat_..." \ "https://qpcjiogcm2.execute-api.us-east-1.amazonaws.com/v1/websites/wb_01J7XB4MQH/analytics?range=30d&interval=auto"Both endpoints honor the same auth as the SDK (admin OR the website’s owning client). 401 means the token is missing/invalid; 404 means the website doesn’t exist or isn’t accessible to your token.
Common gotchas
Section titled “Common gotchas”| Symptom | Likely cause |
|---|---|
getTraffic returns empty: true despite real pageviews | Tracker not embedded on the page, or WEBSITE_ID mismatches the website you’re querying. Check the network tab for /public/v1/c 200s. |
seo.healthScan is null | No scan has been run yet. Trigger with vantage seo scan <websiteId>. |
seo.geo.citationRate is null | No GEO probes in the window. Run a probe with vantage seo geo-probe <websiteId> --prompt "...". |
seo.searchConsole.metrics is null | GSC OAuth not granted, or the metrics cron hasn’t run since the grant. Connect GSC in the portal, wait one cron cycle. |
seo.aiUsage is null | SEO isn’t enabled. Run vantage seo set-tier <websiteId> --tier growth. |
Country breakdown is mostly ZZ | Tracker isn’t sending country, and the request isn’t flowing through CloudFront/Vercel edge — fall through to the unknown sentinel. Add country to the tracker payload. |
See also
Section titled “See also”- Concepts → Analytics — the data model, retention, and why the tabs are split.
- AnalyticsApi reference — full SDK method signatures.
- REST API — auto-generated OpenAPI reference.