<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged plugin-development</title>
<link>https://coderlegion.com/tag/plugin-development</link>
<description>Powered by Question2Answer</description>
<item>
<title>Obsidian Plugin Error Logging Without Telemetry</title>
<link>https://coderlegion.com/31519/obsidian-plugin-error-logging-without-telemetry</link>
<description>&lt;p&gt;Crash reporting feels like a solved problem until your distribution platform says the obvious solution is not allowed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick solution:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type ErrorRecord = {
  timestamp: string;
  level: &quot;error&quot; | &quot;warning&quot;;
  source: string;
  message: string;
  stack?: string;
};

const MAX_RECORDS = 50;

function appendRecord(
  records: ErrorRecord[],
  next: ErrorRecord
): ErrorRecord[] {
  return [...records, next].slice(-MAX_RECORDS);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the current &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.obsidian.md/community-directory/developer-policies&quot;&gt;Obsidian community-plugin developer policies&lt;/a&gt; before adding any reporting path; this design keeps capture local until the user chooses to share it.&lt;/p&gt;
&lt;p&gt;For &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/publish-obsidian-community-plugin/&quot;&gt;Obsidian community&lt;/a&gt; plugins, that boundary is explicit. The current developer policies state that community plugins must not include client-side telemetry. Network use and &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/monitoring-server-side-fcm-notifications-effectively/&quot;&gt;server-side&lt;/a&gt; telemetry also require disclosure.&lt;/p&gt;
&lt;p&gt;That rules out the usual &quot;just add a crash SDK&quot; answer for a plugin intended for the official directory.&lt;/p&gt;
&lt;p&gt;You still need production diagnostics, especially for startup and mobile failures. The solution is to separate &lt;strong&gt;capture&lt;/strong&gt; from &lt;strong&gt;transmission&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Capture locally. Redact aggressively. Let the user decide whether anything leaves the device.&lt;/p&gt;
&lt;h3 id=&quot;start-with-the-policy-boundary&quot;&gt;Start with the policy boundary&lt;/h3&gt;
&lt;p&gt;The goal is not to imitate a remote crash service secretly.&lt;/p&gt;
&lt;p&gt;The goal is to preserve enough local evidence that a user can report a reproducible failure.&lt;/p&gt;
&lt;p&gt;A compliant design should have these properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no automatic error upload;&lt;/li&gt;
&lt;li&gt;no hidden analytics endpoint;&lt;/li&gt;
&lt;li&gt;no third-party crash SDK;&lt;/li&gt;
&lt;li&gt;bounded local storage;&lt;/li&gt;
&lt;li&gt;explicit secret redaction;&lt;/li&gt;
&lt;li&gt;user-visible controls to inspect, copy, clear, or report;&lt;/li&gt;
&lt;li&gt;network transmission only after a deliberate user action.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are publishing to the Obsidian community directory, read the current developer policies before designing observability.&lt;/p&gt;
&lt;h3 id=&quot;use-a-bounded-local-ring-buffer&quot;&gt;Use a bounded local ring buffer&lt;/h3&gt;
&lt;p&gt;A log that grows forever becomes its own reliability problem.&lt;/p&gt;
&lt;p&gt;For a plugin, the useful window is usually recent failures, not months of historical console noise.&lt;/p&gt;
&lt;p&gt;Store the log in the plugin's own configuration area rather than scattering debug files through user notes.&lt;/p&gt;
&lt;h3 id=&quot;serialize-writes&quot;&gt;Serialize writes&lt;/h3&gt;
&lt;p&gt;Mobile and startup errors often happen close together. Multiple asynchronous writes can race and overwrite each other.&lt;/p&gt;
&lt;p&gt;A tiny write queue is enough:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;let writeChain = Promise.resolve();

function persist(records: ErrorRecord[]) {
  writeChain = writeChain.then(() =&amp;gt;
    savePluginData({ errorLog: records })
  );
  return writeChain;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not a database. It simply makes &quot;append error A, then error B&quot; deterministic.&lt;/p&gt;
&lt;p&gt;On startup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;load the existing local log;&lt;/li&gt;
&lt;li&gt;append any new error;&lt;/li&gt;
&lt;li&gt;cap it;&lt;/li&gt;
&lt;li&gt;persist the new snapshot.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A restart should not destroy the exact failure you are trying to diagnose.&lt;/p&gt;
&lt;h3 id=&quot;redact-before-storage-not-before-upload&quot;&gt;Redact before storage, not before upload&lt;/h3&gt;
&lt;p&gt;If the log is supposed to stay local, it may be tempting to postpone redaction until the user presses Report.&lt;/p&gt;
&lt;p&gt;That leaves secrets sitting unmasked on disk.&lt;/p&gt;
&lt;p&gt;Redact &lt;strong&gt;before the record enters the buffer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Your redactor should cover values your plugin knows about and common credential shapes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function redact(input: string, secrets: string[]): string {
  let output = input;

  for (const secret of secrets) {
    if (secret) output = output.split(secret).join(&quot;[REDACTED]&quot;);
  }

  output = output.replace(
    /Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
    &quot;Bearer [REDACTED]&quot;
  );

  output = output.replace(
    /github_pat_[A-Za-z0-9_]+/g,
    &quot;[REDACTED_GITHUB_TOKEN]&quot;
  );

  return output;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not log note bodies, full sync payloads, or arbitrary request content just because they are available.&lt;/p&gt;
&lt;p&gt;The safest diagnostic is usually structural:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;operation name;&lt;/li&gt;
&lt;li&gt;error class;&lt;/li&gt;
&lt;li&gt;status code;&lt;/li&gt;
&lt;li&gt;sanitized message;&lt;/li&gt;
&lt;li&gt;timestamp;&lt;/li&gt;
&lt;li&gt;plugin version;&lt;/li&gt;
&lt;li&gt;platform category.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;test-the-redactor-like-a-security-boundary&quot;&gt;Test the redactor like a security boundary&lt;/h3&gt;
&lt;p&gt;An untested redactor is a promise, not a control.&lt;/p&gt;
&lt;p&gt;Write tests for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;exact known secret values;&lt;/li&gt;
&lt;li&gt;Authorization headers;&lt;/li&gt;
&lt;li&gt;token-like patterns;&lt;/li&gt;
&lt;li&gt;stack traces containing query parameters;&lt;/li&gt;
&lt;li&gt;multiline messages;&lt;/li&gt;
&lt;li&gt;empty strings;&lt;/li&gt;
&lt;li&gt;already redacted values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also test that ordinary technical context survives. A redactor that turns every path, SHA, and URL into &quot;[REDACTED]&quot; produces a safe but useless report.&lt;/p&gt;
&lt;p&gt;Aim for &lt;strong&gt;minimum useful evidence&lt;/strong&gt;, not maximum logging.&lt;/p&gt;
&lt;h3 id=&quot;capture-global-errors-carefully&quot;&gt;Capture global errors carefully&lt;/h3&gt;
&lt;p&gt;Plugin-specific try/catch blocks are valuable, but some failures escape them.&lt;/p&gt;
&lt;p&gt;Where the host lifecycle allows it, register handlers for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;error;&lt;/li&gt;
&lt;li&gt;unhandledrejection.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Normalize those events into the same ErrorRecord shape.&lt;/p&gt;
&lt;p&gt;Just as important: unregister or bind them through the plugin lifecycle so hot reloads and plugin disable/enable cycles do not create duplicate listeners.&lt;/p&gt;
&lt;p&gt;The global hook is a safety net, not a replacement for contextual error handling. A caught sync error can say &quot;failed while fetching remote tree&quot;; a global rejection may only know that a promise failed.&lt;/p&gt;
&lt;h3 id=&quot;give-the-user-inspection-controls&quot;&gt;Give the user inspection controls&lt;/h3&gt;
&lt;p&gt;A local log becomes trustworthy when the user can see and erase it.&lt;/p&gt;
&lt;p&gt;A small settings section can provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;View recent errors;&lt;/li&gt;
&lt;li&gt;Copy sanitized report;&lt;/li&gt;
&lt;li&gt;Clear log;&lt;/li&gt;
&lt;li&gt;Report issue.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make the retention policy visible. If you keep 50 entries, say so.&lt;/p&gt;
&lt;p&gt;This is especially important for privacy-oriented tools. Users should not have to reverse engineer where diagnostic data lives.&lt;/p&gt;
&lt;h3 id=&quot;reporting-should-be-user-initiated&quot;&gt;Reporting should be user initiated&lt;/h3&gt;
&lt;p&gt;A practical reporting flow can open a prefilled GitHub issue URL after letting the user preview the sanitized text locally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const params = new URLSearchParams({
  title: &quot;Bug report&quot;,
  body: buildSanitizedReport(records),
});

window.open(
  &quot;https://github.com/example/project/issues/new?&quot; + params.toString()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin has not submitted anything at this point. It opened a browser page.&lt;/p&gt;
&lt;p&gt;The user can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;review the text;&lt;/li&gt;
&lt;li&gt;delete anything they do not want to share;&lt;/li&gt;
&lt;li&gt;add reproduction details;&lt;/li&gt;
&lt;li&gt;press Submit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That human-in-the-loop boundary is fundamentally different from background telemetry.&lt;/p&gt;
&lt;h3 id=&quot;be-honest-about-blind-spots&quot;&gt;Be honest about blind spots&lt;/h3&gt;
&lt;p&gt;A local logger cannot capture everything.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Document that limitation.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Reliability improves when a system has a known boundary rather than claiming parity with a full remote crash platform.&lt;/p&gt;
&lt;h3 id=&quot;keep-network-behavior-separate-from-diagnostics&quot;&gt;Keep network behavior separate from diagnostics&lt;/h3&gt;
&lt;p&gt;A plugin may legitimately use the network for its main feature. That does not mean diagnostics should piggyback on those requests.&lt;/p&gt;
&lt;p&gt;Keep these concerns separate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;feature network request
    !=
error reporting transport
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The same privacy principle matters for richer workflows: users need to understand when data stays local and when a feature intentionally sends it elsewhere.&lt;/p&gt;
&lt;h3 id=&quot;verification-and-execution-checklist&quot;&gt;Verification and execution checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Read the current directory policy to ensure compliance with telemetry rules.&lt;/li&gt;
&lt;li&gt;Store errors locally only without automatic upload paths.&lt;/li&gt;
&lt;li&gt;Cap the buffer and serialize writes to prevent race conditions during startup.&lt;/li&gt;
&lt;li&gt;Redact secrets before persistence and test the redactor against credentials.&lt;/li&gt;
&lt;li&gt;Verify that issue reporting requires explicit user interaction and review.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31519/obsidian-plugin-error-logging-without-telemetry</guid>
<pubDate>Sat, 17 Oct 2026 16:43:33 +0000</pubDate>
</item>
</channel>
</rss>