mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
d30b916f9c
Every route is calculated in the browser against the public FOSSGIS hosts, so the server never sees a routing request and nobody has a number: not how many a real trip costs, not how big they are, not how much already goes to a self-hosted engine. Deciding whether TREK could run its own router without that number is guessing at the bill, and the counters have to be collecting before the question comes up. Counters, not a log. A row is a day, a profile, a request kind and an engine kind with totals on it: how many requests, how many waypoints, roughly how far, how many were refused. No query, no coordinate, no route, no user, no trip. Nothing leaves the instance: its own database holds them and its own admin reads them at GET /api/route-usage/summary. The client tallies in memory and flushes in batches — on a timer, past a threshold and when the page is hidden — because a counter that cost a request of its own would measure the load by adding to it. Routing all funnels through one fetch in RouteCalculator, so that is the only place instrumented. On by default and switchable off with route_usage_enabled, unlike the shadow log next door: that one stores what people typed, this one stores four integers about a day.
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import type { RouteUsageEntry, RouteUsageProfile, RouteUsageSurface } from '@trek/shared'
|
|
import { routeUsageApi } from '../../api/routeUsage'
|
|
|
|
/**
|
|
* Counts the routing this browser asks for, and posts the totals in batches.
|
|
*
|
|
* All routing happens client-side against an external engine, so the server never
|
|
* sees a route request and cannot count one. Without a count nobody knows what a
|
|
* self-hosted router would have to carry — how many requests a real trip costs, how
|
|
* big they are, how much already goes to an operator's own engine.
|
|
*
|
|
* Counters only. What goes over the wire is four integers per bucket: how many
|
|
* requests, how many waypoints, how many kilometres, how many failed. No coordinate,
|
|
* no route, no trip, nothing that says where anybody went.
|
|
*
|
|
* Batched on purpose. A counter that cost a request of its own would measure the load
|
|
* by adding to it, so the tally is held in memory and flushed on a timer, when it
|
|
* grows past a threshold, and when the page goes away.
|
|
*/
|
|
|
|
/** Flush when the tally has this many requests in it, whichever comes first. */
|
|
const FLUSH_AFTER_REQUESTS = 25
|
|
/** …or after this long, so a quiet session still reports what it did. */
|
|
const FLUSH_AFTER_MS = 60_000
|
|
/** Guard against a pathological session: drop the tally rather than grow forever. */
|
|
const MAX_BUCKETS = 40
|
|
|
|
type Bucket = Omit<RouteUsageEntry, 'profile' | 'surface' | 'selfHosted'> & {
|
|
profile: RouteUsageProfile
|
|
surface: RouteUsageSurface
|
|
selfHosted: boolean
|
|
}
|
|
|
|
const buckets = new Map<string, Bucket>()
|
|
let pending = 0
|
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
let unloadWired = false
|
|
|
|
function flush(): void {
|
|
if (timer !== null) { clearTimeout(timer); timer = null }
|
|
if (!buckets.size) return
|
|
const entries = [...buckets.values()]
|
|
buckets.clear()
|
|
pending = 0
|
|
// Fire and forget: a failed report is a lost count, not a problem worth telling
|
|
// anybody about. The next flush carries on from an empty tally either way.
|
|
void routeUsageApi.report(entries).catch(() => {})
|
|
}
|
|
|
|
function wireUnload(): void {
|
|
if (unloadWired || typeof document === 'undefined') return
|
|
unloadWired = true
|
|
// `visibilitychange`, not `unload`: the latter no longer fires reliably on mobile,
|
|
// where a session ends by switching apps rather than by closing a tab.
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'hidden') flush()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Records one routing request. Never throws and never awaits: it sits on the hot path
|
|
* of every route the map draws.
|
|
*/
|
|
export function countRoute(sample: {
|
|
profile: RouteUsageProfile
|
|
surface: RouteUsageSurface
|
|
selfHosted: boolean
|
|
waypoints: number
|
|
km: number
|
|
failed: boolean
|
|
}): void {
|
|
try {
|
|
wireUnload()
|
|
const key = `${sample.profile}|${sample.surface}|${sample.selfHosted}`
|
|
const bucket = buckets.get(key)
|
|
if (bucket) {
|
|
bucket.requests += 1
|
|
bucket.waypoints += sample.waypoints
|
|
bucket.km += sample.km
|
|
if (sample.failed) bucket.failed += 1
|
|
} else {
|
|
if (buckets.size >= MAX_BUCKETS) return
|
|
buckets.set(key, {
|
|
profile: sample.profile,
|
|
surface: sample.surface,
|
|
selfHosted: sample.selfHosted,
|
|
requests: 1,
|
|
waypoints: sample.waypoints,
|
|
km: sample.km,
|
|
failed: sample.failed ? 1 : 0,
|
|
})
|
|
}
|
|
pending += 1
|
|
if (pending >= FLUSH_AFTER_REQUESTS) { flush(); return }
|
|
if (timer === null) timer = setTimeout(flush, FLUSH_AFTER_MS)
|
|
} catch { /* counting must never break routing */ }
|
|
}
|
|
|
|
/** Flushes now. Exported for tests and for a deliberate "report before you go". */
|
|
export function flushRouteUsage(): void {
|
|
flush()
|
|
}
|