wiki-formant 0.92.0 → 0.94.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 +12 -6
- package/dist/analytics.d.ts +2 -2
- package/dist/analytics.js +7 -2
- package/dist/base.css +14 -0
- package/dist/chart.d.ts +9 -5
- package/dist/chart.js +45 -20
- package/dist/dom.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -957,7 +957,10 @@ An agent is counted as a visitor is, one address and user agent a day, from its
|
|
|
957
957
|
`AI Bot Visit` and `MCP Call` events; it runs no page script, so it is never
|
|
958
958
|
among the visitors, and `visitors_incl_agents` counts the two at once.
|
|
959
959
|
`ownAgents` leaves the site's own out of both. A visit is a run of one
|
|
960
|
-
visitor's views with no gap over 30 minutes
|
|
960
|
+
visitor's views with no gap over 30 minutes, and a bounce is a visit of one view
|
|
961
|
+
with no event of the reader's inside it: a search that keeps the path, a click
|
|
962
|
+
out or a share makes the visit count. A browser under automation
|
|
963
|
+
(`navigator.webdriver`) sends no beacon at all. A multi-tenant app passes a `site`
|
|
961
964
|
key to every call and a `site` resolver to `collect`.
|
|
962
965
|
|
|
963
966
|
Instrumenting the agent lane and not the human one is the easy mistake: it
|
|
@@ -1081,7 +1084,8 @@ which no total makes sense of. A stepped range starts on a whole week or month,
|
|
|
1081
1084
|
so its first point is not a few days of one; the week in progress is its total
|
|
1082
1085
|
so far. `fromZero` starts a count's axis at zero and stops it there, with
|
|
1083
1086
|
no negative ticks under it. A single range draws no range buttons and shows every point.
|
|
1084
|
-
|
|
1087
|
+
No step is finer than the points' own spacing, so weekly readings offer W and M
|
|
1088
|
+
and draw on the days they were taken. `chartSteps` is exported for a chart drawn elsewhere.
|
|
1085
1089
|
|
|
1086
1090
|
`format` writes the axis and the crosshair label. An axis tick also gets the
|
|
1087
1091
|
spacing between ticks as a second argument, so a formatter can write every tick
|
|
@@ -1090,13 +1094,15 @@ decimal places that takes, and `formatChartValue`, the default, uses it.
|
|
|
1090
1094
|
|
|
1091
1095
|
`markers` are events, `{ time, label }`, each drawn as an arrow on the last
|
|
1092
1096
|
point at or before it, so one mid-week lands on its week and two on one point
|
|
1093
|
-
share it.
|
|
1094
|
-
|
|
1095
|
-
|
|
1097
|
+
share it. No words go on the plot: the label is a tooltip (`.chart-tip`) while
|
|
1098
|
+
the crosshair is on the point, one line an event, so it can be the event's full
|
|
1099
|
+
name. `step` opens on a step where the range can make it, and `onView` hears the
|
|
1100
|
+
range, and the step the reader picked (null while the chart picks its own), once
|
|
1101
|
+
the points are in and on every change, for a page that keeps them in its URL:
|
|
1096
1102
|
|
|
1097
1103
|
```tsx
|
|
1098
1104
|
<TimeChart series={days} label="Transactions" aggregate="sum" fromZero
|
|
1099
|
-
markers={[{ time: haltAt, label: '
|
|
1105
|
+
markers={[{ time: haltAt, label: 'Network halt' }]} step="month" onView={v => keep(v)} />
|
|
1100
1106
|
```
|
|
1101
1107
|
`base.css` pins the buttons (`.chart-controls button[aria-pressed]`) to the
|
|
1102
1108
|
canvas's top-left corner, rules a line between groups and dims a disabled step;
|
package/dist/analytics.d.ts
CHANGED
|
@@ -89,7 +89,7 @@ export interface DigestDay {
|
|
|
89
89
|
visitors: number;
|
|
90
90
|
pageviews: number;
|
|
91
91
|
visits: number;
|
|
92
|
-
/** Visits that saw one page. */
|
|
92
|
+
/** Visits that saw one page and did nothing else: a search, a click out or any reader event makes it a visit. */
|
|
93
93
|
bounces: number;
|
|
94
94
|
/** Seconds from each visit's first view to its last, summed. */
|
|
95
95
|
seconds: number;
|
|
@@ -101,7 +101,7 @@ export interface DigestDay {
|
|
|
101
101
|
export interface Digest {
|
|
102
102
|
visitors: number;
|
|
103
103
|
pageviews: number;
|
|
104
|
-
/** Percent of visits that
|
|
104
|
+
/** Percent of visits that are bounces (`DigestDay.bounces`); null with no visits. */
|
|
105
105
|
bounce_rate: number | null;
|
|
106
106
|
/** Mean seconds from a visit's first view to its last; null with no visits. */
|
|
107
107
|
visit_duration: number | null;
|
package/dist/analytics.js
CHANGED
|
@@ -223,6 +223,10 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
223
223
|
), e${s} AS (
|
|
224
224
|
SELECT name, props, visitor, "createdAt" AS at FROM "Event" WHERE "siteId" = $1 AND ${at}
|
|
225
225
|
${tests.length ? `AND visitor IN (SELECT visitor FROM v${s})` : ''}
|
|
226
|
+
), bounced${s} AS (
|
|
227
|
+
SELECT * FROM visits${s} b WHERE views = 1 AND NOT EXISTS (
|
|
228
|
+
SELECT 1 FROM e${s} x WHERE x.visitor = b.visitor AND x.name NOT IN ('AI Bot Visit', 'MCP Call')
|
|
229
|
+
AND x.at >= b.at AND x.at < b.at + interval '30 minutes')
|
|
226
230
|
), ag${s} AS (
|
|
227
231
|
SELECT visitor, at, ${AGENT_NAME} AS agent FROM e${s}
|
|
228
232
|
WHERE name IN ('AI Bot Visit', 'MCP Call') AND visitor IS NOT NULL AND NOT ${ours}
|
|
@@ -230,7 +234,7 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
230
234
|
SELECT * FROM "ViewDay" WHERE "siteId" = $1 AND day >= ${dayAgo(back + 1)}${back ? ` AND day < ${dayAgo(back)}` : ''}${tests.length ? ' AND false' : ''}
|
|
231
235
|
), sessions${s} AS (
|
|
232
236
|
SELECT sum(bounces) AS bounces, sum(visits) AS visits, sum(seconds) AS seconds FROM (
|
|
233
|
-
SELECT count(*)
|
|
237
|
+
SELECT (SELECT count(*) FROM bounced${s}), count(*), coalesce(sum(seconds), 0) FROM visits${s}
|
|
234
238
|
UNION ALL SELECT coalesce(sum(bounces), 0), coalesce(sum(visits), 0), coalesce(sum(seconds), 0) FROM a${s}
|
|
235
239
|
) t(bounces, visits, seconds)
|
|
236
240
|
), archived${s} AS (
|
|
@@ -262,7 +266,8 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
262
266
|
'by_day', ${list(`SELECT date, sum(visitors) AS visitors, sum(pageviews) AS pageviews, sum(visits) AS visits, sum(bounces) AS bounces,
|
|
263
267
|
sum(seconds) AS seconds, sum(agents) AS agents, sum(everyone) AS visitors_incl_agents, sum(follows) AS follow_clicks FROM (
|
|
264
268
|
SELECT ${day('at')}, count(DISTINCT visitor), count(*), 0, 0, 0, 0, 0, 0 FROM v GROUP BY 1
|
|
265
|
-
UNION ALL SELECT ${day('at')}, 0, 0, count(*),
|
|
269
|
+
UNION ALL SELECT ${day('at')}, 0, 0, count(*), 0, coalesce(sum(seconds), 0), 0, 0, 0 FROM visits GROUP BY 1
|
|
270
|
+
UNION ALL SELECT ${day('at')}, 0, 0, 0, count(*), 0, 0, 0, 0 FROM bounced GROUP BY 1
|
|
266
271
|
UNION ALL SELECT ${day('at')}, 0, 0, 0, 0, 0, count(DISTINCT visitor), 0, 0 FROM ag GROUP BY 1
|
|
267
272
|
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 ag) u GROUP BY 1
|
|
268
273
|
${follows ? `UNION ALL SELECT ${day('at')}, 0, 0, 0, 0, 0, 0, 0, count(*) FROM e WHERE ${follows} GROUP BY 1` : ''}
|
package/dist/base.css
CHANGED
|
@@ -614,6 +614,20 @@ pre:hover > .code-copy-btn,
|
|
|
614
614
|
cut the axis to half its labels. */
|
|
615
615
|
.chart-canvas table { table-layout: auto; }
|
|
616
616
|
.chart-status { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; }
|
|
617
|
+
/* An event's name, over the mark the crosshair is on, in the colours of the axis labels. */
|
|
618
|
+
.chart-tip {
|
|
619
|
+
position: absolute;
|
|
620
|
+
z-index: 2;
|
|
621
|
+
max-width: 50%;
|
|
622
|
+
padding: 0.25rem 0.5rem;
|
|
623
|
+
font-size: 0.6875rem;
|
|
624
|
+
line-height: 1.4;
|
|
625
|
+
white-space: pre-line;
|
|
626
|
+
color: var(--ui-on-accent);
|
|
627
|
+
background: var(--ui-accent);
|
|
628
|
+
border-radius: calc(var(--ui-radius) / 2);
|
|
629
|
+
pointer-events: none;
|
|
630
|
+
}
|
|
617
631
|
.chart-controls {
|
|
618
632
|
position: absolute;
|
|
619
633
|
top: 0.5rem;
|
package/dist/chart.d.ts
CHANGED
|
@@ -18,12 +18,12 @@ export type ChartResolution = 'hour' | '4h' | 'day';
|
|
|
18
18
|
export type ChartStep = 'day' | 'week' | 'month';
|
|
19
19
|
/** Points by resolution, from `from` (Unix seconds, 0 for all time), oldest first. */
|
|
20
20
|
export type ChartLoader = (resolution: ChartResolution, from: number) => Promise<readonly ChartPoint[]>;
|
|
21
|
-
/** Something that happened at a time, in Unix seconds,
|
|
21
|
+
/** Something that happened at a time, in Unix seconds, marked on the point that holds it; `label` is its tooltip. */
|
|
22
22
|
export type ChartMarker = {
|
|
23
23
|
time: number;
|
|
24
24
|
label: string;
|
|
25
25
|
};
|
|
26
|
-
/** The range and step a reader has picked, for a page that keeps them in its URL. */
|
|
26
|
+
/** The range and step a reader has picked, for a page that keeps them in its URL; `step` is null while the chart picks its own. */
|
|
27
27
|
export type ChartView = {
|
|
28
28
|
range: ChartRange;
|
|
29
29
|
step: ChartStep | null;
|
|
@@ -32,8 +32,12 @@ export type ChartView = {
|
|
|
32
32
|
export type ChartAggregate = 'last' | 'sum' | 'mean';
|
|
33
33
|
/** Every range, shortest first. A chart fed daily points offers the ones from 30D up. */
|
|
34
34
|
export declare const CHART_RANGES: readonly ChartRange[];
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* A step is offered once it makes four points, and the default is the finest that
|
|
37
|
+
* makes 200 or fewer. None is finer than the points' own `spacing`: weekly readings
|
|
38
|
+
* have no day.
|
|
39
|
+
*/
|
|
40
|
+
export declare function chartSteps(span: number, spacing?: number): {
|
|
37
41
|
steps: ChartStep[];
|
|
38
42
|
fallback: ChartStep;
|
|
39
43
|
};
|
|
@@ -71,7 +75,7 @@ export interface TimeChartProps {
|
|
|
71
75
|
* window links, which load another digest rather than slice this one.
|
|
72
76
|
*/
|
|
73
77
|
controls?: ReactNode;
|
|
74
|
-
/** Events
|
|
78
|
+
/** Events marked on the points that hold them, each named in a tooltip. */
|
|
75
79
|
markers?: readonly ChartMarker[];
|
|
76
80
|
/** The step shown first, where the range can make it. */
|
|
77
81
|
step?: ChartStep;
|
package/dist/chart.js
CHANGED
|
@@ -8,8 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
// loaded on mount, so the page paints without it. Every colour is the canvas
|
|
9
9
|
// box's own `color`, its axis `--chart-axis`, and a candle `--chart-up` or
|
|
10
10
|
// `--chart-down`, each if set. A count's line is curved; a price's is straight,
|
|
11
|
-
// since a curve between two closes draws prices nobody paid. Events are
|
|
12
|
-
// on the point they fall in, so a spike carries the
|
|
11
|
+
// since a curve between two closes draws prices nobody paid. Events are marks
|
|
12
|
+
// on the point they fall in, named in a tooltip over it, so a spike carries the
|
|
13
|
+
// reason for it without words across the plot.
|
|
13
14
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
14
15
|
const DAY = 86_400;
|
|
15
16
|
const RANGE = {
|
|
@@ -29,11 +30,22 @@ const STEPS = ['day', 'week', 'month'];
|
|
|
29
30
|
/** Every range, shortest first. A chart fed daily points offers the ones from 30D up. */
|
|
30
31
|
export const CHART_RANGES = ['24h', '7d', '30d', '90d', '1y', 'all'];
|
|
31
32
|
const DAILY_RANGES = ['30d', '90d', '1y', 'all'];
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* A step is offered once it makes four points, and the default is the finest that
|
|
35
|
+
* makes 200 or fewer. None is finer than the points' own `spacing`: weekly readings
|
|
36
|
+
* have no day.
|
|
37
|
+
*/
|
|
38
|
+
export function chartSteps(span, spacing = DAY) {
|
|
39
|
+
const fine = STEPS.filter(s => STEP[s].span >= spacing * 0.9);
|
|
40
|
+
const finest = fine[0] ?? 'month';
|
|
41
|
+
const steps = STEPS.filter(s => s === finest || (fine.includes(s) && span / STEP[s].span >= 4));
|
|
35
42
|
return { steps, fallback: steps.find(s => span / STEP[s].span <= 200) ?? steps.at(-1) };
|
|
36
43
|
}
|
|
44
|
+
/** The points' own spacing: the median gap, so a missing day or a skipped week does not change it. */
|
|
45
|
+
function spacing(points) {
|
|
46
|
+
const gaps = points.slice(1).map((p, i) => p.time - points[i].time).sort((a, b) => a - b);
|
|
47
|
+
return gaps[Math.floor(gaps.length / 2)] ?? DAY;
|
|
48
|
+
}
|
|
37
49
|
/** The UTC day, Monday or first of the month a time falls in. */
|
|
38
50
|
function bucketStart(time, step) {
|
|
39
51
|
const day = Math.floor(time / DAY);
|
|
@@ -81,7 +93,7 @@ export function bucketChart(points, step, aggregate) {
|
|
|
81
93
|
}
|
|
82
94
|
/**
|
|
83
95
|
* Markers on the points drawn: each on the last point at or before it, so one
|
|
84
|
-
* mid-week sits on its week. Markers on one point share it,
|
|
96
|
+
* mid-week sits on its week. Markers on one point share it, a label a line; one
|
|
85
97
|
* before the first point or after the last point's step is left off.
|
|
86
98
|
*
|
|
87
99
|
* @internal
|
|
@@ -102,7 +114,7 @@ export function placeMarkers(markers, shown, step) {
|
|
|
102
114
|
const time = shown[i].time;
|
|
103
115
|
placed.set(time, [...(placed.get(time) ?? []), m.label]);
|
|
104
116
|
}
|
|
105
|
-
return [...placed].map(([time, labels]) => ({ time, label: labels.join('
|
|
117
|
+
return [...placed].map(([time, labels]) => ({ time, label: labels.join('\n') }));
|
|
106
118
|
}
|
|
107
119
|
const isCandle = (p) => p.open !== undefined && p.high !== undefined && p.low !== undefined;
|
|
108
120
|
/** The decimal places that write every multiple of `step` exactly: two for 0.25, none for 5000. */
|
|
@@ -163,6 +175,9 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
163
175
|
const [step, setStep] = useState(initialStep ?? null);
|
|
164
176
|
const [candles, setCandles] = useState(initialCandles);
|
|
165
177
|
const [loaded, setLoaded] = useState(null);
|
|
178
|
+
// The marker under the crosshair, at the cursor, in the box's pixels.
|
|
179
|
+
const [tip, setTip] = useState(null);
|
|
180
|
+
const placed = useRef(new Map());
|
|
166
181
|
const resolution = RANGE[range].resolution;
|
|
167
182
|
useEffect(() => {
|
|
168
183
|
const el = box.current;
|
|
@@ -227,6 +242,10 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
227
242
|
wickDownColor: down,
|
|
228
243
|
priceFormat,
|
|
229
244
|
});
|
|
245
|
+
api.subscribeCrosshairMove(({ time, point }) => {
|
|
246
|
+
const label = time === undefined || !point ? undefined : placed.current.get(time);
|
|
247
|
+
setTip(label ? { x: point.x, y: point.y, label } : null);
|
|
248
|
+
});
|
|
230
249
|
// The chart cannot be scrolled or zoomed, so every size it takes shows every point,
|
|
231
250
|
// the first included: data drawn before autoSize has measured the box fits nothing.
|
|
232
251
|
api.timeScale().subscribeSizeChange(() => api.timeScale().fitContent());
|
|
@@ -298,9 +317,10 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
298
317
|
});
|
|
299
318
|
if (resolution !== 'day' || !windowed.length)
|
|
300
319
|
return drawn(windowed, [], null);
|
|
301
|
-
const { steps, fallback } = chartSteps(end - windowed[0].time);
|
|
320
|
+
const { steps, fallback } = chartSteps(end - windowed[0].time, spacing(points));
|
|
302
321
|
const chosen = step && steps.includes(step) ? step : fallback;
|
|
303
|
-
|
|
322
|
+
// The points' own step draws them as they are: a Sunday's reading stays on its Sunday.
|
|
323
|
+
if (chosen === steps[0])
|
|
304
324
|
return drawn(windowed, steps, chosen);
|
|
305
325
|
// A stepped range starts on a whole week or month, so its first total is not a few days of one.
|
|
306
326
|
const from = bucketStart(windowed[0].time, chosen);
|
|
@@ -336,24 +356,29 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
336
356
|
const rising = p.value >= (p.open ?? shown[i - 1]?.value ?? p.value);
|
|
337
357
|
return { time: time(p), value: p.volume ?? 0, color: (rising ? c.up : c.down).replace(/[\d.]+\)$/, '0.5)') };
|
|
338
358
|
}));
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
shape: 'arrowDown',
|
|
343
|
-
color: c.ink,
|
|
344
|
-
text: m.label,
|
|
345
|
-
}));
|
|
359
|
+
const marks = placeMarkers(markers ?? [], shown, view?.step ?? null);
|
|
360
|
+
placed.current = new Map(marks.map(m => [m.time, m.label]));
|
|
361
|
+
const drawn = marks.map(m => ({ time: time(m), position: 'aboveBar', shape: 'arrowDown', color: c.ink }));
|
|
346
362
|
c.areaMarkers.setMarkers(asCandles ? [] : drawn);
|
|
347
363
|
c.candleMarkers.setMarkers(asCandles ? drawn : []);
|
|
348
364
|
c.api.timeScale().fitContent();
|
|
349
365
|
}, [ready, source, view, candles, markers]);
|
|
350
|
-
|
|
366
|
+
// Reported once there are points to step, so a step read from a URL is not cleared while they load.
|
|
367
|
+
const shown = !!view;
|
|
368
|
+
const picked = step && step === view?.step ? step : null;
|
|
351
369
|
useEffect(() => {
|
|
352
|
-
|
|
353
|
-
|
|
370
|
+
if (shown)
|
|
371
|
+
onViewRef.current?.({ range, step: picked });
|
|
372
|
+
}, [shown, range, picked]);
|
|
354
373
|
// The steps always show where a range is drawn by day, so the buttons do not jump about; one
|
|
355
374
|
// the range cannot make (any, under 24H and 7D) is disabled.
|
|
356
375
|
const daily = ranges.some(r => RANGE[r].resolution === 'day');
|
|
357
376
|
const status = source && 'error' in source ? source.error : points && !view?.shown.length ? 'No data in this range' : null;
|
|
358
|
-
return (_jsxs("figure", { className: className ? `chart ${className}` : 'chart', children: [_jsx("div", { ref: box, className: "chart-canvas", role: "img", "aria-label": label, style: height ? { height } : undefined }),
|
|
377
|
+
return (_jsxs("figure", { className: className ? `chart ${className}` : 'chart', children: [_jsx("div", { ref: box, className: "chart-canvas", role: "img", "aria-label": label, style: height ? { height } : undefined }), tip && box.current && (
|
|
378
|
+
// Over the cursor, or under it near the top; growing away from the nearer side.
|
|
379
|
+
_jsx("span", { className: "chart-tip", role: "tooltip", style: {
|
|
380
|
+
top: box.current.offsetTop + tip.y,
|
|
381
|
+
...(tip.x < box.current.clientWidth / 2 ? { left: tip.x } : { right: box.current.clientWidth - tip.x }),
|
|
382
|
+
translate: tip.y < 48 ? '0 1rem' : '0 calc(-100% - 0.75rem)',
|
|
383
|
+
}, children: tip.label })), (!ready || (!points && !status)) && _jsx("div", { className: "chart-status skeleton", "aria-hidden": true }), status && _jsx("p", { className: "chart-status", children: status }), (controls || ranges.length > 1 || daily || view?.ohlc) && (_jsxs("div", { className: "chart-controls", children: [controls, ranges.length > 1 && (_jsx("div", { role: "group", "aria-label": "Range", children: ranges.map(r => (_jsx("button", { type: "button", "aria-pressed": r === range, onClick: () => { setRange(r); setStep(null); }, children: RANGE[r].label }, r))) })), daily && (_jsx("div", { role: "group", "aria-label": "Step", children: STEPS.map(s => (_jsx("button", { type: "button", "aria-pressed": s === view?.step, disabled: !view?.steps.includes(s), onClick: () => setStep(s), title: `One point per ${s}`, children: STEP[s].label }, s))) })), view?.ohlc && (_jsx("div", { children: _jsx("button", { type: "button", "aria-pressed": candles, "aria-label": "Candles", title: "Candles", onClick: () => setCandles(c => !c), children: _jsxs("svg", { className: "chart-icon", viewBox: "0 0 12 12", "aria-hidden": true, children: [_jsx("path", { d: "M3.5 1v2M3.5 9v2M8.5 2v3M8.5 9v1", stroke: "currentColor" }), _jsx("rect", { x: "2", y: "3", width: "3", height: "6", rx: "0.5", fill: "currentColor" }), _jsx("rect", { x: "7", y: "5", width: "3", height: "4", rx: "0.5", fill: "currentColor" })] }) }) }))] }))] }));
|
|
359
384
|
}
|
package/dist/dom.js
CHANGED
|
@@ -330,7 +330,10 @@ export function scrollTables(html) {
|
|
|
330
330
|
// ---- analytics beacon -------------------------------------------------------
|
|
331
331
|
let beaconEndpoint = '/api/view';
|
|
332
332
|
let beaconStarted = false;
|
|
333
|
-
|
|
333
|
+
// A browser under automation (Puppeteer, Playwright, Selenium) sets
|
|
334
|
+
// `navigator.webdriver`: a crawler that runs scripts, whose user agent the bot
|
|
335
|
+
// pattern cannot tell from a reader's, so it sends nothing at all.
|
|
336
|
+
const sendBeacon = (body) => navigator.webdriver || navigator.sendBeacon?.(beaconEndpoint, JSON.stringify(body));
|
|
334
337
|
/**
|
|
335
338
|
* Report each page view to the site's own `endpoint`, whose route hands it to
|
|
336
339
|
* `collect` (`wiki-formant/analytics`). Same-origin, so no third-party script
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.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",
|