Skip to content

feat: semantic logging - edit attribution, debounce, vpath resolution - #29

Draft
petergaultney wants to merge 2 commits into
No-Instructions:mainfrom
TrilliantHealth:thds/upstream-pr-logging
Draft

petergaultney wants to merge 2 commits into
No-Instructions:mainfrom
TrilliantHealth:thds/upstream-pr-logging

Conversation

@petergaultney

Copy link
Copy Markdown

Summary

Adds an opt-in semantic_logging mode (default off) that gives self-hosters visibility into who is editing what, without exposing user-identifying information unless explicitly configured.

When semantic_logging = true in relay.toml:

  • Edit attribution: each content change is attributed to the user who made it (via PUD map resolution), not the connection that delivered it. Attribution comes only from an update's newly-created blocks; a pure deletion names no actor (see "Deletion attribution" below).
  • Keystroke debounce: typing runs collapse into one logged burst rather than one line per keystroke. The first edit logs immediately; a summary with totals (edits, bytes, duration) follows once typing stops.
  • Vpath resolution: log lines show the note's vault-relative path (e.g. vpath=daily/2026-08-14.md) instead of raw doc GUIDs.
  • Folder membership changes: adds, removes, and renames within a shared folder are logged with the paths. Adds and renames name the acting user; removals carry deleted_name/deleted_user/deleted_clients - the owner of the removed entries, which is the only identity a deletion contains.
  • Large deletion detection: updates that grow the delete set by 5000+ clock units are flagged - targets mass content reverts without alerting on normal editing.
  • /client-versions endpoint: returns every known client-id-to-version binding as JSON. Server-auth required (Bearer token). Only mounted when semantic_logging is on.
  • Display names: a [server.user_names] table maps relay user ids to human-readable names for log lines. Only consulted when semantic_logging is on.

When semantic_logging = false (default), the server behaves identically to the base (PR #23) - no names, no vpaths, no edit lines, no endpoint.

Deletion attribution

A yjs update records who wrote blocks, never who deleted them: a deletion is a delete set of (client, clock) ranges, and those client ids are the ids of the deleted blocks' authors. Reading the delete set as "the actor" therefore blames the original writer for someone else's deletion - one person's bulk cleanup of stale files was logged as three different users, each the file's last writer, before this was caught.

So the rules here are:

  • Actors (user=/name=/clients=) are resolved from newly-created blocks only. A move (remove + insert in one transaction) still attributes to the mover through its inserted block.
  • A delete-only update logs user=-: the deleting client is genuinely not present in the update.
  • Membership removals additionally log deleted_user/deleted_name/deleted_clients, naming whose entries were removed.
  • The captured first-load identity is used as a fallback only for updates that delete nothing, since a deletion's actor is unknown rather than merely unregistered.

New modules

  • edit_bursts.rs - keystroke collapse with configurable quiet period (10s default)
  • vpath_index.rs - cached GUID-to-path resolution from folder filemeta_v0 maps

Configuration

[server]
semantic_logging = true

[server.user_names]
"abc123" = "Ada Lovelace"

Relationship to the other open PRs

Depends on #23, which is the one real dependency in this set: client_versions.declare() takes the display name added there, AwarenessEntryFacts is defined there, and the edit-attribution path reads the PUD registrations it introduces. Cross-fork branches can't be a GitHub base, so this targets main and the diff includes #23's commit - review only feat: semantic logging - edit attribution, debounce, vpath resolution, and /client-versions.

PR Contains Independent?
#26 log volume: demotions, connection lifecycle logs, identity on warns yes
#23 behavior: version gating, cid declarations, PUD suppression yes
#24 (this one) semantic logging, gated on a config flag that defaults to off no - needs #23
#25 attributed-content endpoint yes

This PR changes no log levels; the demotions it once carried are in #26.

Test plan

  • Verify no edit/vpath/name logging when semantic_logging is false (default)
  • Verify Doc edited lines with correct vpath and user attribution when enabled
  • Verify keystroke debounce collapses a typing run into leading + summary
  • Verify folder membership changes log adds/removes/renames
  • Verify removing another client's entry logs user=- and names the entry's owner under deleted_user (regression-tested in edit_author.rs)
  • Verify /client-versions returns 404 when semantic_logging is false
  • Verify /client-versions requires server auth when enabled
  • Verify large deletion detection fires at 5000+ clock span
  • 296 tests pass

…pression

Three behavioral additions, all backward compatible:

1. allowed_client_versions (relay.toml [server] field): when non-empty,
   doc websocket connections must report one of the listed plugin versions
   via the `v` query param. Server tokens are exempt. Denial warns are
   throttled to one per user per 30 minutes.

2. cid query param + relayClientIds awareness field: a plugin can
   pre-declare the Yjs client ids it mints (e.g. a merge working copy),
   binding them to its reported version at connect time. Also registers
   declared ids in PUD at upgrade, so the authenticated user is bound
   before any update can mis-attribute them.

3. Server-only update suppression: updates authored entirely by the
   server's own 53-bit client ids (PUD registration writes) skip the
   normal event callback path, suppressing the webhook dispatch that
   would otherwise fire for internal bookkeeping.

Supporting infrastructure:
- client_versions.rs: per-client-id version tracking with declared
  (owner-minted) and inferred (first-introduction) trust levels
- edit_author.rs: client-id extraction from updates, is_server_only_update
- AwarenessEntryFacts struct on DocConnection for typed awareness parsing
- PUD registration made public so the cid upgrade path can call it
- doc_id on DocConnection, so its log lines can name the doc
…, and /client-versions

Adds a `semantic_logging` flag (default false) that enables operational
visibility features for self-hosters who want to know who edited what:

- Edit attribution: resolves the PUD map to name the user behind each
  update, not just the connection that delivered it.
- Debounce: collapses keystroke runs into one logged edit burst, so
  typing a sentence produces one "Doc edited" line instead of dozens.
- Vpath resolution: translates doc GUIDs to vault-relative file paths
  in log lines, so an operator reads "notes/standup.md" instead of a
  73-character GUID.
- Folder membership logging: reports adds, removes, and renames as they
  happen, with the user who made the change.
- Large deletion detection: logs when an update grows the delete set by
  5000+ clock units.
- /client-versions endpoint: server-auth-gated JSON of every known
  client id, its plugin version, and its display name.
- user_names config: an id->name map in relay.toml for log readability.

All of these are gated on `semantic_logging = true`. With the default
false, the server behaves identically to the base behavioral PR.
dtkav pushed a commit that referenced this pull request Aug 31, 2026
The event callback's captured user describes whichever connection first
loaded the doc, not the edit in hand. The doc itself holds the truth:
PermanentUserData records client_id -> user, and an update names the
clients it came from.

Lifted from #29 into y-sweet-core (rather
than relay) so the doc's own update observer can reach it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant