wiki-formant 0.52.1 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/analytics.d.ts +14 -3
- package/dist/analytics.js +22 -10
- package/dist/crawlers.d.ts +7 -3
- package/dist/crawlers.js +23 -7
- package/dist/http.d.ts +1 -1
- package/dist/stats-figures.js +2 -7
- package/dist/stats.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -392,7 +392,7 @@ different lists.
|
|
|
392
392
|
```ts
|
|
393
393
|
import { trackAiBot, crawlerRules } from 'wiki-formant/crawlers';
|
|
394
394
|
|
|
395
|
-
trackAiBot(request, event, () => import('@/lib/track')); // in proxy.ts:
|
|
395
|
+
trackAiBot(request, event, () => import('@/lib/track')); // in proxy.ts: whether it counted
|
|
396
396
|
const rules = crawlerRules({ allow: '/', disallow, aiAllow });
|
|
397
397
|
```
|
|
398
398
|
|
|
@@ -408,7 +408,7 @@ Every agent group also allows the agent surface — `/api/mcp`, the three `llms`
|
|
|
408
408
|
|
|
409
409
|
`crawlerRules` returns every group robots.txt needs: the wildcard, one per AI crawler, and one for each search engine. Search engines get the HTML site and none of its twins — `/*.md$` always, plus `searchDisallow` — because the `*` group keeps the machine surface open to agents, and for Google a twin is a page it already has. Two origins wrote that group by hand and the third never did, so Google crawled its twins. The `<Link>` prefetch payload (`?_rsc=`) is refused in every group.
|
|
410
410
|
|
|
411
|
-
`trackAiBot(request, event, load)` counts an `AI Bot Visit` inside `event.waitUntil`. `load` is a dynamic import because the tracker pulls in a database client, which a static import would load for every request the proxy sees.
|
|
411
|
+
`trackAiBot(request, event, load)` counts an `AI Bot Visit` inside `event.waitUntil`: a crawler on the roster wherever it goes, and anything but a search engine that asks for what only agents read (a `.md` twin or `Accept: text/markdown`, the `llms` exports, `/openapi.json`, `/.well-known/`), since most agents are on no roster. Its user agent is kept, so `digest` can name it. `load` is a dynamic import because the tracker pulls in a database client, which a static import would load for every request the proxy sees.
|
|
412
412
|
|
|
413
413
|
## IndexNow
|
|
414
414
|
|
|
@@ -778,10 +778,13 @@ and may be a batch; `trackSearch` normalises a search box's free text so
|
|
|
778
778
|
the same question aggregates as one row, and drops an empty one. Pass the request's headers and the
|
|
779
779
|
event joins whoever sent it, person or agent.
|
|
780
780
|
|
|
781
|
-
`digest(sql, { days })` reads a site back as one JSON object: visitors,
|
|
782
|
-
views, bounce rate, visit length, sources (a link's `utm_source`, else the
|
|
783
|
-
referring host), top and entry pages, countries, devices, visitors
|
|
784
|
-
tool calls, and the searches that found nothing.
|
|
781
|
+
`digest(sql, { days })` reads a site back as one JSON object: visitors, agents,
|
|
782
|
+
page views, bounce rate, visit length, sources (a link's `utm_source`, else the
|
|
783
|
+
referring host), top and entry pages, countries, devices, visitors and agents by
|
|
784
|
+
day, the agents by name, MCP tool calls, and the searches that found nothing.
|
|
785
|
+
An agent is counted as a visitor is, one address and user agent a day, from its
|
|
786
|
+
`AI Bot Visit` and `MCP Call` events; it runs no page script, so it is never
|
|
787
|
+
among the visitors. `ownAgents` leaves the site's own out. A visit is a run of one
|
|
785
788
|
visitor's views with no gap over 30 minutes. A multi-tenant app passes a `site`
|
|
786
789
|
key to every call and a `site` resolver to `collect`.
|
|
787
790
|
|
|
@@ -831,8 +834,8 @@ headline figures, each with its change from the window before
|
|
|
831
834
|
site's stylesheet to colour), one of them per day, every non-empty
|
|
832
835
|
ranked list, and `?days=` links between windows, all time among them, pinned in
|
|
833
836
|
the chart's frame as its own buttons are (`TimeChart`'s `controls`). Each
|
|
834
|
-
figure is a button that charts it: visitors, page views, bounce rate,
|
|
835
|
-
length
|
|
837
|
+
figure is a button that charts it: visitors, agents, page views, bounce rate,
|
|
838
|
+
visit length and clicks to X. `digest`'s `by_day` carries every
|
|
836
839
|
one of those per UTC day, a visit on the day it began, so the days add up to the
|
|
837
840
|
totals. A day with no visits has no bounce rate or visit length and is left out
|
|
838
841
|
of those charts rather than drawn as zero. Everything but the figures and the
|
package/dist/analytics.d.ts
CHANGED
|
@@ -67,8 +67,9 @@ export interface DigestOptions {
|
|
|
67
67
|
siblings?: string[];
|
|
68
68
|
gaps?: Gap[];
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* The site's own agents, left out of `agents`, `top_agents` and
|
|
71
|
+
* `agent_tool_calls`, matched inside the user agent. The default is the
|
|
72
|
+
* conformance suites (`<repo>-mcp-test`) and the studio's own tools.
|
|
72
73
|
*/
|
|
73
74
|
ownAgents?: string[];
|
|
74
75
|
/**
|
|
@@ -89,6 +90,7 @@ export interface DigestDay {
|
|
|
89
90
|
bounces: number;
|
|
90
91
|
/** Seconds from each visit's first view to its last, summed. */
|
|
91
92
|
seconds: number;
|
|
93
|
+
agents: number;
|
|
92
94
|
visitors_incl_agents: number;
|
|
93
95
|
/** Clicks on links to the site's X handle; zero without a handle. */
|
|
94
96
|
follow_clicks: number;
|
|
@@ -100,6 +102,13 @@ export interface Digest {
|
|
|
100
102
|
bounce_rate: number | null;
|
|
101
103
|
/** Mean seconds from a visit's first view to its last; null with no visits. */
|
|
102
104
|
visit_duration: number | null;
|
|
105
|
+
/**
|
|
106
|
+
* Agents, counted as visitors are: an AI crawler, an MCP client, or anything
|
|
107
|
+
* else reading the machine surface (`trackAiBot`), one per address and user
|
|
108
|
+
* agent a day, the site's own left out. They run no page script, so they are
|
|
109
|
+
* never among `visitors`.
|
|
110
|
+
*/
|
|
111
|
+
agents: number;
|
|
103
112
|
/** Visitors plus every agent that sent an event, such as an MCP call. */
|
|
104
113
|
visitors_incl_agents: number;
|
|
105
114
|
/**
|
|
@@ -108,7 +117,7 @@ export interface Digest {
|
|
|
108
117
|
* the archive cannot stand in for it: event figures always, all of them
|
|
109
118
|
* when filtered.
|
|
110
119
|
*/
|
|
111
|
-
previous: Record<'visitors' | 'pageviews' | 'bounce_rate' | 'visit_duration' | 'visitors_incl_agents' | 'follow_clicks', number | null>;
|
|
120
|
+
previous: Record<'visitors' | 'pageviews' | 'bounce_rate' | 'visit_duration' | 'agents' | 'visitors_incl_agents' | 'follow_clicks', number | null>;
|
|
112
121
|
/** The first UTC day this site counted itself, before which only `ViewDay` speaks; null with no views. */
|
|
113
122
|
counted_from: string | null;
|
|
114
123
|
top_sources: Ranked<'source', 'visitors'>;
|
|
@@ -125,6 +134,8 @@ export interface Digest {
|
|
|
125
134
|
events: number;
|
|
126
135
|
visitors: number;
|
|
127
136
|
} | null;
|
|
137
|
+
/** Each agent by name: the roster's label, else what its user agent calls itself. */
|
|
138
|
+
top_agents: Ranked<'agent', 'visitors'>;
|
|
128
139
|
agent_tool_calls: Ranked<'tool', 'events'>;
|
|
129
140
|
gaps: Ranked<'query', 'events'>;
|
|
130
141
|
}
|
package/dist/analytics.js
CHANGED
|
@@ -160,6 +160,11 @@ const VISIT_TEST = {
|
|
|
160
160
|
country: v => `bool_or(country = ${v})`,
|
|
161
161
|
device: v => `bool_or(device = ${v})`,
|
|
162
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* An agent's name from its event: the roster's label, else what its user agent
|
|
165
|
+
* calls itself, `compatible; X` or the first product token.
|
|
166
|
+
*/
|
|
167
|
+
const AGENT_NAME = `coalesce(props->>'bot', substring(props->>'ua' from 'compatible; ?([^/;) ]+)'), nullif(split_part(split_part(props->>'ua', '/', 1), ' ', 1), ''), '(unknown)')`;
|
|
163
168
|
/**
|
|
164
169
|
* Everything a report needs about one site's last `days` days, in one query.
|
|
165
170
|
* Visitor figures count distinct visitors, so a person who comes back on
|
|
@@ -181,6 +186,8 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
181
186
|
const follows = handle && `name = 'Outbound Link: Click' AND props->>'url' ILIKE ANY(${$([`%x.com/${handle}%`, `%twitter.com/${handle}%`])}::text[])`;
|
|
182
187
|
const tests = STATS_FILTERS.flatMap(k => (filter[k] ? [VISIT_TEST[k]($(filter[k]))] : []));
|
|
183
188
|
const day = (at) => `to_char(${at} AT TIME ZONE 'UTC', 'YYYY-MM-DD')`;
|
|
189
|
+
/** The site's own agents, as a test on an event's user agent. */
|
|
190
|
+
const ours = `coalesce(props->>'ua', '') ILIKE ANY(${$(ownAgents.map(a => `%${a}%`))}::text[])`;
|
|
184
191
|
const gapSelects = gaps.map(g => {
|
|
185
192
|
const prop = $(g.prop);
|
|
186
193
|
return `(SELECT props->>${prop} AS query, count(*) AS events FROM e
|
|
@@ -210,6 +217,9 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
210
217
|
), e${s} AS (
|
|
211
218
|
SELECT name, props, visitor, "createdAt" AS at FROM "Event" WHERE "siteId" = $1 AND ${at}
|
|
212
219
|
${tests.length ? `AND visitor IN (SELECT visitor FROM v${s})` : ''}
|
|
220
|
+
), ag${s} AS (
|
|
221
|
+
SELECT visitor, at, ${AGENT_NAME} AS agent FROM e${s}
|
|
222
|
+
WHERE name IN ('AI Bot Visit', 'MCP Call') AND visitor IS NOT NULL AND NOT ${ours}
|
|
213
223
|
), a${s} AS (
|
|
214
224
|
SELECT * FROM "ViewDay" WHERE "siteId" = $1 AND day >= ${dayAgo(back + 1)}${back ? ` AND day < ${dayAgo(back)}` : ''}${tests.length ? ' AND false' : ''}
|
|
215
225
|
), sessions${s} AS (
|
|
@@ -226,6 +236,7 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
226
236
|
'pageviews', (SELECT count(*) FROM v${s}) + (SELECT pageviews FROM archived${s}),
|
|
227
237
|
'bounce_rate', (SELECT round(100.0 * bounces / nullif(visits, 0)) FROM sessions${s}),
|
|
228
238
|
'visit_duration', (SELECT round(seconds / nullif(visits, 0)) FROM sessions${s}),
|
|
239
|
+
'agents', (SELECT count(DISTINCT visitor) FROM ag${s}),
|
|
229
240
|
'visitors_incl_agents', (SELECT count(DISTINCT visitor) FROM (SELECT visitor FROM v${s} UNION SELECT visitor FROM e${s}) u) + (SELECT visitors FROM archived${s})`;
|
|
230
241
|
const [row] = (await sql(`WITH ${chain('', 0)}, ${chain('_p', 1)}, src AS (
|
|
231
242
|
SELECT coalesce(source, '(none)') AS source, count(DISTINCT visitor) AS visitors FROM visits GROUP BY 1
|
|
@@ -243,17 +254,18 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
243
254
|
'countries', ${list(merged('country', 'countries', 'SELECT country, count(DISTINCT visitor) FROM v WHERE country IS NOT NULL GROUP BY 1'), 10)},
|
|
244
255
|
'devices', ${list(merged('device', 'devices', 'SELECT device, count(DISTINCT visitor) FROM v GROUP BY 1'))},
|
|
245
256
|
'by_day', ${list(`SELECT date, sum(visitors) AS visitors, sum(pageviews) AS pageviews, sum(visits) AS visits, sum(bounces) AS bounces,
|
|
246
|
-
sum(seconds) AS seconds, sum(agents) AS visitors_incl_agents, sum(follows) AS follow_clicks FROM (
|
|
247
|
-
SELECT ${day('at')}, count(DISTINCT visitor), count(*), 0, 0, 0, 0, 0 FROM v GROUP BY 1
|
|
248
|
-
UNION ALL SELECT ${day('at')}, 0, 0, count(*), count(*) FILTER (WHERE views = 1), coalesce(sum(seconds), 0), 0, 0 FROM visits GROUP BY 1
|
|
249
|
-
UNION ALL SELECT ${day('at')}, 0, 0, 0, 0, 0, count(DISTINCT visitor), 0
|
|
250
|
-
|
|
251
|
-
UNION ALL SELECT
|
|
252
|
-
|
|
257
|
+
sum(seconds) AS seconds, sum(agents) AS agents, sum(everyone) AS visitors_incl_agents, sum(follows) AS follow_clicks FROM (
|
|
258
|
+
SELECT ${day('at')}, count(DISTINCT visitor), count(*), 0, 0, 0, 0, 0, 0 FROM v GROUP BY 1
|
|
259
|
+
UNION ALL SELECT ${day('at')}, 0, 0, count(*), count(*) FILTER (WHERE views = 1), coalesce(sum(seconds), 0), 0, 0, 0 FROM visits GROUP BY 1
|
|
260
|
+
UNION ALL SELECT ${day('at')}, 0, 0, 0, 0, 0, count(DISTINCT visitor), 0, 0 FROM ag GROUP BY 1
|
|
261
|
+
UNION ALL SELECT ${day('at')}, 0, 0, 0, 0, 0, 0, count(DISTINCT visitor), 0 FROM (SELECT at, visitor FROM v UNION ALL SELECT at, visitor FROM e) u GROUP BY 1
|
|
262
|
+
${follows ? `UNION ALL SELECT ${day('at')}, 0, 0, 0, 0, 0, 0, 0, count(*) FROM e WHERE ${follows} GROUP BY 1` : ''}
|
|
263
|
+
UNION ALL SELECT to_char(day, 'YYYY-MM-DD'), visitors, pageviews, visits, bounces, seconds, 0, visitors, 0 FROM a
|
|
264
|
+
) t(date, visitors, pageviews, visits, bounces, seconds, agents, everyone, follows) GROUP BY 1`, undefined, '1')},
|
|
253
265
|
'follow_clicks', ${follows ? `(SELECT json_build_object('events', count(*), 'visitors', count(DISTINCT visitor)) FROM e WHERE ${follows})` : 'NULL'},
|
|
266
|
+
'top_agents', ${list('SELECT agent, count(DISTINCT visitor) AS visitors FROM ag GROUP BY 1', 10)},
|
|
254
267
|
'agent_tool_calls', ${list(`SELECT coalesce(props->>'tool', '(none)') AS tool, count(*) AS events FROM e
|
|
255
|
-
WHERE name = 'MCP Call' AND props->>'method' = 'tools/call'
|
|
256
|
-
AND NOT coalesce(props->>'ua', '') ILIKE ANY(${$(ownAgents.map(a => `%${a}%`))}::text[])
|
|
268
|
+
WHERE name = 'MCP Call' AND props->>'method' = 'tools/call' AND NOT ${ours}
|
|
257
269
|
GROUP BY 1`, 10)},
|
|
258
270
|
'gaps', ${gapSelects.length ? `(SELECT coalesce(json_agg(t), '[]') FROM (${gapSelects.join(' UNION ALL ')}) t)` : `'[]'::json`}
|
|
259
271
|
) AS digest`, ...values));
|
|
@@ -267,7 +279,7 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
267
279
|
for (const k of Object.keys(p))
|
|
268
280
|
p[k] = null;
|
|
269
281
|
else
|
|
270
|
-
p.visitors_incl_agents = p.follow_clicks = null;
|
|
282
|
+
p.agents = p.visitors_incl_agents = p.follow_clicks = null;
|
|
271
283
|
}
|
|
272
284
|
return d;
|
|
273
285
|
}
|
package/dist/crawlers.d.ts
CHANGED
|
@@ -46,16 +46,20 @@ export declare function crawlerRules(opts: {
|
|
|
46
46
|
searchDisallow?: string | string[];
|
|
47
47
|
}): RobotsGroup[];
|
|
48
48
|
/**
|
|
49
|
-
* Count an
|
|
49
|
+
* Count an agent's visit from a proxy, without holding up its response: a
|
|
50
|
+
* crawler on the roster wherever it goes, and anything but a search engine
|
|
51
|
+
* that asks for what only agents read, since most agents are on no roster.
|
|
52
|
+
* The user agent is kept beside the label, so `digest` can name an agent the
|
|
53
|
+
* roster does not know and leave out the site's own.
|
|
50
54
|
*
|
|
51
55
|
* `load` is a dynamic import of the module that records it, because that
|
|
52
56
|
* module pulls in a database client, and a static import would load it for
|
|
53
57
|
* every request the proxy sees, bots or not. Three proxies wrote these lines.
|
|
54
|
-
* Returns the
|
|
58
|
+
* Returns whether the request was counted.
|
|
55
59
|
*/
|
|
56
60
|
export declare function trackAiBot(request: {
|
|
57
61
|
url: string;
|
|
58
62
|
headers: HeaderReader;
|
|
59
63
|
}, event: {
|
|
60
64
|
waitUntil(promise: Promise<unknown>): void;
|
|
61
|
-
}, load: () => Promise<Pick<Tracker, 'trackEvent'>>):
|
|
65
|
+
}, load: () => Promise<Pick<Tracker, 'trackEvent'>>): boolean;
|
package/dist/crawlers.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// crawlers.ts — one roster of AI crawler tokens, for both surfaces that need it.
|
|
2
2
|
//
|
|
3
3
|
// The proxy counts an "AI Bot Visit" by user-agent substring, and `robots.ts`
|
|
4
|
-
// gives each agent its own group. Both read this one roster.
|
|
4
|
+
// gives each agent its own group. Both read this one roster. The proxy also
|
|
5
|
+
// counts any agent reading the machine surface, roster or not.
|
|
5
6
|
//
|
|
6
7
|
// One difference between the two uses is deliberate. `Applebot-Extended`
|
|
7
8
|
// never fetches a page: it is a robots.txt-only token that Applebot consults
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
// Every other token appears in both, because a crawler obeys only its
|
|
12
13
|
// most-specific matching group: an agent with no group of its own falls through
|
|
13
14
|
// to `*` and is granted whatever that grants.
|
|
15
|
+
import { wantsMarkdown } from './http.js';
|
|
14
16
|
/**
|
|
15
17
|
* Order is significant: `detectAiBot` returns the first token the user agent
|
|
16
18
|
* contains, so a token that is a substring of another must come after it.
|
|
@@ -113,16 +115,30 @@ export function crawlerRules(opts) {
|
|
|
113
115
|
];
|
|
114
116
|
}
|
|
115
117
|
/**
|
|
116
|
-
*
|
|
118
|
+
* What only an agent asks for: a markdown twin, an `llms` export, the OpenAPI
|
|
119
|
+
* document, a well-known descriptor. MCP is left out, since "MCP Call" counts it.
|
|
120
|
+
*/
|
|
121
|
+
const AGENT_ONLY = /\.md$|^\/llms[\w-]*\.txt$|^\/openapi\.json$|^\/\.well-known\//;
|
|
122
|
+
/**
|
|
123
|
+
* Count an agent's visit from a proxy, without holding up its response: a
|
|
124
|
+
* crawler on the roster wherever it goes, and anything but a search engine
|
|
125
|
+
* that asks for what only agents read, since most agents are on no roster.
|
|
126
|
+
* The user agent is kept beside the label, so `digest` can name an agent the
|
|
127
|
+
* roster does not know and leave out the site's own.
|
|
117
128
|
*
|
|
118
129
|
* `load` is a dynamic import of the module that records it, because that
|
|
119
130
|
* module pulls in a database client, and a static import would load it for
|
|
120
131
|
* every request the proxy sees, bots or not. Three proxies wrote these lines.
|
|
121
|
-
* Returns the
|
|
132
|
+
* Returns whether the request was counted.
|
|
122
133
|
*/
|
|
123
134
|
export function trackAiBot(request, event, load) {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
const ua = request.headers.get('user-agent') ?? '';
|
|
136
|
+
const bot = detectAiBot(ua);
|
|
137
|
+
const agent = bot !== null ||
|
|
138
|
+
(!!ua && !SEARCH_ENGINES.some(s => ua.includes(s)) && (AGENT_ONLY.test(new URL(request.url).pathname) || wantsMarkdown(request)));
|
|
139
|
+
if (agent) {
|
|
140
|
+
const props = { ...(bot ? { bot } : {}), ua: ua.slice(0, 80) };
|
|
141
|
+
event.waitUntil(load().then(m => m.trackEvent('AI Bot Visit', request.url, props, request.headers)));
|
|
142
|
+
}
|
|
143
|
+
return agent;
|
|
128
144
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export declare function markdownHeaders(lastModified?: string | null, opts?: {
|
|
|
74
74
|
export declare function wantsMarkdown(request: {
|
|
75
75
|
url: string;
|
|
76
76
|
headers: {
|
|
77
|
-
get(name: string): string | null;
|
|
77
|
+
get(name: string): string | null | undefined;
|
|
78
78
|
};
|
|
79
79
|
}): boolean;
|
|
80
80
|
/** The header every response from a content-negotiated URL carries. */
|
package/dist/stats-figures.js
CHANGED
|
@@ -24,6 +24,7 @@ function change(now, before, { points = false, lowerIsBetter = false } = {}) {
|
|
|
24
24
|
}
|
|
25
25
|
const figuresOf = ({ previous: p, ...d }) => [
|
|
26
26
|
{ label: 'Visitors', value: count(d.visitors), delta: change(d.visitors, p.visitors), day: r => r.visitors },
|
|
27
|
+
{ label: 'Agents', value: count(d.agents), delta: change(d.agents, p.agents), day: r => r.agents },
|
|
27
28
|
{ label: 'Page views', value: count(d.pageviews), delta: change(d.pageviews, p.pageviews), day: r => r.pageviews },
|
|
28
29
|
{
|
|
29
30
|
label: 'Bounce rate',
|
|
@@ -39,12 +40,6 @@ const figuresOf = ({ previous: p, ...d }) => [
|
|
|
39
40
|
day: r => (r.visits ? r.seconds / r.visits : null),
|
|
40
41
|
format: duration,
|
|
41
42
|
},
|
|
42
|
-
{
|
|
43
|
-
label: 'With agents',
|
|
44
|
-
value: count(d.visitors_incl_agents),
|
|
45
|
-
delta: change(d.visitors_incl_agents, p.visitors_incl_agents),
|
|
46
|
-
day: r => r.visitors_incl_agents,
|
|
47
|
-
},
|
|
48
43
|
...(d.follow_clicks
|
|
49
44
|
? [
|
|
50
45
|
{
|
|
@@ -57,7 +52,7 @@ const figuresOf = ({ previous: p, ...d }) => [
|
|
|
57
52
|
: []),
|
|
58
53
|
];
|
|
59
54
|
const DAY = 86_400;
|
|
60
|
-
const EMPTY = { date: '', visitors: 0, pageviews: 0, visits: 0, bounces: 0, seconds: 0, visitors_incl_agents: 0, follow_clicks: 0 };
|
|
55
|
+
const EMPTY = { date: '', visitors: 0, pageviews: 0, visits: 0, bounces: 0, seconds: 0, agents: 0, visitors_incl_agents: 0, follow_clicks: 0 };
|
|
61
56
|
/**
|
|
62
57
|
* The figures as `.stats-figures button[aria-pressed]`, each with its change
|
|
63
58
|
* as `.stats-delta[data-better]` for a stylesheet to colour, and the pressed one per
|
package/dist/stats.js
CHANGED
|
@@ -73,7 +73,7 @@ export function Stats({ digest: d, days, filter = {}, ranges = STATS_RANGES }) {
|
|
|
73
73
|
// Each window is another digest, so a link rather than a button, pinned
|
|
74
74
|
// with the chart's own controls.
|
|
75
75
|
const windows = (_jsx("div", { role: "group", "aria-label": "Window", children: ranges.map(n => (_jsx("a", { href: href(n, filter), "aria-current": n === days ? 'page' : undefined, children: windowLabel(n) }, n))) }));
|
|
76
|
-
return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs("a", { href: href(days, { ...filter, [k]: undefined }), title: "Remove", children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, windows: windows }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => href(days, { ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events') })] })] }));
|
|
76
|
+
return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs("a", { href: href(days, { ...filter, [k]: undefined }), title: "Remove", children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, windows: windows }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => href(days, { ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { title: "Agents", rows: rows(d.top_agents, 'agent', 'visitors') }), _jsx(StatsList, { title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events') })] })] }));
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
79
|
* A stats page's door: `children` for a signed-in reader, else a line saying
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|