Skip to content

Commit 2c3ab22

Browse files
committed
Analytics dashboard: add Prvw Umami data (pageviews, referrers)
1 parent 93548fa commit 2c3ab22

File tree

8 files changed

+62
-15
lines changed

8 files changed

+62
-15
lines changed

apps/analytics-dashboard/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ UMAMI_USERNAME=admin
55
UMAMI_PASSWORD=your-password
66
UMAMI_WEBSITE_ID=your-website-uuid
77
UMAMI_BLOG_WEBSITE_ID=your-blog-website-uuid
8+
UMAMI_PRVW_WEBSITE_ID=your-prvw-website-uuid
89

910
# Paddle (payment provider)
1011
# Get from https://vendors.paddle.com > Developer Tools > API Keys

apps/analytics-dashboard/CLAUDE.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Each source gets its own module under `src/lib/server/sources/`:
4040

4141
| Module | Auth | Data |
4242
| --------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
43-
| `umami.ts` | JWT (username/password login) | Page views, visitors, referrers, countries, download events for veszelovszki.com + getcmdr.com |
43+
| `umami.ts` | JWT (username/password login) | Page views, visitors, referrers, countries, download events for veszelovszki.com, getcmdr.com, and getprvw.com |
4444
| `cloudflare.ts` | Bearer token (via `LICENSE_SERVER_ADMIN_TOKEN`) | Download counts, active users by version/arch/country — fetched from worker endpoints (`/admin/downloads`, `/admin/active-users`) |
4545
| `paddle.ts` | Bearer token, cursor pagination | Completed transactions, subscriptions by status |
4646
| `github.ts` | Optional Bearer token | Release download counts per asset; star history (daily + cumulative) for cmdr and mtp-rs via stargazers API with pagination |
@@ -59,7 +59,8 @@ Auto-deploys to Cloudflare Pages on push to `main` when files in `apps/analytics
5959

6060
- Create CF Pages project `cmdr-analytics-dashboard` in the CF dashboard (or `wrangler pages project create`)
6161
- Add `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` to GitHub repo secrets (for wrangler deploy)
62-
- Set all env vars below as CF Pages secrets (via `wrangler pages secret put` or CF dashboard)
62+
- Set all env vars below as CF Pages secrets (via `wrangler pages secret put` or CF dashboard). Remember to add
63+
`UMAMI_PRVW_WEBSITE_ID` when deploying.
6364
- Configure custom domain `analdash.getcmdr.com` in CF Pages settings
6465
- Set up Cloudflare Access policy for `analdash.getcmdr.com` in the CF dashboard
6566

@@ -74,6 +75,7 @@ All set as CF Pages secrets, never in code.
7475
| `UMAMI_PASSWORD` | Existing Umami credentials |
7576
| `UMAMI_WEBSITE_ID` | getcmdr.com website ID |
7677
| `UMAMI_BLOG_WEBSITE_ID` | veszelovszki.com website ID (env var name kept for CF secrets compatibility) |
78+
| `UMAMI_PRVW_WEBSITE_ID` | getprvw.com website ID |
7779
| `PADDLE_API_KEY_LIVE` | Live API key (not sandbox) |
7880
| `POSTHOG_API_KEY` | Personal `phx_...` key (not the public `phc_...` project key) |
7981
| `POSTHOG_PROJECT_ID` | `136072` |
@@ -93,7 +95,10 @@ aggregate numbers. A true funnel would require cross-site user identity tracking
9395

9496
- **Gold (`#ffc206`)**: getcmdr.com / vdavid/cmdr — the primary product
9597
- **Purple (`#a78bfa`)**: vdavid/mtp-rs — the library repo
96-
- **Autumn green (`#8faa3b`)**: veszelovszki.com — David's personal site These colors are used in metric dots, chart
98+
- **Autumn green (`#8faa3b`)**: veszelovszki.com — David's personal site
99+
- **Cyan (`#22d3ee`)**: getprvw.com — Prvw product site
100+
101+
These colors are used in metric dots, chart
97102
strokes, and chart fills. Keep them consistent when adding new UI.
98103

99104
**Decision**: Single page, not multi-page. **Why**: Only six sections. Scroll is simpler than navigation.

apps/analytics-dashboard/src/app.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ declare global {
77
UMAMI_PASSWORD: string
88
UMAMI_WEBSITE_ID: string
99
UMAMI_BLOG_WEBSITE_ID: string
10+
UMAMI_PRVW_WEBSITE_ID: string
1011
PADDLE_API_KEY_LIVE: string
1112
POSTHOG_API_KEY: string
1213
POSTHOG_PROJECT_ID: string

apps/analytics-dashboard/src/lib/server/fetch-all.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ async function resolveEnv(platform: App.Platform | undefined): Promise<App.Platf
4343
UMAMI_PASSWORD: env.UMAMI_PASSWORD ?? '',
4444
UMAMI_WEBSITE_ID: env.UMAMI_WEBSITE_ID ?? '',
4545
UMAMI_BLOG_WEBSITE_ID: env.UMAMI_BLOG_WEBSITE_ID ?? '',
46+
UMAMI_PRVW_WEBSITE_ID: env.UMAMI_PRVW_WEBSITE_ID ?? '',
4647
PADDLE_API_KEY_LIVE: env.PADDLE_API_KEY_LIVE ?? '',
4748
POSTHOG_API_KEY: env.POSTHOG_API_KEY ?? '',
4849
POSTHOG_PROJECT_ID: env.POSTHOG_PROJECT_ID ?? '',
@@ -71,6 +72,7 @@ export async function fetchDashboardData(
7172
UMAMI_PASSWORD: env.UMAMI_PASSWORD,
7273
UMAMI_WEBSITE_ID: env.UMAMI_WEBSITE_ID,
7374
UMAMI_BLOG_WEBSITE_ID: env.UMAMI_BLOG_WEBSITE_ID,
75+
UMAMI_PRVW_WEBSITE_ID: env.UMAMI_PRVW_WEBSITE_ID,
7476
},
7577
range,
7678
),

apps/analytics-dashboard/src/lib/server/sources/umami.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const mockEnv = {
88
UMAMI_PASSWORD: 'testpass',
99
UMAMI_WEBSITE_ID: '5ea041ae-b99d-4c31-b031-89c4a0005456',
1010
UMAMI_BLOG_WEBSITE_ID: '3ee5c901-70bf-4dc4-bd79-bca403db6aca',
11+
UMAMI_PRVW_WEBSITE_ID: 'a963eeab-7c38-49cb-a968-83a3c82b31d1',
1112
}
1213

1314
const sampleStats = {
@@ -62,7 +63,7 @@ describe('fetchUmamiData', () => {
6263
json: async () => ({ token: 'test-jwt-token' }),
6364
})
6465

65-
// 6 parallel requests: personalSite stats, website stats, referrers, pages, countries, events
66+
// 9 parallel requests: personalSite stats, website stats, prvw stats, referrers, pages, countries, events, prvwReferrers, prvwPages
6667
const rawStats = {
6768
pageviews: 1200,
6869
visitors: 450,
@@ -71,10 +72,10 @@ describe('fetchUmamiData', () => {
7172
totaltime: 86400,
7273
comparison: { pageviews: 1000, visitors: 400, visits: 550, bounces: 180, totaltime: 72000 },
7374
}
74-
for (let i = 0; i < 6; i++) {
75+
for (let i = 0; i < 9; i++) {
7576
fetchMock.mockResolvedValueOnce({
7677
ok: true,
77-
json: async () => (i < 2 ? rawStats : sampleMetrics),
78+
json: async () => (i < 3 ? rawStats : sampleMetrics),
7879
})
7980
}
8081

@@ -86,7 +87,9 @@ describe('fetchUmamiData', () => {
8687

8788
expect(result.data.personalSite.pageviews.value).toBe(1200)
8889
expect(result.data.website.visitors.value).toBe(450)
90+
expect(result.data.prvw.pageviews.value).toBe(1200)
8991
expect(result.data.websitePages).toHaveLength(3)
92+
expect(result.data.prvwReferrers).toHaveLength(3)
9093

9194
// Verify auth was called first
9295
expect(fetchMock.mock.calls[0][0]).toBe('https://umami.example.com/api/auth/login')
@@ -112,7 +115,7 @@ describe('fetchUmamiData', () => {
112115
fetchMock.mockResolvedValueOnce({ ok: false, status: 500 })
113116
// The other parallel requests also need to resolve for Promise.all to work,
114117
// but the first rejection will be caught
115-
for (let i = 0; i < 5; i++) {
118+
for (let i = 0; i < 8; i++) {
116119
fetchMock.mockResolvedValueOnce({ ok: true, json: async () => sampleStats })
117120
}
118121

apps/analytics-dashboard/src/lib/server/sources/umami.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface UmamiMetricItem {
1818
export interface UmamiData {
1919
personalSite: UmamiSiteStats
2020
website: UmamiSiteStats
21+
/** getprvw.com stats */
22+
prvw: UmamiSiteStats
2123
/** Top referrers for getcmdr.com */
2224
websiteReferrers: UmamiMetricItem[]
2325
/** Top pages for getcmdr.com */
@@ -26,6 +28,10 @@ export interface UmamiData {
2628
websiteCountries: UmamiMetricItem[]
2729
/** Download button click events */
2830
downloadEvents: UmamiMetricItem[]
31+
/** Top referrers for getprvw.com */
32+
prvwReferrers: UmamiMetricItem[]
33+
/** Top pages for getprvw.com */
34+
prvwPages: UmamiMetricItem[]
2935
}
3036

3137
interface UmamiEnv {
@@ -34,6 +40,7 @@ interface UmamiEnv {
3440
UMAMI_PASSWORD: string
3541
UMAMI_WEBSITE_ID: string
3642
UMAMI_BLOG_WEBSITE_ID: string
43+
UMAMI_PRVW_WEBSITE_ID: string
3744
}
3845

3946
/** Authenticates with Umami and returns a JWT token. */
@@ -127,18 +134,21 @@ export async function fetchUmamiData(env: UmamiEnv, range: TimeRange): Promise<S
127134
const token = await authenticate(env.UMAMI_API_URL, env.UMAMI_USERNAME, env.UMAMI_PASSWORD)
128135
const { startAt, endAt } = toTimeWindow(range)
129136

130-
const [personalSite, website, websiteReferrers, websitePages, websiteCountries, downloadEvents] = await Promise.all(
137+
const [personalSite, website, prvw, websiteReferrers, websitePages, websiteCountries, downloadEvents, prvwReferrers, prvwPages] = await Promise.all(
131138
[
132139
fetchStats(env.UMAMI_API_URL, token, env.UMAMI_BLOG_WEBSITE_ID, startAt, endAt),
133140
fetchStats(env.UMAMI_API_URL, token, env.UMAMI_WEBSITE_ID, startAt, endAt),
141+
fetchStats(env.UMAMI_API_URL, token, env.UMAMI_PRVW_WEBSITE_ID, startAt, endAt),
134142
fetchMetrics(env.UMAMI_API_URL, token, env.UMAMI_WEBSITE_ID, startAt, endAt, 'referrer'),
135143
fetchMetrics(env.UMAMI_API_URL, token, env.UMAMI_WEBSITE_ID, startAt, endAt, 'path'),
136144
fetchMetrics(env.UMAMI_API_URL, token, env.UMAMI_WEBSITE_ID, startAt, endAt, 'country'),
137145
fetchMetrics(env.UMAMI_API_URL, token, env.UMAMI_WEBSITE_ID, startAt, endAt, 'event'),
146+
fetchMetrics(env.UMAMI_API_URL, token, env.UMAMI_PRVW_WEBSITE_ID, startAt, endAt, 'referrer'),
147+
fetchMetrics(env.UMAMI_API_URL, token, env.UMAMI_PRVW_WEBSITE_ID, startAt, endAt, 'path'),
138148
],
139149
)
140150

141-
const data: UmamiData = { personalSite, website, websiteReferrers, websitePages, websiteCountries, downloadEvents }
151+
const data: UmamiData = { personalSite, website, prvw, websiteReferrers, websitePages, websiteCountries, downloadEvents, prvwReferrers, prvwPages }
142152
await cacheSet('umami-v2', range, data)
143153
return { ok: true, data }
144154
} catch (e) {

apps/analytics-dashboard/src/routes/+page.svelte

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
const COLOR_GOLD = '#ffc206'
8686
const COLOR_PURPLE = '#a78bfa'
8787
const COLOR_GREEN = '#8faa3b' // autumn-y green for veszelovszki.com
88+
const COLOR_CYAN = '#22d3ee' // cyan for getprvw.com
8889
8990
/** Time range in seconds for the selected range. Used as default zoom for star charts. */
9091
const rangeSeconds: Record<string, number> = { '24h': 86400, '7d': 7 * 86400, '30d': 30 * 86400 }
@@ -207,23 +208,31 @@
207208
{@render errorState(data.umami.error)}
208209
{:else}
209210
{@const umami = data.umami.data}
210-
{@const totalPageviews = umami.personalSite.pageviews.value + umami.website.pageviews.value}
211-
{@const prevPageviews = umami.personalSite.pageviews.prev + umami.website.pageviews.prev}
211+
{@const totalPageviews = umami.personalSite.pageviews.value + umami.website.pageviews.value + umami.prvw.pageviews.value}
212+
{@const prevPageviews = umami.personalSite.pageviews.prev + umami.website.pageviews.prev + umami.prvw.pageviews.prev}
212213
{@const delta = formatDelta(totalPageviews, prevPageviews)}
213214

214215
{@render metricRow([
215216
{ label: 'Total page views', value: formatNumber(totalPageviews), delta },
216217
{ label: 'veszelovszki.com views', value: formatNumber(umami.personalSite.pageviews.value), color: COLOR_GREEN },
217218
{ label: 'getcmdr.com views', value: formatNumber(umami.website.pageviews.value), color: COLOR_GOLD },
219+
{ label: 'getprvw.com views', value: formatNumber(umami.prvw.pageviews.value), color: COLOR_CYAN },
218220
])}
219221

220222
{#if umami.websiteReferrers.length > 0}
221223
<div class="mt-4">
222-
<h3 class="mb-2 text-sm font-medium text-text-secondary">Top referrers</h3>
224+
<h3 class="mb-2 text-sm font-medium text-text-secondary">Top referrers (getcmdr.com)</h3>
223225
{@render metricTable(umami.websiteReferrers.slice(0, 10), 'Source', 'Views')}
224226
</div>
225227
{/if}
226228

229+
{#if umami.prvwReferrers.length > 0}
230+
<div class="mt-4">
231+
<h3 class="mb-2 text-sm font-medium text-text-secondary">Top referrers (getprvw.com)</h3>
232+
{@render metricTable(umami.prvwReferrers.slice(0, 10), 'Source', 'Views')}
233+
</div>
234+
{/if}
235+
227236
{#if data.githubStars.ok}
228237
{@const stars = data.githubStars.data}
229238
{@const repoColors: Record<string, string> = { 'vdavid/cmdr': COLOR_GOLD, 'vdavid/mtp-rs': COLOR_PURPLE }}
@@ -256,6 +265,7 @@
256265
{@render externalLinks([
257266
{ label: 'View veszelovszki.com in Umami', href: 'https://anal.veszelovszki.com' },
258267
{ label: 'View getcmdr.com in Umami', href: 'https://anal.veszelovszki.com' },
268+
{ label: 'View getprvw.com in Umami', href: 'https://anal.veszelovszki.com' },
259269
{ label: 'cmdr on GitHub', href: 'https://github.com/vdavid/cmdr' },
260270
{ label: 'mtp-rs on GitHub', href: 'https://github.com/vdavid/mtp-rs' },
261271
])}

apps/analytics-dashboard/src/routes/api/report/+server.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,21 +74,27 @@ function formatReport(data: DashboardData): string {
7474
line(`Couldn't load: ${data.umami.error}`)
7575
} else {
7676
const u = data.umami.data
77-
const totalPv = u.personalSite.pageviews.value + u.website.pageviews.value
78-
const prevPv = u.personalSite.pageviews.prev + u.website.pageviews.prev
77+
const totalPv = u.personalSite.pageviews.value + u.website.pageviews.value + u.prvw.pageviews.value
78+
const prevPv = u.personalSite.pageviews.prev + u.website.pageviews.prev + u.prvw.pageviews.prev
7979
line(`- Total page views: ${num(totalPv)}${delta(totalPv, prevPv)}`)
8080
line(
8181
`- veszelovszki.com views: ${num(u.personalSite.pageviews.value)}${delta(u.personalSite.pageviews.value, u.personalSite.pageviews.prev)}`,
8282
)
8383
line(
8484
`- getcmdr.com views: ${num(u.website.pageviews.value)}${delta(u.website.pageviews.value, u.website.pageviews.prev)}`,
8585
)
86+
line(
87+
`- getprvw.com views: ${num(u.prvw.pageviews.value)}${delta(u.prvw.pageviews.value, u.prvw.pageviews.prev)}`,
88+
)
8689
line(
8790
`- veszelovszki.com visitors: ${num(u.personalSite.visitors.value)}${delta(u.personalSite.visitors.value, u.personalSite.visitors.prev)}`,
8891
)
8992
line(
9093
`- getcmdr.com visitors: ${num(u.website.visitors.value)}${delta(u.website.visitors.value, u.website.visitors.prev)}`,
9194
)
95+
line(
96+
`- getprvw.com visitors: ${num(u.prvw.visitors.value)}${delta(u.prvw.visitors.value, u.prvw.visitors.prev)}`,
97+
)
9298

9399
if (data.githubStars.ok) {
94100
const s = data.githubStars.data
@@ -107,12 +113,21 @@ function formatReport(data: DashboardData): string {
107113

108114
if (u.websiteReferrers.length > 0) {
109115
blank()
110-
line('Top referrers:')
116+
line('Top referrers (getcmdr.com):')
111117
const totalRef = u.websiteReferrers.reduce((s, r) => s + r.y, 0)
112118
for (const ref of u.websiteReferrers.slice(0, 15)) {
113119
line(` ${ref.x || '(direct)'}: ${num(ref.y)} (${pct(ref.y, totalRef)})`)
114120
}
115121
}
122+
123+
if (u.prvwReferrers.length > 0) {
124+
blank()
125+
line('Top referrers (getprvw.com):')
126+
const totalRef = u.prvwReferrers.reduce((s, r) => s + r.y, 0)
127+
for (const ref of u.prvwReferrers.slice(0, 15)) {
128+
line(` ${ref.x || '(direct)'}: ${num(ref.y)} (${pct(ref.y, totalRef)})`)
129+
}
130+
}
116131
}
117132
blank()
118133

0 commit comments

Comments
 (0)