LogTide
v1.2.0
Released
Feature

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-dispatch cron 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 new digests.max_recipients capability, an “Email Digests” settings page and a public /unsubscribe page. Migration 053
  • 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 a merge_key with 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. Migration 055
  • 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/duplicates and POST /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_regression field). 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_time on 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-local inputs 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/login now 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 as auth.login_failed audit 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. New docs/architecture/auth.md documents the provider architecture
  • Login failures classified by typed error code, not message strings: the provider path now throws a typed AuthError carrying an AuthErrorCode, 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 clean 503 instead of a raw 500. The redundant parallel usersService.login path was removed
  • Ingestion health counters no longer depend on the metering toggle (#279): METERING_ENABLED=false silently suppressed every ingestion.* counter, including ingestion.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 logs table, which is empty on non-TimescaleDB backends. The ingestion path now carries the service on the exceptions row directly (migration 054) and groups previously stuck on unknown recover 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 /login in 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 shared Select component; 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 echarts to 6.1.0 (GHSA-fgmj-fm8m-jvvx / CVE-2026-45249): versions before 6.1.0 render a raw HTML series.data[i].name through an innerHTML sink 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 protobufjs to 7.6.5 (GHSA-j3f2-48v5-ccww, moderate): affected versions can enter an infinite loop while parsing .proto option syntax (DoS). LogTide never parses untrusted .proto text at runtime, so real exposure was limited; the root pnpm override floor was raised to >=7.6.5 <8
  • Bumped brace-expansion to 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