1.2.0: Error-Group Merging, Email Digests & a UX Audit Pass
A feature release. Duplicate error groups are fixed at the root and auto-merged at ingestion, scheduled email digest reports ship complete and enabled, clock skew detection covers logs, spans and metrics, and a full UX audit lands ~25 fixes and refinements across the dashboard.
- Duplicate error groups solved end to end: fingerprint fix for node: frames, auto-merge at ingestion, and a one-click manual merge for historical splits
- Scheduled email digest reports (#154) are complete and enabled: five report sections, live hourly scheduling, one-click unsubscribe
- Clock skew detection for logs, spans and metrics (#279): misconfigured shipper clocks are surfaced on the project overview, never rejected
- Local login now routes through the auth provider registry (#266, community contribution) with user-enumeration hardening
- UX audit pass: shareable log-search URLs, data-aware command palette, remembered project & time range, System theme, error regression detection and more
- Security bumps clear three Dependabot advisories: echarts 6.1.0, protobufjs 7.6.5, brace-expansion 5.0.7
A feature release with three threads: duplicate error groups are addressed at every layer (root-cause fingerprint fix, automatic merge at ingestion, manual merge for pre-existing splits), the scheduled email digest feature that shipped disabled since 1.0.0-beta is now finished and active, and a UX audit of the whole dashboard lands roughly twenty-five fixes and refinements. Plus clock skew detection for every telemetry signal, a community-contributed auth refactor, and three dependency security bumps.
Added
- Scheduled email digest reports, completed and enabled (#154): the digest feature whose foundation landed in #209 is now finished and active. The report grew to five sections, each with a quiet empty state: log volume with period-over-period trend, top 5 services by error+critical count (engine-agnostic, so TimescaleDB/ClickHouse/MongoDB all work), new error groups first seen in the period, a security summary (windowed detection totals, top triggered Sigma rules, open incident count), and monitor uptime. Emails are real HTML built on the shared template library with a full plaintext fallback, and digest recipients — who may have no account — get a one-click unsubscribe link backed by a public tokenized endpoint. Scheduling was redesigned: a single static hourly
digest-dispatchcron sweeps enabled configs and matches their delivery hour/day at runtime, so schedule changes are live on both queue backends with no worker restart. New session-auth CRUD under/api/v1/digests, a recipient cap via a newdigests.max_recipientscapability, an “Email Digests” settings page and a public/unsubscribepage. Migration053 - Auto-merge duplicate error groups at ingestion: alongside the exact stack fingerprint, each group now has a coarser
merge_key(exception type + normalized message + top application frame). When an incoming exception has no exact fingerprint match but shares amerge_keywith an existing group in the same project, it folds into that group instead of spawning a duplicate — while errors thrown from different sites, or with genuinely different messages once dynamic tokens (numbers, UUIDs, hex, emails) are masked, stay separate. The key is computed by a single Postgres function used by the trigger, the runtime lookup and a one-time backfill of existing groups, so there is no SQL/JS normalization to keep in sync. Migration055 - Merge duplicate error groups manually: for groups that already split before the fingerprint fix below, the error detail page detects other groups with the same exception type and message and offers a one-click “Merge N” that folds them into the current group — exceptions reassigned, occurrence counts summed, affected services unioned, first/last-seen widened, all in one org-scoped transaction. New
GET /api/v1/error-groups/:id/duplicatesandPOST /api/v1/error-groups/:id/merge(membership-checked, cross-org merges refused) - Error regression detection: when an error group marked resolved starts occurring again, it is automatically reopened and its notification is flagged as a regression, with distinct wording in the in-app notification (“Regression: …”), the email subject (“[Regression] …”) and the webhook envelope (high severity plus an
is_regressionfield). Previously a resolved error could recur silently in the resolved bucket - Clock skew detection for logs, spans and metrics (#279): telemetry whose client-supplied timestamp is more than 24 hours in the past or more than 5 minutes ahead of the server clock is counted and surfaced — on the project overview for tenants and in admin ingestion health for operators — but never rejected, so backfilling historical data stays valid. Such logs can never be counted by a threshold alert rule (whose largest window is 24 hours), which previously made a misconfigured shipper look like an alerting bug. Spans are checked on
end_timeon purpose: long-running spans legitimately have old start times, while clock skew shifts both ends together. The ingestion health endpoint reports skew per signal with per-signal banner copy - “Create alert” from an error group and duplicate actions for monitors and alert rules: the error detail page can open the alert builder prefilled from the error (name, error/critical levels, attributed service); the monitoring and alerts lists gained Duplicate actions that open the respective forms prefilled from an existing monitor or the full rule
- Command palette searches your data: typing in the palette now offers “Search logs for …”, “Search errors for …” and — when the text looks like a 16/32-char hex trace id — “Open trace …”, landing on the target pages pre-filled
- Shareable log search URLs: the log search page writes its active filters back to the URL (query, project, service, levels, trace/session id, custom time range), so a search can be bookmarked or sent to a teammate and reopened exactly as filtered
- The selected project and time window are remembered across pages: a shared persisted store carries the last selected project across Metrics, Traces and Monitoring, and the picked time range across Logs and Traces — no more resetting to defaults on every page switch. Deep links with explicit parameters still win
- Custom time range picker: the “Custom” option in the log/trace time-range picker replaced the two native
datetime-localinputs with a proper in-app range calendar plus dedicated From/To time inputs, matching the rest of the UI - Theme “System” option: the theme control is now a Light / Dark / System menu; System follows the OS color scheme and tracks it live
- Log search quality of life: recent query terms shown as one-click chips (last 8, deduplicated), matched terms highlighted in the message column (rendered as plain text segments, never raw HTML), relative-time tooltips on timestamps, and a persisted Cozy/Compact row-density toggle
- Dashboard auto-refresh: the custom dashboard header has an auto-refresh control (off / 30s / 1m / 5m), persisted per browser and paused automatically while editing the layout
- Metrics empty state with copy-ready OTLP setup: the “No metrics found” state now mirrors the traces onboarding panel with Node.js/Python/Go metric-exporter snippets, copy buttons and doc links
Changed
- Local login routed through the auth provider registry (#266, community contribution by @Justy116):
POST /auth/loginnow authenticates via the same provider path used by OIDC and LDAP instead of a parallel implementation, and registration auto-login rides that path too. The local provider gained user-enumeration hardening: the password is verified before the disabled-account check, so a wrong password returns the same generic error whether or not the account is disabled. Disabled-account and SSO-required rejections are now recorded asauth.login_failedaudit events with a reason in the metadata (never in the HTTP response). If auto-login after signup fails transiently, the endpoint returns 201 with the created user instead of a 500 after the user row is committed. Newdocs/architecture/auth.mddocuments the provider architecture - Login failures classified by typed error code, not message strings: the provider path now throws a typed
AuthErrorcarrying anAuthErrorCode, and the login route maps that code to the HTTP status and audit reason — previously rewording a message could silently turn a wrong-password 401 into a 500. A login while the local provider is disabled now returns a clean503instead of a raw 500. The redundant parallelusersService.loginpath was removed - Ingestion health counters no longer depend on the metering toggle (#279):
METERING_ENABLED=falsesilently suppressed everyingestion.*counter, includingingestion.pii_rejected, the safety counter for records dropped because PII masking failed. Those are operational health signals, not usage metering, so they now bypass the toggle
Fixed
- The same error split into several error groups: Node.js runtime frames using the
node:scheme (e.g.node:internal/process/task_queues) were classified as application code, and since async stack traces vary in which internal frames they carry, one logical error could fan out into five or more groups.node:frames are now treated as library code — excluded from the fingerprint and from “Show App Code Only” — so recurrences collapse into a single group - Error groups always showed “unknown” affected service on ClickHouse and MongoDB: service attribution relied on a Postgres lookup into the
logstable, which is empty on non-TimescaleDB backends. The ingestion path now carries the service on theexceptionsrow directly (migration054) and groups previously stuck onunknownrecover as they recur - “Total Logs Today” and “Error Rate” collapsed to the last hour: when the hourly continuous aggregate is not kept warm (refresh policy not running, seeded data, fresh install), the dashboard stats silently lost everything but the last hour. Low-volume organizations now compute today/yesterday from an exact raw count; high-volume instances keep the fast aggregate path
- A bad client-side telemetry key logged the user out on every page view: the global 401 interceptor treated a 401 from any API request as an expired session, but ingestion/telemetry endpoints authenticate with an API key or DSN — so a wrong or revoked DSN key bounced the user to
/loginin a loop. Those endpoints are now excluded from the session-expiry logout - Error-group merge scoped to a single project: error groups are unique per
(organization_id, project_id, fingerprint), but the duplicate list, merge and exception-retag scoped only by organization — so a merge could delete a sibling project’s group or rewrite its exceptions. All three are now scoped to the target group’s project, with cross-project isolation regression tests - Persisted stores survive blocked localStorage: the time-range and current-project stores re-read localStorage on every
get(), so with localStorage unavailable (private mode, quota) a selection never stuck. Both now hold an in-memory source of truth hydrated from localStorage once - Sidebar flashed “Select organization” on a hard reload: the org store now caches the whole selected organization object and hydrates from it immediately, reconciling once the authoritative list loads; logout properly clears the cache
- UX consistency pass: Security, Usage and Metrics now share one segmented time-range component; the Monitoring page’s six native
<select>dropdowns use the sharedSelectcomponent; the Metrics Explorer no longer duplicates the header’s project/time-range selectors; PII rule toggles show a confirmation toast; the projects page tab title reads “Projects” instead of “Dashboard”; the members “Joined” column uses one consistent short-date format with a relative-time tooltip; and the project stat cards show correct trend units (“+1 vs yesterday” for Active Services, percentage points for Error Rate) with accurately labeled windows
Security
- Bumped
echartsto 6.1.0 (GHSA-fgmj-fm8m-jvvx/CVE-2026-45249): versions before 6.1.0 render a raw HTMLseries.data[i].namethrough aninnerHTMLsink in the built-in tooltip of a Lines series, bypassing the automatic HTML escaping. The frontend does not use the Lines series, so LogTide was not exposed in practice - Bumped
protobufjsto 7.6.5 (GHSA-j3f2-48v5-ccww, moderate): affected versions can enter an infinite loop while parsing.protooption syntax (DoS). LogTide never parses untrusted.prototext at runtime, so real exposure was limited; the root pnpm override floor was raised to>=7.6.5 <8 - Bumped
brace-expansionto 5.0.7 (GHSA-3jxr-9vmj-r5cp/CVE-2026-13149, high): affected versions expand consecutive non-expanding{}groups in exponential time, so a ~90-byte input can block the calling thread for minutes (DoS). Pulled in transitively via tooling; the override floor was raised to>=5.0.7