- A compromised lock (failed heartbeat; another process may take it over)
aborts the update holding it, download included, before it publishes,
instead of only being logged.
- A failed publish removes files it had just added when the dataset had none
before, as well as restoring replaced ones from backup: never half a set.
- The watcher reloads hand-placed or externally published files only once
they have held still for the settle interval and passed the row's
validator, so a file still being copied never replaces healthy data.
- Back to write-file-atomic 8, with engines aligned to its range
(^24.15.0 || >=26.0.0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Scheduled and catch-up runs give each dataset 30 minutes, download
included, so a stalled upstream can't hold up the rest or, under croner's
overrun protection, every later run.
- Downloads go through fetchUpstream (project User-Agent, a timeout on the
wait for a response); the body stays bounded by the caller's signal.
- Catch-up asks whether a scheduled run fell between the last check and now,
instead of a 25-hour age that missed a check skipped just after one.
- A remote that names no version (no Last-Modified) is always fetched, not
taken for unchanged; maxmindIdentifier is null when an edition omits it.
- The watcher records a dataset's files as loaded only after the reload
succeeds, so a failed load is retried on their next change.
- write-file-atomic 7, which supports the project's Node >=24 (8 needs 24.15).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxMind and CAIDA each had their own updater with its own lock, state file,
publish step, boot download and timer. One engine (common/dataset-updater.js)
now owns all of that, and each dataset is a row in common/datasets.js that
holds only its own logic: find the newest remote version, fetch or distill
it, validate, reload.
- Locks: proper-lockfile, a heartbeat lock shared by every process, so a
crashed holder's lock goes stale on its own; a booting server waits for a
live holder and loads what it published.
- State: one file per dataset, written with write-file-atomic; the old
.maxmind-update-state.json / .caida-update-state.json are read once, so an
upgrade doesn't re-download.
- Schedule: croner at a fixed time (DATASET_UPDATE_CRON, default 04:30 local)
instead of 24 h from process start, plus a catch-up run at boot for a check
a downtime skipped. DATASET_AUTO_UPDATE (default on) replaces
MAXMIND_AUTO_UPDATE / CAIDA_AUTO_UPDATE, which still apply when it is unset.
- Reload: the engine watches every dataset's files, so a publish by another
process or a hand-placed .mmdb is picked up; MaxMind's own watcher goes.
MaxMind's City and ASN editions now publish together under one version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pino-pretty transport runs in a worker thread. In a loaded parallel
run, a spec that logs shortly before finishing could occasionally be kept
alive by that thread and never exit, stalling the whole suite until the
CI timeout. `pnpm test` now preloads tests/setup.js, which switches the
logger to its synchronous JSON output unless LOG_FORMAT is already set.
Runtime logging is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keep resolver health checks on demand and surface actionable runtime availability failures through the shared logger. Remove the GitHub Actions workflow so transient resolver connectivity does not affect code CI.
Co-authored-by: OpenAI Codex <codex@openai.com>
A beta pack is now en's full skeleton with untranslated values left as
"", so translation review is always a `"" -> text` diff and English left
in place can't masquerade as translated. pnpm i18n-new scaffolds a pack
and its registry line (--privacy / --checklist for the optional files,
which ship finished or not at all); pnpm i18n-sync realigns packs when
en changes. The gate flips accordingly (exact key parity everywhere, ""
legal only in beta main packs) and a Vite plugin prunes empty values at
build time, so shipped chunks carry only translated strings and ""
behaves exactly like an absent key at runtime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A locale can now ship partially translated: registry status drives a Beta
badge in the picker, missing keys walk variant -> base -> en (one chain
definition feeds vue-i18n, the privacy/checklist datasets and the
changelog), and matchLocale gained family matching so pt-PT finds a
pt-BR pack. Changelog history is only required of full locales, removing
the biggest barrier for new-language PRs. tests/locale-packs.test.js is
the hard gate a pack must pass; scripts/i18n-status.js (un-gitignored
for contributors) reports coverage without ever failing. Only visible
behavior change: ?hl= resolves regional tags (?hl=zh-CN -> zh) instead
of dropping to English.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fetch script was gitignored while the data test hard-required its
output, so outside contributors were blocked by a tool they couldn't
see. The script is now committed (pnpm fetch-favicons) and the data
test auto-downloads missing icons on local runs; CI stays offline and
keeps enforcing that PNGs are committed. Also adds a locale-coverage
check for list names.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every geo route now returns an IANA zone name, added on the way out by the
withTimeZone() middleware rather than by any handler — including the private
API pass-through, which needs no new upstream field.
The zone is derived from the latitude/longitude the response itself carries,
not from a second lookup of the same IP. Asking another database would
eventually place the address elsewhere and print a zone that contradicts the
city next to it. Coordinate accuracy measured against MaxMind's own time_zone
over 30k sampled addresses: 99.80% exact, 99.98% correct offset; the rest are
neighbouring zones across a land border that share an offset anyway.
Only the name ships. Geo routes sit behind a 24h edge cache, so a cached UTC
offset would be an hour wrong for everyone between a DST switch and the
entry's expiry — the frontend renders the offset per view instead, reusing
the formatter the analytics path already had.
The field travels the full report path: event payload, builder, schema (as a
typed timeZone kind, not a free string), the /r/:id table, the Markdown
export and the JSON download.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Frontend (@sentry/vue, loaded via build-time-gated dynamic import — no
VITE_SENTRY_DSN_FRONTEND, no Sentry in the bundle; init joins the mount
gate so instrumentation precedes the first render):
- uncaught errors, route-change tracing / Web Vitals, error-only masked
Session Replay, console.error capture fingerprinted per message prefix
so each fallback source stays a distinct issue
- explicit ip-source:exhausted signal via the app-events bus
- hidden source maps uploaded (then deleted) by @sentry/vite-plugin,
gated on SENTRY_AUTH_TOKEN
Backend (@sentry/node, bootstrapped by sentry-instrument.js via
node --import so ESM loader hooks land before express; never loaded
without SENTRY_DSN_BACKEND):
- Express error capture + per-route tracing
- logger hook mirrors warn+ lines to Sentry Logs and elevates error+
to grouped, alertable Issues
- Sentry Crons check-ins for the MaxMind / CAIDA / service-status jobs
via the no-op-without-DSN wrapper common/sentry-cron.js
Delivery & ops:
- first-party tunnel /api/monitoring relays envelopes past ad blockers;
DSN-allowlisted (never an open relay), exempt from the shared rate
limiter with its own generous ceiling
- rate-limit ledger file becomes opt-in now that the warn line reaches
Sentry Logs; ecosystem.config.cjs carries the --import flag for pm2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>