feat(admin): unify root visits and expose redirect settings #27

Merged
eleith merged 1 commits from feat/admin-link-details into main 2026-11-14 01:48:25 +00:00
33 changed files with 924 additions and 389 deletions

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
---
'@goto/web': minor
'@goto/worker': minor
'@goto/cli': minor
---
Use one `(root)` inventory row to reach Root details and remove redundant Root summary navigation. Expose root/fallback response settings and named-link settings, inheritance, schedules and tags without changing existing URLs.
Count every tracked `/` request as a valid Root visit, independent of response status. Apply that meaning to legacy Root visits during report aggregation without rewriting raw rows. Details version 3 keeps totals, charts and browser/OS breakdowns consistent; older prepared details stay unavailable until normal report refresh. Invalid clicks remain limited to non-root traffic.
+81 -50
View File
@@ -34,7 +34,7 @@ test('shared credentials protect admin pages and sign-out removes access', async
.getByRole('region', { name: 'Visits', exact: true })
.locator('.admin-stat-value');
await expect(page.getByRole('region', { name: 'Domains', exact: true })).toContainText('2');
await expect(visitsValue).toHaveText('2');
await expect(visitsValue).toHaveText('3');
await expect(page.locator('.admin-cards .admin-stat-label')).toHaveText([
'Domains',
'Links',
@@ -57,18 +57,18 @@ test('shared credentials protect admin pages and sign-out removes access', async
await expect(chartTable).toBeVisible();
await expect(chartTable).toHaveAccessibleName('Visits');
await expect(chartTable.getByRole('columnheader')).toHaveText(['Period (UTC)', 'Visits']);
await expect(chartTable.getByRole('row', { name: 'Total 2', exact: true })).toBeVisible();
await expect(chartTable.getByRole('row', { name: 'Total 3', exact: true })).toBeVisible();
await expect(chartTable).not.toContainText('(partial)');
await expect(trend.getByRole('region', { name: 'Visit data', exact: true })).toHaveAttribute(
'tabindex',
'0'
);
await expect(graph).toBeVisible();
await expect(graph).toHaveAccessibleName(/^Visits by day\. Total: 2\./);
await expect(graph).toHaveAccessibleName(/^Visits by day\. Total: 3\./);
const domainLink = page.getByRole('link', { name: 'go.example.org', exact: true });
const domainRow = page.getByRole('listitem').filter({ has: domainLink });
const domainCounts = domainRow.locator('.admin-inventory-number');
await expect(domainCounts).toHaveText(['6', '1']);
await expect(domainCounts).toHaveText(['6', '2']);
await expect(domainRow.locator('.admin-inventory-label').first()).toHaveCSS(
'clip-path',
'inset(50%)'
@@ -87,33 +87,33 @@ test('shared credentials protect admin pages and sign-out removes access', async
'aria-pressed',
'true'
);
await expect(visitsValue).toHaveText('1');
await expect(domainCounts).toHaveText(['6', '0']);
await expect(graph).toHaveAccessibleName(/^Visits by day\. Total: 1\./);
await expect(visitsValue).toHaveText('2');
await expect(domainCounts).toHaveText(['6', '1']);
await expect(graph).toHaveAccessibleName(/^Visits by day\. Total: 2\./);
await timeframe.getByRole('button', { name: '3 months', exact: true }).click();
await expect(timeframe.getByRole('button', { name: '3 months', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await expect(visitsValue).toHaveText('3');
await expect(domainCounts).toHaveText(['6', '2']);
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 3\./);
await expect(visitsValue).toHaveText('4');
await expect(domainCounts).toHaveText(['6', '3']);
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 4\./);
await page.goBack();
await expect(timeframe.getByRole('button', { name: '7 days', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await expect(visitsValue).toHaveText('1');
await expect(domainCounts).toHaveText(['6', '0']);
await expect(graph).toHaveAccessibleName(/^Visits by day\. Total: 1\./);
await expect(visitsValue).toHaveText('2');
await expect(domainCounts).toHaveText(['6', '1']);
await expect(graph).toHaveAccessibleName(/^Visits by day\. Total: 2\./);
await page.goForward();
await expect(timeframe.getByRole('button', { name: '3 months', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await expect(visitsValue).toHaveText('3');
await expect(domainCounts).toHaveText(['6', '2']);
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 3\./);
await expect(visitsValue).toHaveText('4');
await expect(domainCounts).toHaveText(['6', '3']);
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 4\./);
page.off('request', recordSelectionRequest);
expect(selectionRequests).toEqual([]);
await page.reload();
@@ -121,8 +121,8 @@ test('shared credentials protect admin pages and sign-out removes access', async
'aria-pressed',
'true'
);
await expect(visitsValue).toHaveText('3');
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 3\./);
await expect(visitsValue).toHaveText('4');
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 4\./);
expect(navigationWarnings).toEqual([]);
await domainLink.click();
await expect(page.getByRole('heading', { name: 'Domain', exact: true })).toBeVisible();
@@ -144,24 +144,28 @@ test('shared credentials protect admin pages and sign-out removes access', async
await expect(overviewLink).toHaveAttribute('href', '/admin?period=all');
await expect(page.getByRole('link', { name: 'Overview', exact: true })).toHaveCount(0);
await expect(graph).toHaveAttribute('aria-busy', 'false');
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 2\./);
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 3\./);
await expect(timeframe.getByRole('button', { name: '3 months', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await expect(visitsValue).toHaveText('2');
await expect(visitsValue).toHaveText('3');
await expect(page.locator('.admin-cards .admin-stat-label')).toHaveText([
'Links',
'Visits',
'Invalid clicks',
'Base visits'
'Invalid clicks'
]);
await expect(
page.getByRole('region', { name: 'Invalid clicks', exact: true }).locator('.admin-stat-value')
).toHaveText('1');
const linksInventory = page.getByRole('region', { name: 'Links on go.example.org', exact: true });
const domainDetails = page.getByRole('region', { name: 'Domain details', exact: true });
const baseVisitsLink = page.getByRole('link', { name: 'Base visits', exact: true });
const rootRow = linksInventory.getByRole('listitem').filter({
has: page.getByRole('link', { name: '(root)', exact: true })
});
const rootLink = rootRow.getByRole('link', { name: '(root)', exact: true });
await expect(rootRow.locator('.admin-inventory-secondary')).toHaveText('[not found]');
await expect(rootRow.locator('.admin-inventory-state')).toHaveText('Active');
const columns = ['Path', 'Destination', 'State', 'Visits'];
await expect(linksInventory.locator('.admin-inventory-heading > span')).toHaveText(columns);
const retainedLink = linksInventory
@@ -180,18 +184,18 @@ test('shared credentials protect admin pages and sign-out removes access', async
.getByRole('listbox', { name: 'Timeframe', exact: true })
.getByRole('option', { name: '30 days', exact: true })
.click();
await expect(visitsValue).toHaveText('1');
await expect(visitsValue).toHaveText('2');
await expect(retainedVisits).toHaveText('0');
await expect(graph).toHaveAccessibleName(/^Visits by day\. Total: 1\./);
await expect(graph).toHaveAccessibleName(/^Visits by day\. Total: 2\./);
expect(
await linksInventory.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true);
await page.setViewportSize({ width: 1280, height: 720 });
await timeframe.getByRole('button', { name: '3 months', exact: true }).click();
await expect(visitsValue).toHaveText('2');
await expect(visitsValue).toHaveText('3');
await expect(retainedVisits).toHaveText('1');
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 2\./);
expect(await cardSizes()).toEqual(overviewCardSizes);
await expect(graph).toHaveAccessibleName(/^Visits by week\. Total: 3\./);
expect(await cardSizes()).toEqual(overviewCardSizes.slice(0, 3));
const historicalInvalid = linksInventory
.getByRole('listitem')
.filter({ has: page.getByRole('link', { name: '/a/b', exact: true }) });
@@ -199,29 +203,22 @@ test('shared credentials protect admin pages and sign-out removes access', async
await expect(
domainDetails.getByRole('heading', { name: 'Domain details', exact: true })
).toBeVisible();
await expect(domainDetails.locator('dt')).toHaveText([
'Domain',
'Base destination',
'Base status'
]);
await expect(domainDetails.locator('dt')).toHaveText(['Domain', 'Root', 'Fallback']);
await expect(domainDetails.locator('dd')).toHaveText([
'go.example.org',
'404 Not found',
'Fallback'
'Uses fallback',
'404 Not found'
]);
await expect(
page.getByRole('region', { name: 'Base visits', exact: true }).locator('.admin-stat-value')
).toHaveText('0');
await expect(rootRow.locator('.admin-inventory-number')).toHaveText('1');
await expect(domainDetails.getByRole('link', { name: 'Root', exact: true })).toHaveCount(0);
await expect(domainDetails.getByRole('link', { name: 'Fallback', exact: true })).toHaveCount(0);
await expect(
linksInventory.getByText('https://example.org/first-domain', { exact: true })
).toBeVisible();
await expect(
linksInventory.getByRole('link', { name: 'https://example.org/first-domain', exact: true })
).toHaveCount(0);
await expect(baseVisitsLink).toHaveAttribute(
'href',
'/admin/domains/go.example.org/links?period=all'
);
await expect(rootLink).toHaveAttribute('href', '/admin/domains/go.example.org/links?period=all');
const linkDetails = page.getByRole('region', { name: 'Link details', exact: true });
const domainDetailsLink = linkDetails.getByRole('link', { name: 'go.example.org', exact: true });
@@ -233,7 +230,26 @@ test('shared credentials protect admin pages and sign-out removes access', async
await expect(
linkDetails.getByRole('heading', { name: 'Link details', exact: true })
).toBeVisible();
await expect(linkDetails.locator('dt')).toHaveText(['Domain', 'Path', 'Destination']);
await expect(linkDetails.locator('dt')).toHaveText([
'Domain',
'Path',
'Destination',
'HTTP status',
'Forward query',
'Starts',
'Expires',
'Tags'
]);
await expect(linkDetails.locator('time').first()).toBeVisible();
await expect(linkDetails.locator('time').first()).toHaveAttribute(
'datetime',
'2020-01-01T00:00:00Z'
);
await expect(linkDetails.locator('time').last()).toHaveAttribute(
'datetime',
'2100-01-01T00:00:00Z'
);
await expect(linkDetails.getByText('work, docs', { exact: true })).toBeVisible();
await expect(linkDetails.locator('dd').nth(1)).toHaveText('/docs');
await expect(page.locator('.admin-cards .admin-stat-label')).toHaveText(['Visits', 'Status']);
await expect(page.getByRole('region', { name: 'Status', exact: true })).toContainText('Active');
@@ -281,17 +297,27 @@ test('shared credentials protect admin pages and sign-out removes access', async
await expect(visitsValue).toHaveText('0');
await domainDetailsLink.click();
await baseVisitsLink.click();
await rootLink.click();
await expect(page).toHaveURL(`${appServer.origin}/admin/domains/go.example.org/links?period=all`);
await expect(page.getByRole('heading', { name: 'Link', exact: true })).toBeVisible();
await expect(page).toHaveTitle('Link — go.example.org/ — Goto');
await expect(visitsValue).toHaveText('0');
await domainDetailsLink.click();
await expect(page.getByRole('heading', { name: 'Root', exact: true })).toBeVisible();
await expect(page).toHaveTitle('Root — go.example.org — Goto');
await expect(visitsValue).toHaveText('1');
await expect(page.getByRole('region', { name: 'Status', exact: true })).toHaveCount(0);
const rootDetails = page.getByRole('region', { name: 'Root details', exact: true });
await expect(rootDetails.locator('dd')).toHaveText([
'go.example.org',
'go.example.org/',
'Uses fallback'
]);
await expect(page.getByRole('region', { name: 'Fallback', exact: true })).toContainText(
'404 Not found'
);
await rootDetails.getByRole('link', { name: 'go.example.org', exact: true }).click();
await expect(timeframe.getByRole('button', { name: '3 months', exact: true })).toHaveAttribute(
'aria-pressed',
'true'
);
await expect(visitsValue).toHaveText('2');
await expect(visitsValue).toHaveText('3');
await page
.getByRole('navigation', { name: 'Administration', exact: true })
@@ -356,7 +382,12 @@ test.describe('analytics disabled', () => {
).toHaveText(['Path', 'Destination', 'State']);
await expect(
page.getByRole('region', { name: 'Domain details', exact: true }).locator('dt')
).toHaveText(['Domain', 'Base destination', 'Base status']);
).toHaveText(['Domain', 'Root', 'Fallback']);
await expect(
page
.getByRole('region', { name: 'Links on go.example.org', exact: true })
.getByRole('link', { name: '(root)', exact: true })
).toBeVisible();
await page.getByRole('link', { name: '/docs', exact: true }).click();
await expect(page.getByRole('region', { name: 'Link details', exact: true })).toBeVisible();
await expect(page.getByRole('region', { name: 'Browsers', exact: true })).toHaveCount(0);
+7 -1
View File
@@ -34,7 +34,12 @@ const configuration = {
'/_app/version.json': { url: 'https://example.org/public-asset' },
'/a/b': { url: 'https://example.org/decoded' },
'/a%2Fb': { url: 'https://example.org/literal-percent' },
'/docs': { url: 'https://example.org/first-domain' },
'/docs': {
url: 'https://example.org/first-domain',
valid_from: '2020-01-01T00:00:00Z',
valid_until: '2100-01-01T00:00:00Z',
tags: ['work', 'docs']
},
'/healthz': { url: 'https://example.org/public-health' }
}
},
@@ -70,6 +75,7 @@ async function seedVisits() {
os: 'Linux'
},
{ domain: 'go.example.org', path: '/a/b', outcome: 'invalid' },
// Legacy Root classification exercises report normalization without rewriting raw visits.
{ domain: 'go.example.org', path: '/', outcome: 'invalid' },
{
domain: 'go.example.org',
+1 -1
View File
@@ -75,7 +75,7 @@ test('the compiled writer persists outcomes, isolates locks and drains visits be
)
.all('go.example.org', '/', '/missing')
).toEqual([
{ path: '/', outcome: 'invalid' },
{ path: '/', outcome: 'valid' },
{ path: '/missing', outcome: 'invalid' }
]);
} finally {
@@ -38,10 +38,12 @@
<a
class="admin-inventory-row admin-inventory-link"
href={`${link.href}?period=${timeframe}`}
aria-label={link.path}
aria-label={link.path === '/' ? '(root)' : link.path}
>
<span class="admin-inventory-primary"><code>{link.path}</code></span>
<span class="admin-inventory-secondary">{link.url ?? '404 Not found'}</span>
<span class="admin-inventory-primary"
><code>{link.path === '/' ? '(root)' : link.path}</code></span
>
<span class="admin-inventory-secondary">{link.url ?? '[not found]'}</span>
<span class="admin-inventory-state">
{#if link.state === 'active'}Active
{:else if link.state === 'disabled'}Disabled
@@ -1,5 +1,10 @@
import { expect, test } from 'vitest';
import { formatUpdated, formatUpdatedCompact } from './format.js';
import { formatUpdated, formatUpdatedCompact, formatUtc } from './format.js';
test('formats detail timestamps in UTC with seconds', () => {
expect(formatUtc('2026-11-06T22:03:48Z')).toBe('Nov 6, 2026, 22:03:48 UTC');
expect(formatUtc('2026-11-06T17:03:48-05:00')).toBe('Nov 6, 2026, 22:03:48 UTC');
});
test('formats the snapshot update time in UTC', () => {
const updatedAt = Date.parse('2026-11-06T22:03:00Z');
@@ -1,7 +1,18 @@
const utcFormatter = new Intl.DateTimeFormat('en', {
dateStyle: 'medium',
timeStyle: 'medium',
hourCycle: 'h23',
timeZone: 'UTC'
});
export function formatCount(value: number | null): string {
return value === null ? '—' : value.toLocaleString('en');
}
export function formatUtc(value: string): string {
return `${utcFormatter.format(new Date(value))} UTC`;
}
export function formatUpdated(value: number | null): string {
if (value === null) return '';
const formatter = new Intl.DateTimeFormat('en', {
+13 -13
View File
@@ -315,20 +315,20 @@ describe('validity and fallback', () => {
expect(missing.visit).toMatchObject({ path: '/home', outcome: 'invalid' });
});
test('an unconfigured root uses the fallback response but remains an invalid base visit', () => {
test('a root request using fallback is still a valid Root visit', () => {
const domain = preparedDomain({}, fallback);
const { response, visit } = resolveRedirect(domain, host, '/?incoming=ignored', 'GET');
expect(response.status).toBe(302);
expect(response.headers.get('location')).toBe(fallbackUrl);
expect(visit).toMatchObject({ path: '/', outcome: 'invalid' });
expect(visit).toMatchObject({ path: '/', outcome: 'valid' });
});
test('an unconfigured root remains a base visit with an invalid outcome', () => {
test('a root request returning 404 is still a valid Root visit', () => {
const domain = preparedDomain();
const { response, visit } = resolveRedirect(domain, host, '/', 'GET');
expect(response.status).toBe(404);
expect(visit).toMatchObject({ path: '/', outcome: 'invalid' });
expect(visit).toMatchObject({ path: '/', outcome: 'valid' });
});
test('default fallback options preserve the URL without appending the incoming path or query', () => {
@@ -421,22 +421,22 @@ describe('query forwarding', () => {
describe('direct self-loops', () => {
test.each([
['/', 'https://GO.EXAMPLE.TEST:443/?changed=one#fragment'],
['/%64ocs?incoming=one', 'https://go.example.test/docs?different=two'],
['/a%2Fb', 'https://go.example.test/a/b#fragment'],
['/docs', 'http://go.example.test:80/docs'],
['/docs', 'https:///go.example.test/docs']
])('runtime blocks a direct fallback loop: %s -> %s', (target, url) => {
['/', 'https://GO.EXAMPLE.TEST:443/?changed=one#fragment', 'valid'],
['/%64ocs?incoming=one', 'https://go.example.test/docs?different=two', 'invalid'],
['/a%2Fb', 'https://go.example.test/a/b#fragment', 'invalid'],
['/docs', 'http://go.example.test:80/docs', 'invalid'],
['/docs', 'https:///go.example.test/docs', 'invalid']
])('runtime blocks a direct fallback loop: %s -> %s', (target, url, expectedOutcome) => {
const domain = preparedDomain({}, preparedResponse({ url, forward_query: true }));
const { response, visit } = resolveRedirect(domain, host, target, 'GET');
expect(response.status).toBe(404);
expect(visit?.outcome).toBe('invalid');
expect(visit?.outcome).toBe(expectedOutcome);
expect(response.headers.has('location')).toBe(false);
expect(response.headers.get('cache-control')).toBe('no-store');
});
test('a direct root loop returns an invalid 404 without trying fallback', () => {
test('a Root loop is blocked with 404 without making the Root request invalid', () => {
const root = preparedResponse({
url: 'https://GO.EXAMPLE.TEST:443/?owned=old#section',
forward_query: true
@@ -447,7 +447,7 @@ describe('direct self-loops', () => {
expect(response.status).toBe(404);
expect(response.headers.has('location')).toBe(false);
expect(response.headers.get('cache-control')).toBe('no-store');
expect(visit).toMatchObject({ path: '/', outcome: 'invalid' });
expect(visit).toMatchObject({ path: '/', outcome: 'valid' });
});
test('a direct link loop returns an invalid 404 without trying fallback', () => {
+7 -7
View File
@@ -91,13 +91,12 @@ function resolveVisitResponse(
target: ParsedTarget,
now: number
): ResolvedResponse {
const isRoot = target.path === '/';
let redirect = domain.fallback;
let outcome: VisitOutcome = 'invalid';
if (target.path === '/') {
if (domain.root !== null) {
redirect = domain.root;
outcome = 'valid';
}
if (isRoot) {
outcome = 'valid';
if (domain.root !== null) redirect = domain.root;
} else {
const link = findActiveLink(domain, target.path, now);
if (link) {
@@ -108,7 +107,7 @@ function resolveVisitResponse(
if (redirect === null) {
const response = emptyResponse(404);
return { response, outcome: 'invalid' };
return { response, outcome };
}
let destination = redirect.url;
@@ -116,7 +115,8 @@ function resolveVisitResponse(
const selfRedirect = isSelfRedirect(destination, host, target.path);
if (selfRedirect) {
const response = emptyResponse(404);
return { response, outcome: 'invalid' };
if (!isRoot) outcome = 'invalid';
return { response, outcome };
}
const response = redirectResponse(destination, redirect.status_code);
@@ -36,7 +36,7 @@ test('reads prepared output and hides snapshots from a different retention polic
};
const days = visitDays(period);
const details: VisitDetails = {
version: 2,
version: 3,
scope: summary.scope,
period,
daily: days.map((day, index) => ({
@@ -1,11 +1,6 @@
import { error } from '@sveltejs/kit';
import { countBrowserOsVisitsByTimeframe } from '@goto/analytics';
import {
availableVisitTimeframes,
countVisitWindows,
visitWindow,
type VisitTimeframe
} from '@goto/analytics/daily';
import { availableVisitTimeframes, countVisitWindows, visitWindow } from '@goto/analytics/daily';
import { currentLinkState } from '#lib/server/link-state.js';
import { readDomainDetails } from '#lib/server/visit-reports.js';
import type { PageServerLoad } from './$types';
@@ -36,35 +31,32 @@ export const load = (async ({ locals, params }) => {
domainDetails ?? undefined,
windows
);
const baseCountsByTimeframe = countVisitWindows(detailsByPath.get('/')?.daily, windows);
const invalidClicksByTimeframe: Partial<Record<VisitTimeframe, number>> = {};
for (const { timeframe } of windows) {
const totals = countsByTimeframe[timeframe];
if (!totals) continue;
const base = baseCountsByTimeframe[timeframe];
if (!base) continue;
invalidClicksByTimeframe[timeframe] = totals.invalidVisits - base.invalidVisits;
}
let rootUrl: string | null = null;
if (prepared.root !== null) rootUrl = prepared.root.url;
else if (prepared.fallback !== null) rootUrl = prepared.fallback.url;
const rootCountsByTimeframe = countVisitWindows(detailsByPath.get('/')?.daily, windows);
const rootHref = linkHref(domain, '/');
const now = Date.now();
const links = Object.entries(prepared.links).map(([path, link]) => {
const daily = detailsByPath.get(path)?.daily;
const countsByTimeframe = countVisitWindows(daily, windows);
return {
path,
url: link.url,
state: currentLinkState(link, now),
countsByTimeframe,
href: linkHref(domain, path)
};
});
const hasRoot = prepared.root !== null;
let baseUrl: string | null = null;
if (prepared.root !== null) {
baseUrl = prepared.root.url;
} else if (prepared.fallback !== null) {
baseUrl = prepared.fallback.url;
}
const links = [
{
path: '/',
url: rootUrl,
state: 'active' as const,
countsByTimeframe: rootCountsByTimeframe,
href: rootHref
},
...Object.entries(prepared.links).map(([path, link]) => {
const daily = detailsByPath.get(path)?.daily;
const countsByTimeframe = countVisitWindows(daily, windows);
return {
path,
url: link.url,
state: currentLinkState(link, now),
countsByTimeframe,
href: linkHref(domain, path)
};
})
];
const asOf = domainDetails ? Date.parse(domainDetails.period.asOf) : null;
const chart = domainDetails ? { daily: domainDetails.daily, period: domainDetails.period } : null;
return {
@@ -73,18 +65,13 @@ export const load = (async ({ locals, params }) => {
collectionEnabled,
available: domainDetails !== null,
asOf,
linkCount: links.length,
linkCount: currentPaths.length,
timeframes,
countsByTimeframe,
browserOsVisitsByTimeframe,
invalidClicksByTimeframe,
chart,
base: {
url: baseUrl,
hasRoot,
countsByTimeframe: baseCountsByTimeframe,
href: linkHref(domain, '/')
},
root: prepared.root,
fallback: prepared.fallback,
links
};
}) satisfies PageServerLoad;
@@ -20,8 +20,7 @@
data.chart ? visitGranularity(timeframe, data.chart.period.retentionMonths) : 'day'
);
let visits = $derived(data.countsByTimeframe[timeframe]?.validVisits ?? null);
let baseVisits = $derived(data.base.countsByTimeframe[timeframe]?.validVisits ?? null);
let invalidClicks = $derived(data.invalidClicksByTimeframe[timeframe] ?? null);
let invalidClicks = $derived(data.countsByTimeframe[timeframe]?.invalidVisits ?? null);
let browserOsVisits = $derived(data.browserOsVisitsByTimeframe[timeframe] ?? null);
</script>
@@ -47,13 +46,6 @@
{#if data.collectionEnabled}
<StatCard label="Visits" value={visits} />
<StatCard label="Invalid clicks" value={invalidClicks} />
<a
class="base-visits-link"
href={`${data.base.href}?period=${timeframe}`}
aria-label="Base visits"
>
<StatCard label="Base visits" value={baseVisits} />
</a>
{/if}
</div>
<section class="admin-info" aria-labelledby="domain-details-heading">
@@ -61,14 +53,17 @@
<dl>
<dt>Domain</dt>
<dd>{data.domain}</dd>
<dt>Base destination</dt>
<dt>Root</dt>
<dd>
{#if data.base.url}<a class="admin-destination" href={data.base.url}>{data.base.url}</a>
{:else}404 Not found{/if}
{#if data.root}
<a class="admin-destination" href={data.root.url}>{data.root.url}</a>
{:else}Uses fallback{/if}
</dd>
<dt>Base status</dt>
<dt>Fallback</dt>
<dd>
{#if data.base.hasRoot}Active{:else}Fallback{/if}
{#if data.fallback}
<a class="admin-destination" href={data.fallback.url}>{data.fallback.url}</a>
{:else}404 Not found{/if}
</dd>
</dl>
</section>
@@ -80,22 +75,11 @@
{/if}
<section>
<h2>Links</h2>
{#if data.links.length === 0}<p>No links configured.</p>
{:else}
<LinkInventory
label={`Links on ${data.domain}`}
links={data.links}
{timeframe}
collectionEnabled={data.collectionEnabled}
/>
{/if}
<LinkInventory
label={`Links on ${data.domain}`}
links={data.links}
{timeframe}
collectionEnabled={data.collectionEnabled}
/>
</section>
</AdminPage>
<style>
.base-visits-link {
color: inherit;
text-decoration: none;
border-radius: var(--radius-card);
}
</style>
@@ -0,0 +1,43 @@
import { error } from '@sveltejs/kit';
import { countBrowserOsVisitsByTimeframe } from '@goto/analytics';
import { availableVisitTimeframes, countVisitWindows, visitWindow } from '@goto/analytics/daily';
import { readVisitDetails } from '#lib/server/visit-reports.js';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, params }) => {
const snapshot = locals.snapshot;
const domain = params.domain;
if (!Object.hasOwn(snapshot.domains, domain)) throw error(404, 'Domain not configured');
const prepared = snapshot.domains[domain]!;
const response = prepared.root;
const fallback = prepared.fallback;
const configuration = snapshot.configuration;
const retentionMonths = configuration.analytics.retention_months;
const collectionEnabled = retentionMonths !== 0;
const scope = { kind: 'link' as const, domain, path: '/' };
const details = collectionEnabled ? await readVisitDetails(configuration, scope) : null;
const timeframes = retentionMonths === 0 ? [] : availableVisitTimeframes(retentionMonths);
const windows = details
? timeframes.map((timeframe) => ({ timeframe, window: visitWindow(details.period, timeframe) }))
: [];
const countsByTimeframe = countVisitWindows(details?.daily, windows);
const browserOsVisitsByTimeframe = countBrowserOsVisitsByTimeframe(details ?? undefined, windows);
const asOf = details ? Date.parse(details.period.asOf) : null;
const chart = details ? { daily: details.daily, period: details.period } : null;
return {
domain,
domainHref: `/admin/domains/${encodeURIComponent(domain)}`,
response,
fallback,
fallbackSource: prepared.provenance.fallback,
retentionMonths,
collectionEnabled,
available: details !== null,
asOf,
timeframes,
countsByTimeframe,
browserOsVisitsByTimeframe,
chart
};
}) satisfies PageServerLoad;
@@ -0,0 +1,103 @@
<script lang="ts">
import { page } from '$app/state';
import { resolveVisitTimeframe, visitGranularity, visitWindow } from '@goto/analytics/daily';
import VisitsChart from '#lib/components/admin/VisitsChart.svelte';
import BrowserOsBreakdowns from '#lib/components/admin/BrowserOsBreakdowns.svelte';
import VisitTimeframes from '#lib/components/admin/VisitTimeframes.svelte';
import AdminPage from '#lib/components/admin/AdminPage.svelte';
import StatCard from '#lib/components/admin/StatCard.svelte';
import ReportNotice from '#lib/components/admin/ReportNotice.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let currentUrl = $derived(page.shallow?.url ?? page.url);
let timeframe = $derived(
resolveVisitTimeframe(data.timeframes, currentUrl.searchParams.get('period'))
);
let window = $derived(data.chart ? visitWindow(data.chart.period, timeframe) : null);
let granularity = $derived(
data.chart ? visitGranularity(timeframe, data.chart.period.retentionMonths) : 'day'
);
let visits = $derived(data.countsByTimeframe[timeframe]?.validVisits ?? null);
let browserOsVisits = $derived(data.browserOsVisitsByTimeframe[timeframe] ?? null);
</script>
<AdminPage
siteTitle={data.title}
title="Root"
documentTitle={`Root — ${data.domain}`}
updatedAt={data.asOf}
overviewHref={`/admin?period=${timeframe}`}
>
{#snippet actions()}
{#if data.available && data.timeframes.length > 0}
<VisitTimeframes
timeframes={data.timeframes}
retentionMonths={data.retentionMonths}
value={timeframe}
/>
{/if}
{/snippet}
<ReportNotice collectionEnabled={data.collectionEnabled} available={data.available} />
<div class="admin-cards">
{#if data.collectionEnabled}
<StatCard label="Visits" value={visits} />
{/if}
</div>
<section class="admin-info" aria-labelledby="root-details-heading">
<h2 id="root-details-heading">Root details</h2>
<dl>
<dt>Domain</dt>
<dd><a href={`${data.domainHref}?period=${timeframe}`}>{data.domain}</a></dd>
<dt>Address</dt>
<dd>{data.domain}/</dd>
<dt>Destination</dt>
<dd>
{#if data.response}
<a class="admin-destination" href={data.response.url}>
{data.response.url}
</a>
{:else}
<a href="#fallback">Uses fallback</a>
{/if}
</dd>
{#if data.response}
<dt>HTTP status</dt>
<dd>{data.response.status_code}</dd>
<dt>Forward query</dt>
<dd>{data.response.forward_query ? 'Yes' : 'No'}</dd>
{/if}
</dl>
</section>
<section id="fallback" class="admin-info" aria-labelledby="fallback-heading">
<h2 id="fallback-heading">Fallback</h2>
<dl>
<dt>Source</dt>
<dd>
{#if data.fallbackSource === 'domain'}Domain
{:else if data.fallbackSource === 'global'}Global
{:else}Default{/if}
</dd>
<dt>Destination</dt>
<dd>
{#if data.fallback}
<a class="admin-destination" href={data.fallback.url}>
{data.fallback.url}
</a>
{:else}404 Not found{/if}
</dd>
{#if data.fallback}
<dt>HTTP status</dt>
<dd>{data.fallback.status_code}</dd>
<dt>Forward query</dt>
<dd>{data.fallback.forward_query ? 'Yes' : 'No'}</dd>
{/if}
</dl>
</section>
{#if data.chart && window}
<VisitsChart daily={data.chart.daily} {window} {granularity} />
{/if}
{#if browserOsVisits && visits !== null}
<BrowserOsBreakdowns {browserOsVisits} totalVisits={visits} />
{/if}
</AdminPage>
@@ -1,7 +1,7 @@
import { error } from '@sveltejs/kit';
import { countBrowserOsVisitsByTimeframe } from '@goto/analytics';
import { availableVisitTimeframes, countVisitWindows, visitWindow } from '@goto/analytics/daily';
import { currentLinkState, type LinkState } from '#lib/server/link-state.js';
import { currentLinkState } from '#lib/server/link-state.js';
import { readVisitDetails } from '#lib/server/visit-reports.js';
import type { PageServerLoad } from './$types';
@@ -12,26 +12,16 @@ export const load = (async ({ locals, params }) => {
const prepared = snapshot.domains[domain]!;
// Kit has decoded route parameters; preserve literal percent paths as-is.
const path = `/${params.path ?? ''}`;
if (path !== '/' && !Object.hasOwn(prepared.links, path)) throw error(404, 'Link not configured');
let url: string | null = null;
let state: LinkState | 'fallback';
if (path === '/') {
const root = prepared.root;
if (root !== null) {
url = root.url;
state = 'active';
} else {
state = 'fallback';
}
} else {
const link = prepared.links[path]!;
url = link.url;
state = currentLinkState(link, Date.now());
}
let fallback: string | 404 = 404;
if (prepared.fallback !== null) {
fallback = prepared.fallback.url;
}
if (!Object.hasOwn(prepared.links, path)) throw error(404, 'Link not configured');
const link = prepared.links[path]!;
const response = {
url: link.url,
status_code: link.status_code,
forward_query: link.forward_query
};
const state = currentLinkState(link, Date.now());
const validFrom = link.valid_from ?? null;
const validUntil = link.valid_until ?? null;
const configuration = snapshot.configuration;
const retentionMonths = configuration.analytics.retention_months;
const collectionEnabled = retentionMonths !== 0;
@@ -50,9 +40,12 @@ export const load = (async ({ locals, params }) => {
domain,
path,
domainHref: `/admin/domains/${encodeURIComponent(domain)}`,
url,
fallback,
response,
state,
validFrom,
validUntil,
tags: link.tags,
sources: link.provenance,
retentionMonths,
collectionEnabled,
available: details !== null,
@@ -1,12 +1,14 @@
<script lang="ts">
import { page } from '$app/state';
import { resolveVisitTimeframe, visitGranularity, visitWindow } from '@goto/analytics/daily';
import type { SettingSource } from '@goto/config';
import VisitsChart from '#lib/components/admin/VisitsChart.svelte';
import BrowserOsBreakdowns from '#lib/components/admin/BrowserOsBreakdowns.svelte';
import VisitTimeframes from '#lib/components/admin/VisitTimeframes.svelte';
import AdminPage from '#lib/components/admin/AdminPage.svelte';
import StatCard from '#lib/components/admin/StatCard.svelte';
import ReportNotice from '#lib/components/admin/ReportNotice.svelte';
import { formatUtc } from '#lib/components/admin/format.js';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
@@ -22,6 +24,15 @@
let browserOsVisits = $derived(data.browserOsVisitsByTimeframe[timeframe] ?? null);
</script>
{#snippet sourceHint(source: SettingSource)}
<span class="source-hint">
{#if source === 'builtin'}Built-in
{:else if source === 'global'}Global defaults
{:else if source === 'domain'}Domain defaults
{:else}Link override{/if}
</span>
{/snippet}
<AdminPage
siteTitle={data.title}
title="Link"
@@ -51,8 +62,6 @@
<StatCard label="Status" value="Scheduled" />
{:else if data.state === 'expired'}
<StatCard label="Status" value="Expired" />
{:else}
<StatCard label="Status" value="Fallback" />
{/if}
</div>
<section class="admin-info" aria-labelledby="link-details-heading">
@@ -64,9 +73,36 @@
<dd>{data.path}</dd>
<dt>Destination</dt>
<dd>
{#if data.url}<a class="admin-destination" href={data.url}>{data.url}</a>
{:else if data.fallback === 404}404 Not found{:else}{data.fallback}{/if}
<a class="admin-destination" href={data.response.url}>
{data.response.url}
</a>
</dd>
<dt>HTTP status</dt>
<dd>
{data.response.status_code}
{@render sourceHint(data.sources.status_code)}
</dd>
<dt>Forward query</dt>
<dd>
{data.response.forward_query ? 'Yes' : 'No'}
{@render sourceHint(data.sources.forward_query)}
</dd>
{#if data.validFrom}
<dt>Starts</dt>
<dd>
<time datetime={data.validFrom}>{formatUtc(data.validFrom)}</time>
</dd>
{/if}
{#if data.validUntil}
<dt>Expires</dt>
<dd>
<time datetime={data.validUntil}>{formatUtc(data.validUntil)}</time>
</dd>
{/if}
{#if data.tags.length > 0}
<dt>Tags</dt>
<dd>{data.tags.join(', ')}</dd>
{/if}
</dl>
</section>
{#if data.chart && window}
@@ -76,3 +112,11 @@
<BrowserOsBreakdowns {browserOsVisits} totalVisits={visits} />
{/if}
</AdminPage>
<style>
.source-hint {
margin-inline-start: calc(var(--space-1) * 2);
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
</style>
@@ -1,4 +1,4 @@
import { validateConfig } from '@goto/config';
import { validateConfig, type DomainConfiguration } from '@goto/config';
import { createVisitPeriod, visitDays, type VisitDetails } from '@goto/analytics';
import { beforeEach, expect, test, vi } from 'vitest';
import { prepareSnapshot } from '#lib/server/snapshot.js';
@@ -9,27 +9,43 @@ vi.mock('#lib/server/visit-reports.js', () => ({ readVisitDetails: vi.fn() }));
beforeEach(() => vi.resetAllMocks());
const domain = 'go.example.test';
function request(path = '/docs', host = domain, retention_months = 3, rootUrl?: string) {
const linkMetadata = {
response: { url: 'https://example.test/docs', status_code: 308, forward_query: true },
state: 'active',
validFrom: '2000-01-01T00:00:00Z',
validUntil: '2100-01-01T00:00:00Z',
tags: ['guides', 'reference'],
sources: { status_code: 'link', forward_query: 'domain' }
};
function request(path = '/docs', options: { host?: string; retentionMonths?: number } = {}) {
const domainConfiguration: Partial<DomainConfiguration> = {
link_defaults: { forward_query: true },
links: {
'/docs': {
url: 'https://example.test/docs',
status_code: 308,
enabled: true,
valid_from: '2000-01-01T00:00:00Z',
valid_until: '2100-01-01T00:00:00Z',
tags: ['guides', 'reference']
},
'/a/b': { url: 'https://example.test/nested', enabled: true, tags: [] },
'/a%2Fb': { url: 'https://example.test/percent', enabled: false, tags: [] }
}
};
const prepared = validateConfig(
{
auth: { credentials: { username: 'test', password: 'link-test-password' } },
analytics: { retention_months },
domains: {
[domain]: {
...(rootUrl ? { root: { url: rootUrl } } : {}),
links: {
'/docs': { url: 'https://example.test/docs' },
'/a/b': { url: 'https://example.test/nested' },
'/a%2Fb': { url: 'https://example.test/percent', enabled: false }
}
}
}
analytics: { retention_months: options.retentionMonths ?? 3 },
link_defaults: { status_code: 301 },
domains: { [domain]: domainConfiguration }
},
{ env: {} }
);
return {
locals: { snapshot: prepareSnapshot(prepared) },
params: { domain: host, path: path.slice(1) }
params: { domain: options.host ?? domain, path: path.slice(1) }
} as Parameters<typeof load>[0];
}
@@ -48,7 +64,7 @@ const browserOsCounts = Object.entries(counts).map(([day, [validVisits, invalidV
invalidVisits
}));
const details: VisitDetails = {
version: 2,
version: 3,
scope: { kind: 'link', domain, path: '/docs' },
period,
daily: visitDays(period).map((day) => {
@@ -59,7 +75,7 @@ const details: VisitDetails = {
operatingSystems: browserOsCounts
};
test('projects only the requested link series and compact window counts and breakdowns', async () => {
test('projects only requested link settings, series and compact window counts and breakdowns', async () => {
vi.mocked(readVisitDetails).mockResolvedValue(details);
const data = await load(request());
expect(readVisitDetails).toHaveBeenCalledExactlyOnceWith(expect.any(Object), {
@@ -71,9 +87,7 @@ test('projects only the requested link series and compact window counts and brea
domain,
path: '/docs',
domainHref: '/admin/domains/go.example.test',
url: 'https://example.test/docs',
fallback: 404,
state: 'active',
...linkMetadata,
retentionMonths: 3,
collectionEnabled: true,
available: true,
@@ -102,15 +116,21 @@ test('projects only the requested link series and compact window counts and brea
});
});
test('keeps decoded nested and literal-percent identities distinct and supports historical root traffic', async () => {
test('keeps decoded nested and literal-percent identities distinct', async () => {
vi.mocked(readVisitDetails)
.mockResolvedValueOnce({ ...details, scope: { kind: 'link', domain, path: '/a/b' } })
.mockResolvedValueOnce({ ...details, scope: { kind: 'link', domain, path: '/a%2Fb' } })
.mockResolvedValueOnce({ ...details, scope: { kind: 'link', domain, path: '/' } });
.mockResolvedValueOnce({ ...details, scope: { kind: 'link', domain, path: '/a%2Fb' } });
const nested = await load(request('/a/b'));
const percent = await load(request('/a%2Fb'));
const root = await load(request('/'));
expect(nested.path).toBe('/a/b');
expect(nested).toMatchObject({
response: { url: 'https://example.test/nested', status_code: 301, forward_query: true },
state: 'active',
validFrom: null,
validUntil: null,
tags: [],
sources: { status_code: 'global', forward_query: 'domain' }
});
expect(percent.path).toBe('/a%2Fb');
expect(percent.state).toBe('disabled');
expect(percent.countsByTimeframe.all?.validVisits).toBe(15);
@@ -124,38 +144,14 @@ test('keeps decoded nested and literal-percent identities distinct and supports
domain,
path: '/a%2Fb'
});
expect(readVisitDetails).toHaveBeenNthCalledWith(3, expect.any(Object), {
kind: 'link',
domain,
path: '/'
});
expect(root.path).toBe('/');
expect(root.state).toBe('fallback');
expect(root.url).toBeNull();
expect(root.fallback).toBe(404);
expect(root.countsByTimeframe.all?.validVisits).toBe(15);
});
test('shows the configured root response while preserving its historical analytics scope', async () => {
vi.mocked(readVisitDetails).mockResolvedValue({
...details,
scope: { kind: 'link', domain, path: '/' }
});
const data = await load(request('/', domain, 3, 'https://example.test/start'));
expect(readVisitDetails).toHaveBeenCalledExactlyOnceWith(expect.any(Object), {
kind: 'link',
domain,
path: '/'
});
expect(data.path).toBe('/');
expect(data.state).toBe('active');
expect(data.url).toBe('https://example.test/start');
expect(data.countsByTimeframe.all).toEqual({ validVisits: 15, invalidVisits: 5 });
});
test('rejects unknown scopes before reading and keeps unavailable counts absent', async () => {
await expect(load(request('/docs', 'unknown.test'))).rejects.toMatchObject({ status: 404 });
await expect(load(request('/docs', { host: 'unknown.test' }))).rejects.toMatchObject({
status: 404
});
await expect(load(request('/not-configured'))).rejects.toMatchObject({ status: 404 });
await expect(load(request('/'))).rejects.toMatchObject({ status: 404 });
expect(readVisitDetails).not.toHaveBeenCalled();
vi.mocked(readVisitDetails).mockResolvedValue(null);
const data = await load(request());
@@ -164,12 +160,12 @@ test('rejects unknown scopes before reading and keeps unavailable counts absent'
expect(data.chart).toBeNull();
expect(data.asOf).toBeNull();
expect(data.countsByTimeframe).toEqual({});
expect(data.url).toBe('https://example.test/docs');
expect(data).toMatchObject(linkMetadata);
expect(data.retentionMonths).toBe(3);
});
test('disabled analytics skips the report read and leaves link details usable', async () => {
const data = await load(request('/docs', domain, 0));
const data = await load(request('/docs', { retentionMonths: 0 }));
expect(readVisitDetails).not.toHaveBeenCalled();
expect(data.collectionEnabled).toBe(false);
expect(data.browserOsVisitsByTimeframe).toEqual({});
@@ -178,6 +174,5 @@ test('disabled analytics skips the report read and leaves link details usable',
expect(data.timeframes).toEqual([]);
expect(data.chart).toBeNull();
expect(data.countsByTimeframe).toEqual({});
expect(data.url).toBe('https://example.test/docs');
expect(data.state).toBe('active');
expect(data).toMatchObject(linkMetadata);
});
@@ -0,0 +1,137 @@
import { validateConfig, type DomainConfiguration, type RedirectResponse } from '@goto/config';
import { createVisitPeriod, visitDays, type VisitDetails } from '@goto/analytics';
import { beforeEach, expect, test, vi } from 'vitest';
import { prepareSnapshot } from '#lib/server/snapshot.js';
import { readVisitDetails } from '#lib/server/visit-reports.js';
import { load } from './+page.server.js';
vi.mock('#lib/server/visit-reports.js', () => ({ readVisitDetails: vi.fn() }));
beforeEach(() => vi.resetAllMocks());
const domain = 'go.example.test';
const fallback: RedirectResponse = {
url: 'https://example.test/fallback',
status_code: 307,
forward_query: true
};
function request(options: { host?: string; root?: RedirectResponse } = {}) {
const domainConfiguration: Partial<DomainConfiguration> = {
fallback,
link_defaults: { forward_query: true },
links: {}
};
if (options.root) domainConfiguration.root = options.root;
const prepared = validateConfig(
{
auth: { credentials: { username: 'test', password: 'link-test-password' } },
analytics: { retention_months: 3 },
link_defaults: { status_code: 301 },
domains: { [domain]: domainConfiguration }
},
{ env: {} }
);
return {
locals: { snapshot: prepareSnapshot(prepared) },
params: { domain: options.host ?? domain }
} as Parameters<typeof load>[0];
}
const period = createVisitPeriod(3, Date.parse('2026-06-15T12:00:00.000Z'));
const rootBrowserOsCounts = [
{ day: period.from.slice(0, 10), name: 'Other', validVisits: 7, invalidVisits: 0 }
];
const rootDetails: VisitDetails = {
version: 3,
period,
scope: { kind: 'link', domain, path: '/' },
daily: visitDays(period).map((day, index) => ({
...day,
validVisits: index === 0 ? 7 : 0,
invalidVisits: 0
})),
browsers: rootBrowserOsCounts,
operatingSystems: rootBrowserOsCounts
};
test('supports historical root traffic without a configured root', async () => {
vi.mocked(readVisitDetails).mockResolvedValue(rootDetails);
const data = await load(request());
expect(readVisitDetails).toHaveBeenCalledExactlyOnceWith(expect.any(Object), {
kind: 'link',
domain,
path: '/'
});
expect(data).toMatchObject({
domain,
domainHref: '/admin/domains/go.example.test',
response: null,
fallback,
fallbackSource: 'domain'
});
expect(data.countsByTimeframe.all?.validVisits).toBe(7);
});
test('keeps root HTTP settings independent of link defaults and fallback with historical analytics scope', async () => {
vi.mocked(readVisitDetails).mockResolvedValue(rootDetails);
const response: RedirectResponse = {
url: 'https://example.test/start',
status_code: 302,
forward_query: false
};
const data = await load(request({ root: response }));
expect(readVisitDetails).toHaveBeenCalledExactlyOnceWith(expect.any(Object), {
kind: 'link',
domain,
path: '/'
});
expect(data).toMatchObject({
response,
fallback,
fallbackSource: 'domain'
});
expect(data.countsByTimeframe.all).toEqual({ validVisits: 7, invalidVisits: 0 });
});
test('keeps absent root and default or global fallback metadata usable without analytics', async () => {
const prepared = validateConfig(
{
auth: { credentials: { username: 'test', password: 'link-test-password' } },
analytics: { retention_months: 0 },
domains: { [domain]: { links: {} } }
},
{ env: {} }
);
const event = {
locals: { snapshot: prepareSnapshot(prepared) },
params: { domain }
} as Parameters<typeof load>[0];
const root = await load(event);
expect(root).toMatchObject({
response: null,
fallback: null,
fallbackSource: 'builtin'
});
const global = validateConfig(
{
auth: { credentials: { username: 'test', password: 'link-test-password' } },
analytics: { retention_months: 0 },
fallback,
domains: { [domain]: { links: {} } }
},
{ env: {} }
);
event.locals.snapshot = prepareSnapshot(global);
const inherited = await load(event);
expect(inherited).toMatchObject({
response: null,
fallback,
fallbackSource: 'global'
});
expect(readVisitDetails).not.toHaveBeenCalled();
});
test('rejects unknown domains before reading root traffic', async () => {
await expect(load(request({ host: 'unknown.test' }))).rejects.toMatchObject({ status: 404 });
expect(readVisitDetails).not.toHaveBeenCalled();
});
@@ -1,4 +1,4 @@
import { validateConfig } from '@goto/config';
import { validateConfig, type DomainConfiguration, type RedirectResponse } from '@goto/config';
import { createVisitPeriod, visitDays, type ReportScope, type VisitDetails } from '@goto/analytics';
import { beforeEach, expect, test, vi } from 'vitest';
import { prepareSnapshot } from '#lib/server/snapshot.js';
@@ -11,20 +11,33 @@ beforeEach(() => vi.resetAllMocks());
const domain = 'go.example.test';
const period = createVisitPeriod(3, Date.parse('2026-06-15T12:00:00.000Z'));
function request(host = domain, rootUrl?: string, retentionMonths = 3) {
const links: Record<string, { url: string; enabled?: boolean }> = {
function request(
host = domain,
settings: {
root?: RedirectResponse;
fallback?: RedirectResponse;
globalFallback?: RedirectResponse;
retentionMonths?: number;
links?: Record<string, { url: string; enabled?: boolean }>;
} = {}
) {
const links = settings.links ?? {
'/docs': { url: 'https://example.test/docs' },
'/a/b': { url: 'https://example.test/nested' },
'/a%2Fb': { url: 'https://example.test/percent', enabled: false }
};
const prepared = validateConfig(
{
auth: { credentials: { username: 'test', password: 'domain-test-password' } },
analytics: { retention_months: retentionMonths },
domains: { [domain]: { ...(rootUrl ? { root: { url: rootUrl } } : {}), links } }
},
{ env: {} }
);
const domainConfiguration: Pick<DomainConfiguration, 'root' | 'fallback'> & {
links: typeof links;
} = { links };
if (settings.root) domainConfiguration.root = settings.root;
if (settings.fallback) domainConfiguration.fallback = settings.fallback;
const configuration: Record<string, unknown> = {
auth: { credentials: { username: 'test', password: 'domain-test-password' } },
analytics: { retention_months: settings.retentionMonths ?? 3 },
domains: { [domain]: domainConfiguration }
};
if (settings.globalFallback) configuration.fallback = settings.globalFallback;
const prepared = validateConfig(configuration, { env: {} });
return {
locals: { snapshot: prepareSnapshot(prepared) },
params: { domain: host }
@@ -45,7 +58,7 @@ function details(scope: ReportScope, counts: Record<string, [number, number]>):
invalidVisits
}));
return {
version: 2,
version: 3,
scope,
period,
daily,
@@ -57,20 +70,20 @@ function details(scope: ReportScope, counts: Record<string, [number, number]>):
const domainDetails = details(
{ kind: 'domain', domain },
{
'2026-03-15': [3, 2],
'2026-03-15': [4, 1],
'2026-05-16': [5, 4],
'2026-06-08': [2, 3],
'2026-06-09': [1, 2],
'2026-06-15': [4, 3]
'2026-06-08': [3, 2],
'2026-06-09': [2, 1],
'2026-06-15': [5, 2]
}
);
const baseDetails = details(
const rootDetails = details(
{ kind: 'link', domain, path: '/' },
{
'2026-03-15': [3, 1],
'2026-06-08': [0, 1],
'2026-06-09': [0, 1],
'2026-06-15': [0, 1]
'2026-03-15': [4, 0],
'2026-06-08': [1, 0],
'2026-06-09': [1, 0],
'2026-06-15': [1, 0]
}
);
const docsDetails = details(
@@ -85,15 +98,15 @@ const emptyDetails = details({ kind: 'link', domain, path: '/a%2Fb' }, {});
const report = {
domain: domainDetails,
links: [
{ path: '/', details: baseDetails },
{ path: '/', details: rootDetails },
{ path: '/docs', details: docsDetails },
{ path: '/a%2Fb', details: emptyDetails }
]
};
const baseCounts = {
'7d': { validVisits: 0, invalidVisits: 2 },
'30d': { validVisits: 0, invalidVisits: 3 },
all: { validVisits: 3, invalidVisits: 4 }
const rootCounts = {
'7d': { validVisits: 2, invalidVisits: 0 },
'30d': { validVisits: 3, invalidVisits: 0 },
all: { validVisits: 7, invalidVisits: 0 }
};
test('projects domain counts and browser/OS visits without exposing link series or dated browser/OS counts', async () => {
@@ -113,33 +126,35 @@ test('projects domain counts and browser/OS visits without exposing link series
linkCount: 3,
timeframes: ['7d', '30d', 'all'],
countsByTimeframe: {
'7d': { validVisits: 5, invalidVisits: 5 },
'30d': { validVisits: 7, invalidVisits: 8 },
all: { validVisits: 15, invalidVisits: 14 }
'7d': { validVisits: 7, invalidVisits: 3 },
'30d': { validVisits: 10, invalidVisits: 5 },
all: { validVisits: 19, invalidVisits: 10 }
},
browserOsVisitsByTimeframe: {
'7d': {
browsers: [{ name: 'Other', visits: 5 }],
operatingSystems: [{ name: 'Other', visits: 5 }]
},
'30d': {
browsers: [{ name: 'Other', visits: 7 }],
operatingSystems: [{ name: 'Other', visits: 7 }]
},
'30d': {
browsers: [{ name: 'Other', visits: 10 }],
operatingSystems: [{ name: 'Other', visits: 10 }]
},
all: {
browsers: [{ name: 'Other', visits: 15 }],
operatingSystems: [{ name: 'Other', visits: 15 }]
browsers: [{ name: 'Other', visits: 19 }],
operatingSystems: [{ name: 'Other', visits: 19 }]
}
},
invalidClicksByTimeframe: { '7d': 3, '30d': 5, all: 10 },
chart: { daily: domainDetails.daily, period },
base: {
url: null,
hasRoot: false,
countsByTimeframe: baseCounts,
href: '/admin/domains/go.example.test/links'
},
root: null,
fallback: null,
links: [
{
path: '/',
url: null,
state: 'active',
countsByTimeframe: rootCounts,
href: '/admin/domains/go.example.test/links'
},
{
path: '/docs',
url: 'https://example.test/docs',
@@ -173,53 +188,132 @@ test('projects domain counts and browser/OS visits without exposing link series
});
});
test('shows an active root destination without reclassifying retained outcomes or subtracting valid root traffic', async () => {
test('keeps root and fallback responses separate and includes all normalized root history in valid domain visits', async () => {
vi.mocked(readDomainDetails).mockResolvedValue(report);
const data = await load(request(domain, 'https://example.test/start'));
expect(data.base.url).toBe('https://example.test/start');
expect(data.base.hasRoot).toBe(true);
expect(data.base.countsByTimeframe).toEqual(baseCounts);
expect(data.countsByTimeframe.all?.validVisits).toBe(15);
expect(data.invalidClicksByTimeframe.all).toBe(10);
const root: RedirectResponse = {
url: 'https://example.test/start',
status_code: 307,
forward_query: true
};
const fallback: RedirectResponse = {
url: 'https://example.test/missing',
status_code: 308,
forward_query: false
};
const data = await load(
request(domain, {
root,
fallback,
globalFallback: { url: 'https://example.test/global', status_code: 302, forward_query: true }
})
);
expect(data.root).toEqual(root);
expect(data.fallback).toEqual(fallback);
expect(data.links[0]).toEqual({
path: '/',
url: root.url,
state: 'active',
countsByTimeframe: rootCounts,
href: '/admin/domains/go.example.test/links'
});
expect(data.countsByTimeframe.all).toEqual({ validVisits: 19, invalidVisits: 10 });
expect(data.linkCount).toBe(3);
expect(data.links.map(({ path }) => path)).toEqual(['/docs', '/a/b', '/a%2Fb']);
expect(data.links.map(({ path }) => path)).toEqual(['/', '/docs', '/a/b', '/a%2Fb']);
});
test('missing base details leave base and non-base invalid counts unavailable, not zero', async () => {
test('keeps root reachable through the domain fallback with no named links', async () => {
vi.mocked(readDomainDetails).mockResolvedValue(report);
const fallback: RedirectResponse = {
url: 'https://example.test/missing',
status_code: 302,
forward_query: false
};
const data = await load(request(domain, { fallback, links: {} }));
expect(readDomainDetails).toHaveBeenCalledExactlyOnceWith(expect.any(Object), domain, []);
expect(data.root).toBeNull();
expect(data.fallback).toEqual(fallback);
expect(data.linkCount).toBe(0);
expect(data.links).toEqual([
{
path: '/',
url: fallback.url,
state: 'active',
countsByTimeframe: rootCounts,
href: '/admin/domains/go.example.test/links'
}
]);
expect(data.countsByTimeframe.all).toEqual({ validVisits: 19, invalidVisits: 10 });
});
test('missing root details leave root visits unavailable without hiding non-root invalid counts', async () => {
vi.mocked(readDomainDetails).mockResolvedValue({
domain: domainDetails,
links: [{ path: '/docs', details: docsDetails }]
});
const data = await load(request());
expect(data.available).toBe(true);
expect(data.base.countsByTimeframe).toEqual({});
expect(data.invalidClicksByTimeframe).toEqual({});
expect(data.countsByTimeframe.all?.validVisits).toBe(15);
expect(data.links[0]).toEqual({
path: '/',
url: null,
state: 'active',
countsByTimeframe: {},
href: '/admin/domains/go.example.test/links'
});
expect(data.countsByTimeframe.all).toEqual({ validVisits: 19, invalidVisits: 10 });
});
test('rejects unknown domains before reading and keeps unavailable inventory usable', async () => {
await expect(load(request('unknown.test'))).rejects.toMatchObject({ status: 404 });
expect(readDomainDetails).not.toHaveBeenCalled();
vi.mocked(readDomainDetails).mockResolvedValue(null);
const data = await load(request());
const fallback: RedirectResponse = {
url: 'https://example.test/global',
status_code: 307,
forward_query: true
};
const data = await load(request(domain, { globalFallback: fallback }));
expect(data.root).toBeNull();
expect(data.fallback).toEqual(fallback);
expect(data.available).toBe(false);
expect(data.browserOsVisitsByTimeframe).toEqual({});
expect(data.chart).toBeNull();
expect(data.countsByTimeframe).toEqual({});
expect(data.invalidClicksByTimeframe).toEqual({});
expect(data.links[0]?.countsByTimeframe).toEqual({});
expect(data.links[0]).toEqual({
path: '/',
url: fallback.url,
state: 'active',
countsByTimeframe: {},
href: '/admin/domains/go.example.test/links'
});
expect(data.linkCount).toBe(3);
});
test('disabled collection reads no reports and preserves current inventory', async () => {
const data = await load(request(domain, undefined, 0));
const root: RedirectResponse = {
url: 'https://example.test/start',
status_code: 301,
forward_query: true
};
const fallback: RedirectResponse = {
url: 'https://example.test/missing',
status_code: 302,
forward_query: false
};
const data = await load(request(domain, { root, fallback, retentionMonths: 0 }));
expect(readDomainDetails).not.toHaveBeenCalled();
expect(data.collectionEnabled).toBe(false);
expect(data.browserOsVisitsByTimeframe).toEqual({});
expect(data.timeframes).toEqual([]);
expect(data.chart).toBeNull();
expect(data.countsByTimeframe).toEqual({});
expect(data.invalidClicksByTimeframe).toEqual({});
expect(data.base.hasRoot).toBe(false);
expect(data.root).toEqual(root);
expect(data.fallback).toEqual(fallback);
expect(data.links[0]).toEqual({
path: '/',
url: root.url,
state: 'active',
countsByTimeframe: {},
href: '/admin/domains/go.example.test/links'
});
expect(data.linkCount).toBe(3);
});
@@ -4,21 +4,11 @@
import IconWarningCircle from 'virtual:icons/ph/warning-circle';
import IconSpinner from 'virtual:icons/ph/spinner';
import AdminPage from '#lib/components/admin/AdminPage.svelte';
import { formatUtc } from '#lib/components/admin/format.js';
import type { ActionData, PageData, SubmitFunction } from './$types';
let { data, form }: { data: PageData; form: ActionData | null } = $props();
let pending = $state(false);
const utcFormatter = new Intl.DateTimeFormat('en', {
dateStyle: 'medium',
timeStyle: 'medium',
hourCycle: 'h23',
timeZone: 'UTC'
});
function formatUtc(value: string): string {
return `${utcFormatter.format(new Date(value))} UTC`;
}
const checkWorker: SubmitFunction = () => {
pending = true;
return async ({ update }) => {
@@ -41,11 +31,16 @@
<dd>{data.appUrl}</dd>
<dt>Authentication</dt>
<dd>{data.authMode === 'oidc' ? 'OIDC' : 'Shared credentials'}</dd>
<dt>Redirect status code</dt>
</dl>
</section>
<section class="admin-info" aria-labelledby="application-defaults-heading">
<h2 id="application-defaults-heading">Application defaults</h2>
<dl>
<dt>Status code</dt>
<dd>{data.redirectStatusCode}</dd>
<dt>Forward query</dt>
<dt>Query forwarding</dt>
<dd>{data.forwardQuery ? 'Yes' : 'No'}</dd>
<dt>Fallback destination</dt>
<dt>Fallback</dt>
<dd>{data.fallbackDestination === 404 ? '404 Not found' : data.fallbackDestination}</dd>
</dl>
</section>
@@ -42,7 +42,7 @@ function request(overrides: Record<string, unknown> = {}) {
function emptyOverview(): VisitDetails {
const period = createVisitPeriod(3, Date.parse('2026-06-15T12:34:56.000Z'));
return {
version: 2,
version: 3,
scope: { kind: 'overview' },
period,
daily: visitDays(period).map((day) => ({ ...day, validVisits: 0, invalidVisits: 0 })),
@@ -58,7 +58,7 @@ function details(
invalidVisits
}));
return {
version: 2,
version: 3,
scope,
period,
daily,
@@ -22,10 +22,10 @@ test('builds gap-free UTC daily details with zeros and partial calendar edges',
domain: null,
path: null,
day: '2026-05-31',
validVisits: 1,
invalidVisits: 4,
baseValidVisits: 0,
baseInvalidVisits: 2
validVisits: 3,
invalidVisits: 2,
baseValidVisits: 2,
baseInvalidVisits: 0
}
],
browsers: [
@@ -44,8 +44,8 @@ test('builds gap-free UTC daily details with zeros and partial calendar edges',
path: null,
day: '2026-05-31',
name: 'Firefox',
validVisits: 1,
invalidVisits: 4
validVisits: 3,
invalidVisits: 2
}
],
operatingSystems: [
@@ -64,8 +64,8 @@ test('builds gap-free UTC daily details with zeros and partial calendar edges',
path: null,
day: '2026-05-31',
name: 'Linux',
validVisits: 1,
invalidVisits: 4
validVisits: 3,
invalidVisits: 2
}
]
},
@@ -77,18 +77,18 @@ test('builds gap-free UTC daily details with zeros and partial calendar edges',
version: 1,
scope: { kind: 'overview' },
period,
totals: { validVisits: 3, invalidVisits: 5 },
base: { validVisits: 1, invalidVisits: 2 },
totals: { validVisits: 5, invalidVisits: 3 },
base: { validVisits: 3, invalidVisits: 0 },
domains: []
});
expect(report?.details.version).toBe(2);
expect(report?.details.version).toBe(3);
expect(report?.details.browsers).toEqual([
{ day: '2026-02-28', name: 'Firefox', validVisits: 2, invalidVisits: 1 },
{ day: '2026-05-31', name: 'Firefox', validVisits: 1, invalidVisits: 4 }
{ day: '2026-05-31', name: 'Firefox', validVisits: 3, invalidVisits: 2 }
]);
expect(report?.details.operatingSystems).toEqual([
{ day: '2026-02-28', name: 'Linux', validVisits: 2, invalidVisits: 1 },
{ day: '2026-05-31', name: 'Linux', validVisits: 1, invalidVisits: 4 }
{ day: '2026-05-31', name: 'Linux', validVisits: 3, invalidVisits: 2 }
]);
expect(report?.details.daily).toHaveLength(visitDays(period).length);
expect(report?.details.daily[0]).toEqual({
@@ -109,8 +109,8 @@ test('builds gap-free UTC daily details with zeros and partial calendar edges',
start: '2026-05-31T00:00:00.000Z',
end: '2026-06-01T00:00:00.000Z',
partial: true,
validVisits: 1,
invalidVisits: 4
validVisits: 3,
invalidVisits: 2
});
});
@@ -126,18 +126,18 @@ test('indexes explicit domain and link scopes, including empty links and root, w
{
...overview,
day: '2026-06-01',
validVisits: 8,
invalidVisits: 3,
baseValidVisits: 1,
baseInvalidVisits: 1
validVisits: 9,
invalidVisits: 2,
baseValidVisits: 2,
baseInvalidVisits: 0
},
{
...domain,
day: '2026-06-01',
validVisits: 4,
invalidVisits: 2,
baseValidVisits: 1,
baseInvalidVisits: 1
validVisits: 5,
invalidVisits: 1,
baseValidVisits: 2,
baseInvalidVisits: 0
},
{
...link,
@@ -150,27 +150,27 @@ test('indexes explicit domain and link scopes, including empty links and root, w
{
...root,
day: '2026-06-01',
validVisits: 1,
invalidVisits: 1,
baseValidVisits: 1,
baseInvalidVisits: 1
validVisits: 2,
invalidVisits: 0,
baseValidVisits: 2,
baseInvalidVisits: 0
}
],
browsers: [
{ ...overview, day: '2026-06-01', name: 'Other', validVisits: 8, invalidVisits: 3 },
{ ...domain, day: '2026-06-01', name: 'Firefox', validVisits: 4, invalidVisits: 2 },
{ ...overview, day: '2026-06-01', name: 'Other', validVisits: 9, invalidVisits: 2 },
{ ...domain, day: '2026-06-01', name: 'Firefox', validVisits: 5, invalidVisits: 1 },
{ ...link, day: '2026-06-01', name: 'Chrome', validVisits: 2, invalidVisits: 0 },
{ ...root, day: '2026-06-01', name: 'Firefox', validVisits: 1, invalidVisits: 1 }
{ ...root, day: '2026-06-01', name: 'Firefox', validVisits: 2, invalidVisits: 0 }
],
operatingSystems: [
{ ...overview, day: '2026-06-01', name: 'Other', validVisits: 8, invalidVisits: 3 },
{ ...domain, day: '2026-06-01', name: 'Linux', validVisits: 4, invalidVisits: 2 },
{ ...overview, day: '2026-06-01', name: 'Other', validVisits: 9, invalidVisits: 2 },
{ ...domain, day: '2026-06-01', name: 'Linux', validVisits: 5, invalidVisits: 1 },
{ ...link, day: '2026-06-01', name: 'Linux', validVisits: 2, invalidVisits: 0 },
{ ...root, day: '2026-06-01', name: 'Linux', validVisits: 1, invalidVisits: 1 }
{ ...root, day: '2026-06-01', name: 'Linux', validVisits: 2, invalidVisits: 0 }
]
},
[
{ domain: 'go.example.test', paths: ['/docs', '/empty', '/'] },
{ domain: 'go.example.test', paths: ['/docs', '/empty'] },
{ domain: 'empty.example.test', paths: ['/docs'] }
],
period
@@ -192,22 +192,21 @@ test('indexes explicit domain and link scopes, including empty links and root, w
version: 1,
scope: { kind: 'domain', domain: 'go.example.test' },
period,
totals: { validVisits: 4, invalidVisits: 2 },
base: { validVisits: 1, invalidVisits: 1 },
totals: { validVisits: 5, invalidVisits: 1 },
base: { validVisits: 2, invalidVisits: 0 },
links: [
{ path: '/docs', totals: { validVisits: 2, invalidVisits: 0 } },
{ path: '/empty', totals: { validVisits: 0, invalidVisits: 0 } },
{ path: '/', totals: { validVisits: 1, invalidVisits: 1 } }
{ path: '/empty', totals: { validVisits: 0, invalidVisits: 0 } }
]
});
expect(overviewReport?.summary).toEqual({
version: 1,
scope: { kind: 'overview' },
period,
totals: { validVisits: 8, invalidVisits: 3 },
base: { validVisits: 1, invalidVisits: 1 },
totals: { validVisits: 9, invalidVisits: 2 },
base: { validVisits: 2, invalidVisits: 0 },
domains: [
{ domain: 'go.example.test', totals: { validVisits: 4, invalidVisits: 2 } },
{ domain: 'go.example.test', totals: { validVisits: 5, invalidVisits: 1 } },
{ domain: 'empty.example.test', totals: { validVisits: 0, invalidVisits: 0 } }
]
});
@@ -221,10 +220,21 @@ test('indexes explicit domain and link scopes, including empty links and root, w
{ day: '2026-06-01', name: 'Chrome', validVisits: 2, invalidVisits: 0 }
]);
expect(domainReport?.details.browsers).toEqual([
{ day: '2026-06-01', name: 'Firefox', validVisits: 4, invalidVisits: 2 }
{ day: '2026-06-01', name: 'Firefox', validVisits: 5, invalidVisits: 1 }
]);
expect(base?.summary.totals).toEqual({ validVisits: 2, invalidVisits: 0 });
expect(base?.details.daily.find(({ start }) => start === '2026-06-01T00:00:00.000Z')).toEqual({
start: '2026-06-01T00:00:00.000Z',
end: '2026-06-02T00:00:00.000Z',
partial: false,
validVisits: 2,
invalidVisits: 0
});
expect(base?.details.browsers).toEqual([
{ day: '2026-06-01', name: 'Firefox', validVisits: 2, invalidVisits: 0 }
]);
expect(base?.details.operatingSystems).toEqual([
{ day: '2026-06-01', name: 'Linux', validVisits: 1, invalidVisits: 1 }
{ day: '2026-06-01', name: 'Linux', validVisits: 2, invalidVisits: 0 }
]);
expect(empty?.summary.totals).toEqual({ validVisits: 0, invalidVisits: 0 });
expect(empty?.details.daily).toEqual(
@@ -258,6 +268,7 @@ test('indexes explicit domain and link scopes, including empty links and root, w
);
expect(unconfiguredRoot?.summary.totals).toEqual({ validVisits: 0, invalidVisits: 0 });
for (const report of reports) {
expect(report.details.version).toBe(3);
expect(report.details.scope).toEqual(report.summary.scope);
expect(report.details.period).toEqual(period);
}
@@ -153,7 +153,7 @@ function buildDetails(
days: ReturnType<typeof visitDays>
): VisitDetails {
return {
version: 2,
version: 3,
scope,
period,
daily: buildDailyDetails(counts.daily, days),
@@ -47,6 +47,7 @@ const visit: ClassifiedVisit = {
const overview = { kind: 'overview' as const };
const domain = { kind: 'domain' as const, domain: 'go.example.test' };
const docs = { kind: 'link' as const, domain: 'go.example.test', path: '/docs' };
const root = { ...docs, path: '/' };
function sum(values: VisitTotals[]): VisitTotals {
return values.reduce(
@@ -69,10 +70,16 @@ test('publishes summary/detail pairs for all scopes, deletes expired history and
.execute();
await insertVisit(ctx.db, { ...visit, visited_at: cutoff });
await insertVisit(ctx.db, { ...visit, path: '/', outcome: 'invalid', visited_at: now - 2000 });
await insertVisit(ctx.db, {
...visit,
path: '/missing',
outcome: 'invalid',
visited_at: now - 1000
});
await prepareVisitReports(ctx, now);
const summary = await readVisitSummary(ctx.db, domain);
const details = await readVisitDetails(ctx.db, domain);
expect(summary?.totals).toEqual({ validVisits: 1, invalidVisits: 1 });
expect(summary?.totals).toEqual({ validVisits: 2, invalidVisits: 1 });
expect(summary?.scope).toEqual(domain);
expect(summary?.period.from).toBe('2026-02-28T12:00:00.000Z');
expect(summary?.period.asOf).toBe('2026-05-31T12:00:00.000Z');
@@ -83,17 +90,36 @@ test('publishes summary/detail pairs for all scopes, deletes expired history and
invalidVisits: 0
});
expect(sum(details!.daily)).toEqual(summary?.totals);
expect(details?.version).toBe(2);
expect(details?.version).toBe(3);
expect(details?.browsers).toEqual([
{ day: '2026-02-28', name: 'Firefox', validVisits: 1, invalidVisits: 0 },
{ day: '2026-05-31', name: 'Firefox', validVisits: 0, invalidVisits: 1 }
{ day: '2026-05-31', name: 'Firefox', validVisits: 1, invalidVisits: 1 }
]);
expect(details?.operatingSystems).toEqual([
{ day: '2026-02-28', name: 'Linux', validVisits: 1, invalidVisits: 0 },
{ day: '2026-05-31', name: 'Linux', validVisits: 0, invalidVisits: 1 }
{ day: '2026-05-31', name: 'Linux', validVisits: 1, invalidVisits: 1 }
]);
expect(await ctx.db.selectFrom('analytics_reports').select('id').execute()).toHaveLength(4);
expect(await ctx.db.selectFrom('visits').select('id').execute()).toHaveLength(2);
expect(await ctx.db.selectFrom('visits').select('id').execute()).toHaveLength(3);
const rootDetails = await readVisitDetails(ctx.db, root);
expect((await readVisitSummary(ctx.db, root))?.totals).toEqual({
validVisits: 1,
invalidVisits: 0
});
expect(sum(rootDetails!.daily)).toEqual({ validVisits: 1, invalidVisits: 0 });
expect(rootDetails?.browsers).toEqual([
{ day: '2026-05-31', name: 'Firefox', validVisits: 1, invalidVisits: 0 }
]);
expect(rootDetails?.operatingSystems).toEqual([
{ day: '2026-05-31', name: 'Linux', validVisits: 1, invalidVisits: 0 }
]);
expect(
await ctx.db
.selectFrom('visits')
.select(['path', 'outcome'])
.where('path', '=', '/')
.execute()
).toEqual([{ path: '/', outcome: 'invalid' }]);
await prepareVisitReports(ctx, now);
expect((await readVisitSummary(ctx.db, overview))?.totals).toEqual(summary?.totals);
} finally {
+7 -1
View File
@@ -14,7 +14,7 @@ const summary: OverviewSummary = {
domains: []
};
const details: VisitDetails = {
version: 2,
version: 3,
scope,
period,
daily: visitDays(period).map((day, index) => ({
@@ -30,6 +30,12 @@ test('readers decode their self-contained document independently', () => {
expect(parseVisitDetails(JSON.stringify(details), scope)).toEqual(details);
});
test('details reader rejects version 2 reports with legacy Root classification', () => {
expect(() => parseVisitDetails(JSON.stringify({ ...details, version: 2 }), scope)).toThrow(
'Invalid analytics report'
);
});
test('details reader can require the same reporting policy and window', () => {
expect(parseVisitDetails(JSON.stringify(details), scope, period)).toEqual(details);
const nextPeriod = createVisitPeriod(3, Date.UTC(2026, 3, 2, 12));
+6 -2
View File
@@ -34,7 +34,7 @@ function report(
return {
summary,
details: {
version: 2,
version: 3,
scope,
period,
daily: visitDays(period).map((day, index) => ({
@@ -50,7 +50,11 @@ function report(
}
test('encodes each summary variant and explicit root without changing document shapes', () => {
const reports = [report(overview), report(domain), report(root, asOf, zeroVisitTotals())];
const reports = [
report(overview),
report(domain),
report(root, asOf, { validVisits: 2, invalidVisits: 0 })
];
const rows = encodeVisitReports(reports);
for (const [index, prepared] of reports.entries()) {
+1 -1
View File
@@ -91,7 +91,7 @@ const dailyBrowserOsCounts = Type.Object(
export const VisitDetailsSchema = Type.Object(
{
version: Type.Literal(2),
version: Type.Literal(3),
scope: ReportScopeSchema,
period: ReportPeriodSchema,
daily: Type.Array(
+71 -23
View File
@@ -71,7 +71,7 @@ async function seedVisits(): Promise<void> {
.execute();
}
test('aggregates explicit UTC scope identities, outcomes and root subsets', async () => {
test('aggregates explicit UTC scopes with legacy Root outcomes normalized only in reports', async () => {
await seedVisits();
const result = await aggregateVisitReports(db, since, asOf, inventory);
@@ -93,10 +93,10 @@ test('aggregates explicit UTC scope identities, outcomes and root subsets', asyn
domain: null,
path: null,
day: '2026-02-01',
validVisits: 1,
invalidVisits: 4,
baseValidVisits: 1,
baseInvalidVisits: 2
validVisits: 3,
invalidVisits: 2,
baseValidVisits: 3,
baseInvalidVisits: 0
},
{
scopeKind: 'domain',
@@ -113,10 +113,10 @@ test('aggregates explicit UTC scope identities, outcomes and root subsets', asyn
domain,
path: null,
day: '2026-02-01',
validVisits: 1,
invalidVisits: 2,
baseValidVisits: 1,
baseInvalidVisits: 1
validVisits: 2,
invalidVisits: 1,
baseValidVisits: 2,
baseInvalidVisits: 0
},
{
scopeKind: 'link',
@@ -133,10 +133,10 @@ test('aggregates explicit UTC scope identities, outcomes and root subsets', asyn
domain,
path: '/',
day: '2026-02-01',
validVisits: 1,
invalidVisits: 1,
baseValidVisits: 1,
baseInvalidVisits: 1
validVisits: 2,
invalidVisits: 0,
baseValidVisits: 2,
baseInvalidVisits: 0
},
{
scopeKind: 'domain',
@@ -176,8 +176,8 @@ test('aggregates explicit UTC scope identities, outcomes and root subsets', asyn
path: null,
day: '2026-02-01',
name: 'Firefox',
validVisits: 0,
invalidVisits: 3
validVisits: 2,
invalidVisits: 1
},
{
scopeKind: 'overview',
@@ -214,8 +214,8 @@ test('aggregates explicit UTC scope identities, outcomes and root subsets', asyn
path: null,
day: '2026-02-01',
name: 'Linux',
validVisits: 0,
invalidVisits: 2
validVisits: 2,
invalidVisits: 0
},
{
scopeKind: 'overview',
@@ -254,8 +254,8 @@ test('aggregates explicit UTC scope identities, outcomes and root subsets', asyn
path: '/',
day: '2026-02-01',
name: 'Firefox',
validVisits: 0,
invalidVisits: 1
validVisits: 1,
invalidVisits: 0
},
{
scopeKind: 'link',
@@ -267,6 +267,54 @@ test('aggregates explicit UTC scope identities, outcomes and root subsets', asyn
invalidVisits: 0
}
]);
expect(result.browsers).toEqual(
expect.arrayContaining([
{
scopeKind: 'domain',
domain,
path: null,
day: '2026-02-01',
name: 'Firefox',
validVisits: 1,
invalidVisits: 0
}
])
);
expect(result.operatingSystems).toEqual(
expect.arrayContaining([
{
scopeKind: 'domain',
domain,
path: null,
day: '2026-02-01',
name: 'Linux',
validVisits: 1,
invalidVisits: 0
},
{
scopeKind: 'link',
domain,
path: '/',
day: '2026-02-01',
name: 'Linux',
validVisits: 1,
invalidVisits: 0
}
])
);
expect(
await db
.selectFrom('visits')
.select(['path', 'outcome'])
.where('outcome', '=', 'invalid')
.orderBy('visited_at')
.execute()
).toEqual([
{ path: '/', outcome: 'invalid' },
{ path: '/unconfigured', outcome: 'invalid' },
{ path: '/', outcome: 'invalid' },
{ path: '/docs', outcome: 'invalid' }
]);
expect(
result.operatingSystems.filter(
({ scopeKind, domain: rowDomain }) => scopeKind === 'link' && rowDomain === otherDomain
@@ -304,10 +352,10 @@ test('empty inventory keeps historical overview traffic but no domain or link sc
domain: null,
path: null,
day: '2026-02-01',
validVisits: 1,
invalidVisits: 4,
baseValidVisits: 1,
baseInvalidVisits: 2
validVisits: 3,
invalidVisits: 2,
baseValidVisits: 3,
baseInvalidVisits: 0
}
]);
expect(
+2 -1
View File
@@ -62,7 +62,8 @@ function scopedVisits(since: number, asOf: number, inventory: ReportInventory) {
const currentLink = linkRules.length === 0 ? sql`0` : sql.join(linkRules, sql` OR `);
return sql`
WITH retained_visits AS (
SELECT domain, path, outcome, visited_at, browser, os
SELECT domain, path, CASE WHEN path = '/' THEN 'valid' ELSE outcome END AS outcome,
visited_at, browser, os
FROM visits
WHERE visited_at >= ${since} AND visited_at < ${asOf}
), scoped_visits AS (
@@ -56,7 +56,7 @@ function preparedReport(
return {
summary,
details: {
version: 2,
version: 3,
scope,
period,
daily: visitDays(period).map((day, index) => ({
@@ -75,7 +75,7 @@ test('reads summary and details for each explicit SQL scope including root', asy
const reports = [
preparedReport(overviewScope, asOf, { validVisits: 2, invalidVisits: 1 }),
preparedReport(domainScope, asOf, { validVisits: 1, invalidVisits: 1 }),
preparedReport(rootScope)
preparedReport(rootScope, asOf, { validVisits: 2, invalidVisits: 0 })
];
await publishVisitReports(db, encodeVisitReports(reports));
+1 -1
View File
@@ -113,7 +113,7 @@ function requestPaths(links) {
}
function visitOutcome(prepared, path, visitedAt) {
if (path === '/') return prepared.root ? 'valid' : 'invalid';
if (path === '/') return 'valid';
const link = prepared.links[path];
if (!link || !link.enabled) return 'invalid';
if (link.valid_from !== undefined && visitedAt < Date.parse(link.valid_from)) return 'invalid';
+1 -1
View File
@@ -91,7 +91,7 @@ test('fixed inputs produce repeatable retained traffic with request-time outcome
assert(visit.user_agent.endsWith(' GotoSeed/42'));
assert.deepEqual(classifyUserAgent(visit.user_agent), { browser: visit.browser, os: visit.os });
if (visit.path === '/') {
assert.equal(visit.outcome, visit.domain === 'one.example.test' ? 'valid' : 'invalid');
assert.equal(visit.outcome, 'valid');
}
if (visit.path === '/future') assert.equal(visit.outcome, 'invalid');
}