Obsidian Plugin Error Logging Without Telemetry
Build a local-first error log with bounded retention, secret redaction, and user-initiated reporting for plugin ecosystems that prohibit client telemetry.
Table of Contents12 sections

Crash reporting feels like a solved problem until your distribution platform says the obvious solution is not allowed.
Quick solution:
type ErrorRecord = {
timestamp: string;
level: "error" | "warning";
source: string;
message: string;
stack?: string;
};
const MAX_RECORDS = 50;
function appendRecord(
records: ErrorRecord[],
next: ErrorRecord
): ErrorRecord[] {
return [...records, next].slice(-MAX_RECORDS);
}
Check the current Obsidian community-plugin developer policies before adding any reporting path; this design keeps capture local until the user chooses to share it.
For Obsidian community plugins, that boundary is explicit. The current developer policies state that community plugins must not include client-side telemetry. Network use and server-side telemetry also require disclosure.
That rules out the usual “just add a crash SDK” answer for a plugin intended for the official directory.
You still need production diagnostics, especially for startup and mobile failures. The solution is to separate capture from transmission.
Capture locally. Redact aggressively. Let the user decide whether anything leaves the device.
Start with the policy boundary
The goal is not to imitate a remote crash service secretly.
The goal is to preserve enough local evidence that a user can report a reproducible failure.
A compliant design should have these properties:
- no automatic error upload;
- no hidden analytics endpoint;
- no third-party crash SDK;
- bounded local storage;
- explicit secret redaction;
- user-visible controls to inspect, copy, clear, or report;
- network transmission only after a deliberate user action.
If you are publishing to the Obsidian community directory, read the current developer policies before designing observability.
Use a bounded local ring buffer
A log that grows forever becomes its own reliability problem.
For a plugin, the useful window is usually recent failures, not months of historical console noise.
Store the log in the plugin’s own configuration area rather than scattering debug files through user notes.
Serialize writes
Mobile and startup errors often happen close together. Multiple asynchronous writes can race and overwrite each other.
A tiny write queue is enough:
let writeChain = Promise.resolve();
function persist(records: ErrorRecord[]) {
writeChain = writeChain.then(() =>
savePluginData({ errorLog: records })
);
return writeChain;
}
This is not a database. It simply makes “append error A, then error B” deterministic.
On startup:
- load the existing local log;
- append any new error;
- cap it;
- persist the new snapshot.
A restart should not destroy the exact failure you are trying to diagnose.
Redact before storage, not before upload
If the log is supposed to stay local, it may be tempting to postpone redaction until the user presses Report.
That leaves secrets sitting unmasked on disk.
Redact before the record enters the buffer.
Your redactor should cover values your plugin knows about and common credential shapes:
function redact(input: string, secrets: string[]): string {
let output = input;
for (const secret of secrets) {
if (secret) output = output.split(secret).join("[REDACTED]");
}
output = output.replace(
/Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
"Bearer [REDACTED]"
);
output = output.replace(
/github_pat_[A-Za-z0-9_]+/g,
"[REDACTED_GITHUB_TOKEN]"
);
return output;
}
Do not log note bodies, full sync payloads, or arbitrary request content just because they are available.
The safest diagnostic is usually structural:
- operation name;
- error class;
- status code;
- sanitized message;
- timestamp;
- plugin version;
- platform category.
Test the redactor like a security boundary
An untested redactor is a promise, not a control.
Write tests for:
- exact known secret values;
- Authorization headers;
- token-like patterns;
- stack traces containing query parameters;
- multiline messages;
- empty strings;
- already redacted values.
Also test that ordinary technical context survives. A redactor that turns every path, SHA, and URL into “[REDACTED]” produces a safe but useless report.
Aim for minimum useful evidence, not maximum logging.
Capture global errors carefully
Plugin-specific try/catch blocks are valuable, but some failures escape them.
Where the host lifecycle allows it, register handlers for:
- error;
- unhandledrejection.
Normalize those events into the same ErrorRecord shape.
Just as important: unregister or bind them through the plugin lifecycle so hot reloads and plugin disable/enable cycles do not create duplicate listeners.
The global hook is a safety net, not a replacement for contextual error handling. A caught sync error can say “failed while fetching remote tree”; a global rejection may only know that a promise failed.
Give the user inspection controls
A local log becomes trustworthy when the user can see and erase it.
A small settings section can provide:
- View recent errors;
- Copy sanitized report;
- Clear log;
- Report issue.
Make the retention policy visible. If you keep 50 entries, say so.
This is especially important for privacy-oriented tools. Users should not have to reverse engineer where diagnostic data lives.
Reporting should be user initiated
A practical reporting flow can open a prefilled GitHub issue URL after letting the user preview the sanitized text locally:
const params = new URLSearchParams({
title: "Bug report",
body: buildSanitizedReport(records),
});
window.open(
"https://github.com/example/project/issues/new?" + params.toString()
);
The plugin has not submitted anything at this point. It opened a browser page.
The user can:
- review the text;
- delete anything they do not want to share;
- add reproduction details;
- press Submit.
That human-in-the-loop boundary is fundamentally different from background telemetry.
Be honest about blind spots
A local logger cannot capture everything.
If the plugin fails before its code initializes, its own global handlers may never run. A corrupted manifest or syntax error during loading can occur outside the logger’s lifetime.
Document that limitation.
Useful fallback guidance can tell advanced users how to capture the Obsidian developer console when the plugin does not load far enough to show its own report UI.
Reliability improves when a system has a known boundary rather than claiming parity with a full remote crash platform.
Keep network behavior separate from diagnostics
A plugin may legitimately use the network for its main feature. That does not mean diagnostics should piggyback on those requests.
Keep these concerns separate:
feature network request
!=
error reporting transport
If the plugin syncs files, calls an API, or uses an AI service, disclose that according to platform rules. The local error buffer should not silently convert those connections into a telemetry channel.
The same privacy principle matters for richer workflows: users need to understand when data stays local and when a feature intentionally sends it elsewhere.
Verification and execution checklist
- Read the current directory policy to ensure compliance with telemetry rules.
- Store errors locally only without automatic upload paths.
- Cap the buffer and serialize writes to prevent race conditions during startup.
- Redact secrets before persistence and test the redactor against credentials.
- Verify that issue reporting requires explicit user interaction and review.
Continue Exploring
You Might Also Like

Sonar Duplication in TypeScript Static Sites
Learn how SonarQube scope affects duplication metrics in TypeScript static sites, and why moving repeated records to excluded JSON changes the report without deduplicating repository data.

Obsidian Voice Notes: Record, Transcribe, and Summarize Without Losing the Original
A resilient voice-note workflow for Obsidian that keeps the original recording, transcription, and AI summary as separate recoverable stages.

Preserving Citations in Publishing Pipelines: Typed Metadata and Link Safety
Learn how to preserve citation provenance from source records through generated drafts and schemas to ensure clickable live links.