Skip to content

Event logs

HappyView keeps an event log of system activity: lexicon changes, record operations, script runs and errors, operator actions, API key events, backfills and Jetstream connectivity. Events are stored in the database and read from System > Event Logs or the admin API.

Events follow a category.action naming convention. Each event has a severity level (info, warn, or error), an optional actor_did (who triggered it), an optional subject (what was affected), and a detail JSON object with event-specific data.

Event TypeSeveritySubjectDetail
lexicon.createdinfoLexicon NSIDrevision, source
lexicon.updatedinfoLexicon NSIDrevision, source
lexicon.deletedinfoLexicon NSIDdelete_records_job and records_at_delete when its records are deleted too
lexicon.publishedinfoLexicon NSIDdid, uri, cid
lexicon.publish_failedwarnLexicon NSIDerror
lexicon.unpublishedinfoLexicon NSIDdid, uri
lexicon_publishing.rule_created, .rule_updated, .rule_deletedinfoThe rule’s patternrule_id, target_kind, linked_repo_id, auto_publish

The first three are logged when lexicons are uploaded, updated, or deleted via the admin API. The actor_did is the operator who performed the action. The rest come from publishing lexicons. A failure repeating the last error isn’t logged again.

Event TypeSeveritySubjectDetail
project.deployedinfoThe bundle’s sha256changes, backfills

Logged when a project is deployed through PUT /admin/project. A dry run logs nothing. changes and backfills are the ones the deploy answered with.

Event TypeSeveritySubjectDetail
record.createdinfoRecord AT URIcollection, did, rkey
record.deletedinfoRecord AT URIcollection, did, rkey
record.skippedinfoRecord AT URIcollection, did, rkey, reason
record.skip_ignoredwarnRecord AT URIcollection, did, rkey, ignored, reason
record.cid_mismatchwarnRecord AT URIcollection, did, rkey, claimed_cid, reason
record.index_failedwarnRecord AT URIerror
record.unstored_claimedwarnRecord AT URIcollection, did, rkey
record.delete_hook_failedwarnRecord AT URIorigin, job_id, cid, error

Logged when records are received from Jetstream and stored or removed from the local database, or skipped because a record script returned nil. An update is logged as record.created. These are system-triggered events (actor_did is null). They write one row per record, so successful operations are only logged when verbose_event_logging is on. If a database error occurs, the same event type is logged with error severity regardless of that setting, and the error message is included in the detail.

record.unstored_claimed is a record of a collection that an external record storage claims, arriving from somewhere other than that storage; HappyView refuses it. record.delete_hook_failed and the record.deleted events whose origin is happyview.reconcile-collection come from the job that removes records their PDS no longer holds. That job logs every record it deletes, whatever verbose_event_logging says.

Event TypeSeveritySubjectDetail
script.executedinfoMethod NSIDmethod, caller_did, duration_ms, response_size, input, response
script.errorerrorMethod NSIDerror, script_source, input, caller_did, method, duration_ms
script.executedinfoRecord or label AT URIhost_kind, host_id, trigger, attempts
script.dead_letterederrorRecord or label AT URIhost_kind, host_id, trigger, error
script.logas loggedThe script’s triggertrigger, level, message, fields
script.created, script.updatedinfoScript idscript_type
script.deletedinfoScript idnone
script.codemod_appliedinfoScript idscript_type, markers

For XRPC query and procedure scripts, script.executed is written on every call and stores the full request and response, so it is only logged when verbose_event_logging is on. script.error is always logged, with what you need to reproduce the failure: the error message, the complete script source, the input that triggered it, and the caller’s DID.

For record and label scripts, script.executed fires once per record or label that reaches a script, so it too is only logged when verbose_event_logging is on. script.dead_lettered means a script failed all retry attempts; see Dead letters below.

script.log is a line from require("internal.logging"), with the severity the script named. A debug line is never stored.

Event TypeSeveritySubjectDetail
user.createdinfoNew operator’s DIDtemplate, permissions
user.deletedinfoRemoved operator’s idnone
user.bootstrappedinfoBootstrapped operator’s DIDsource when SUPERADMIN_DID seeded them
user.permissions_updatedinfoOperator’s idgranted, revoked
user.super_transferredwarnNew superadmin’s idfrom_user_id, to_user_id
user.super_set_by_platforminfoNew superadmin’s DIDuser_id

These describe operators, the users of the admin API. The user.bootstrapped event is logged when an operator is given superadmin automatically (see Auth: Auto-bootstrap).

Event TypeSeveritySubjectDetail
auth.permission_deniedwarnThe permission namerequired_permission, user_id

Logged when an operator attempts to access an endpoint they don’t have permission for.

Event TypeSeveritySubjectDetail
space.moderator_readinfoSpace AT URIaction, space_id, user_id, and what was read (see below)

Logged when an operator reads a space’s records or blobs through the admin spaces API. action is list_records or get_blob. A list_records event includes the repo and collection filters and the uris returned. A get_blob event includes the blob cid and the repo that references it. Listing spaces and viewing their metadata or members is not logged.

Event TypeSeveritySubjectDetail
api_key.createdinfoKey namekey_prefix, permissions
api_key.revokedinfoKey idnone
Event TypeSeveritySubjectDetail
script_variable.upsertedinfoVariable keynone
script_variable.deletedinfoVariable keynone
Event TypeSeveritySubjectDetail
backfill.startedinfoCollection NSIDjob_id, scope, account_count, source; a gap replay has no subject and adds reason, after_seq and tip_seq
backfill.completedinfoCollection NSIDjob_id, total_records, and total_repos for a crawl or source otherwise (a replay adds bytes_downloaded)
backfill.cancelling, .cancelled, .pausing, .resumedinfononejob_id
backfill.retry_startedinfoCollection NSIDjob_id, source_job_id, and retried_repos or blocks
backfill.discovery_failedwarnCollection NSIDjob_id, collection, error
backfill.units_leftwarnCollection NSIDjob_id, queued_units, reason
backfill.write_failederrorThe job’s idjob_id, write, error

A backfill that fails writes no event of its own: its error is on the backfill, in the admin API and the dashboard. See Backfill for background.

Event TypeSeveritySubjectDetail
job.startedinfoJob typejob_id, job_type
job.pausedinfoJob typejob_id
job.cancelledinfoJob typejob_id
job.completedinfoJob typejob_id, result
job.failederrorJob typejob_id, error
Event TypeSeveritySubjectDetail
jetstream.connectedinfononeurl, protocol, compression, dictionary_id
jetstream.disconnectedwarnnonereason
jetstream.gapwarnnonereason, and more by reason (below)
jetstream.invalid_requesterrornonemessage, retry_in_secs
jetstream.account_deletedinfoAccount DIDpurged, seq
jetstream.account_takendowninfoAccount DIDpurged, seq, status (takendown or suspended)
jetstream.account_inactiveinfoAccount DIDstatus (deactivated, desynchronized or throttled)
jetstream.account_status_mismatchwarnAccount DIDstatus, pds, seq
jetstream.sync_purgedinfoAccount DIDpurged, rev
jetstream.record_invalidwarnAccount DIDuri, seq, error
jetstream.apply_failederrorAccount DIDuri (or kind and, for an account event, status), seq, error

jetstream.connected and jetstream.disconnected are logged when the WebSocket connection to Jetstream is established or lost.

jetstream.gap means events may have been missed. The same gap (same reason and cursor) is logged once per run of the Jetstream consumer. Its reason is one of:

  • cursor_too_old: the saved cursor was past Jetstream’s retention and no saved time could replace it, so HappyView resumed from the live tip. after_seq is the saved seq (the last one known applied), resumed_at_seq the first seq received after it, and message Jetstream’s explanation. When HappyView replays the gap from the Jetstream archive instead, resumed_at_seq is the archive’s sealed tip and replay_job_id names the backfill job that fills the gap.
  • outdated_cursor: Jetstream moved a time cursor older than its retention up to the oldest event it has. message names where it resumed.
  • future_cursor: the saved seq was past Jetstream’s newest, because its seq numbering restarted. HappyView reconnects by the saved witnessed time, which Jetstream translates to the new numbering, or, with none saved, follows the new numbering from the live tip. from_seq is the cursor it asked for.

jetstream.invalid_request means Jetstream refused the subscription as invalid, or kept refusing it without saying why after HappyView dropped what it could from the request. HappyView retries after retry_in_secs (an hour by default).

jetstream.account_deleted is logged when an account this instance holds records for is deleted: its records (other than local writes not yet echoed back) are removed. jetstream.account_takendown is logged the same way for an account that is taken down or suspended. A tombstone is written for every deleted, taken-down or suspended account, but the event is logged only when this instance held, and purged, records for it. The tombstone means a late or replayed copy of the account’s records is refused. jetstream.account_status_mismatch is logged instead when the relay reports an account deleted (status) but the account’s current PDS (pds) says it is active: HappyView keeps its records and writes no tombstone. This happens when the relay is still relaying a PDS the account moved away from. Takedowns and suspensions are never checked this way, because a relay may impose them without the account’s PDS knowing. jetstream.account_inactive is logged when such an account is deactivated, desynchronized or throttled; its records are kept. When an account becomes active again, its tombstone is lifted, but removed records come back only through a new backfill. jetstream.sync_purged is logged when an account’s repo is resynced: its records in registered collections are removed, and the current ones that follow in the stream replace them. Events for accounts this instance holds nothing for are not logged.

jetstream.record_invalid is logged once for an event HappyView skips immediately because retrying cannot help. That is a create or update with no record body, an unknown operation, data the database rejects, or a network lexicon schema that cannot be applied.

jetstream.apply_failed is logged when an event keeps failing for a reason that is not a database outage. HappyView retries it for about 31 seconds, logs this event and skips it, so one bad event does not stall the stream. Database outages are never skipped: see Real-time indexing.

Event TypeSeveritySubjectDetail
api_client.createdinfoClient nameclient_key, client_id_url
api_client.updated, api_client.deletedinfoClient idnone
api_client.secret_createdinfoClient nameclient_id, secret_id, label
api_client.secret_revokedinfoClient nameclient_id, secret_id, active_secrets
api_client.auth_key_rotatedinfoClient idkid, status, activates_at, orphaned_sessions
api_client.auth_key_revokedwarnClient idkid, sessions_destroyed
api_client.auth_keys_revoked_allwarnClient idkids, sessions_destroyed, revoked
oauth.instance_key_rotatedinfononekid, status, activates_at, orphaned_sessions
oauth.instance_key_revokedwarnnonekid, sessions_destroyed
dpop_key.provisionedinfoProvision idclient_key, thumbprint, confidential
dpop_session.createdinfo, or warn when not refreshableClient keyscopes, refreshable
dpop_session.deletedinfoClient keynone
dpop_session.device_deletedinfoClient keysession_id
delegation.account_linkedinfoAccount DIDnone
delegation.account_unlinkedinfoAccount DIDnone
delegation.delegate_addedinfoDelegate’s DIDaccount_did, role
delegation.delegate_removedinfoDelegate’s DIDaccount_did
domain.created, domain.deleted, domain.primary_changedinfoDomain URLid
external_auth.refreshedinfoPlugin idexpires_at
external_auth.refresh_failedwarnPlugin idreason
plugin.added, plugin.reloadedinfoPlugin idurl, or registry (did, name, channel, version); capabilities when the operator consented to them
plugin.secrets_updatedinfoPlugin idnone
plugin.allowed_hosts_updatedinfoPlugin idallowed_hosts
plugin.removedinfoPlugin idnone; one event per plugin a cascading removal takes
plugin.logas loggedPlugin idlevel, message
labeler.added, labeler.deletedinfoLabeler DIDnone
labeler.updatedinfoLabeler DIDstatus
labeler.connectedinfoLabeler DIDdid
labeler.disconnectedwarnLabeler DIDdid, last_seq
service_auth.access_deniederrorMethod NSIDfragment, reason
service_identity.updatedinfononemode
service_entry.deletedinfoEntry idnone
service_entry.plc_syncedinfononemode
verification_method.created, verification_method.deletedinfoFragment idfragment_id
setting.updatedinfoSetting keyvalue, or content_type and size_bytes for the logo
setting.deletedinfoSetting keynone
setup.completedinfononenone
event_logs.purgedwarnThe purge job’s idThe purge’s filter: event_type, category, severity, subject, after, before

Use the admin API to query event logs with filters:

const TOKEN = "hv_..."; // your API key
const headers = { Authorization: `Bearer ${TOKEN}` };
interface Event {
id: string;
event_type: string;
severity: string;
actor_did?: string;
subject?: string;
detail: Record<string, unknown>;
created_at: string;
}
interface EventsResponse {
events: Event[];
cursor?: string;
}
// Get all errors
const errors: EventsResponse = await fetch(
"http://127.0.0.1:3000/admin/events?severity=error",
{ headers },
).then((r) => r.json());
// Get script errors for a specific lexicon
const scriptErrors: EventsResponse = await fetch(
"http://127.0.0.1:3000/admin/events?event_type=script.error&subject=com.example.feed.like",
{ headers },
).then((r) => r.json());
// Get all lexicon-related events
const lexiconEvents: EventsResponse = await fetch(
"http://127.0.0.1:3000/admin/events?category=lexicon",
{ headers },
).then((r) => r.json());
// Paginate through results
const page: EventsResponse = await fetch(
"http://127.0.0.1:3000/admin/events?limit=20&cursor=2026-03-01T11:59:00Z",
{ headers },
).then((r) => r.json());

See the Admin API reference for full parameter documentation.

Event logs are automatically cleaned up after 30 days. Change that with Event Log Retention (days) under System > General, or the EVENT_LOG_RETENTION_DAYS environment variable; the setting wins. A background task runs hourly to delete events older than the retention period. Job logs and stored script performance metrics follow the same period.

Set the value to 0 to disable automatic cleanup and keep logs indefinitely.

See the Configuration reference for the other retention settings.

When a record or label script fails after all retry attempts, the event is stored as a dead letter. You can manage dead letters from the Data > Dead Letters page in the dashboard.

From the dead letters page you can:

  • Retry: replay the stored event through the script (use after fixing the script)
  • Re-index: fetch the record fresh from the PDS and run it through the full indexing pipeline (use when the record may have changed)
  • Dismiss: mark the dead letter as resolved without retrying

Bulk actions are available for selected rows or all entries matching the current filters.