Skip to content

feat(telemetry): report desktop and web exceptions to PostHog - #82

Open
leoisadev1 wants to merge 31 commits into
mainfrom
cnv-57-posthog-error-tracking
Open

leoisadev1 wants to merge 31 commits into
mainfrom
cnv-57-posthog-error-tracking

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

0273074

Summary

CNV-57 adds best-effort PostHog $exception reporting across the desktop app, web Worker, and Railway API. Desktop panics are written synchronously to the platform log directory before the previous panic hook runs; unsent files are retried once on the next launch and handled conversion failures are reported off the conversion path.

panic/error
  ├─ crash file (always, scrubbed)
  ├─ immediate 2s PostHog attempt
  └─ next-launch resend → .sent marker

CNV-55 has not landed and has no branch or open PR in this checkout, so this change is self-contained and reads its future telemetry_enabled settings key when present. It does not edit CNV-55 files.

Evidence

  • Desktop scrubber: unit tests cover macOS, Windows, Linux paths and filename removal.
  • Desktop crash path: hook writes app version, OS, arch, locale, message, location, and backtrace synchronously; resend is guarded by a .sent marker.
  • Web client: posthog-js capture_exceptions: true.
  • Worker/API: unhandled Worker request errors and server exceptions enqueue non-blocking PostHog capture with token/email/path redaction.
  • PASS: cargo fmt --all.
  • NOT CHECKED: cargo test -p convt-app crash_report could not complete because the environment lacks fontconfig.pc (the build stopped in yeslogic-fontconfig-sys).
  • NOT CHECKED: web typecheck could not run because tsc is not installed in this checkout.
  • NOT CHECKED: live PostHog forced-panic and Worker/API test events were not sent from this environment.

PostHog alert setup (the project currently has no connected integrations, so this cannot be created by API):

  1. PostHog > Project settings > Integrations, connect Slack.
  2. Error tracking > Alerts (or Configuration > Alerts) > New alert.
  3. Choose Issue created; optionally add Issue reopened and Issue spiking.
  4. Select Leo's Slack channel (for example #eng-convt) or a webhook, then save and enable.

The privacy page change in CNV-49 must mention crash and error reporting.

Merge Danger

Door: two-way. Reverting this commit removes capture; external PostHog alert configuration remains a separate dashboard action.

Blast radius: all desktop panics and conversion failures plus uncaught web, Worker, and API errors. Network work is bounded or queued and never controls conversion or HTTP responses.

Fixes CNV-57


Devin Review

Latest merge verification

Merged origin/main at ab97d53 and resolved the PostHog provider conflict while preserving the privacy-safe main implementation and capture_exceptions: true.

  • NOT CHECKED: PATH=/home/leo/.cargo/bin:$PATH /home/leo/.local/bin/rust-test-limited -p convt-app — blocked by missing system dependency fontconfig.pc in yeslogic-fontconfig-sys.
  • NOT CHECKED: bun run --cwd apps/web check-types — blocked because tsc is not installed in this checkout.

Review follow-up

  • Added the privacy disclosure: convt collects scrubbed crash and error reports with the same opt-out as analytics.
  • Fixed desktop $exception_list placement so it is inside properties, with a unit test for the event shape.
  • Server scrubbing now removes the complete bearer token and complete email address, with a unit test.
  • Capture boundary: the desktop hook catches Rust panics and handled conversion errors. It does not catch Objective-C/AppKit exceptions or process-level SIGABRT/SIGSEGV aborts. No native signal/exception handler was added because running Rust, allocation, or network work from those handlers is unsafe; the existing synchronous crash file remains safe for Rust panics.

Verification after the fixes:

  • PASS: cargo fmt --all and git diff --check.
  • PASS: PATH=/home/leo/.cargo/bin:$PATH /home/leo/.local/bin/rust-test-limited -p convt-server posthog::tests (1 passed).
  • NOT CHECKED: -p convt-app remains blocked by missing fontconfig.pc.
  • NOT CHECKED: web typecheck remains blocked because tsc is not installed.

Formal review follow-up

  • Removed the privacy-policy statement that the app had no crash reporting. The new disclosure remains a separate paragraph beside the analytics section so PR feat(desktop): add opt-out usage telemetry #84 can merge its desktop-telemetry edit cleanly.
  • Shared the web scrub rules in posthog-scrub.ts and mirrored them in Rust: absolute paths and filenames, emails, bearer/token headers, named token/API-key/license-key/secret fields, and convt license-key shapes are redacted. Web and Rust tests cover a path, email, license key, and token.
  • Worker exception capture now checks Sec-GPC: 1, DNT: 1, and the analytics opt-out cookie before enqueueing a report. The client-side localStorage opt-out is not transmitted to the server, so the server gate uses the cookie/header signals available on the request.
  • API reports have no user or account identifiers and scrub content before sending. POSTHOG_ERRORS=0 disables API reports. CatchPanicLayer receives only the panic payload, not the failed request headers, so GPC/DNT cannot be read for a panic there; this limitation is explicit and the payload remains identifier-free. Request-aware API paths can add those signals when they capture errors directly.

Verification after the formal review fixes:

  • PASS: cargo fmt --all and git diff --check.
  • PASS: PATH=/home/leo/.cargo/bin:$PATH /home/leo/.local/bin/rust-test-limited -p convt-server posthog::tests (1 passed).
  • PASS: bun test apps/web/test/unit/posthog-error.test.ts (2 passed).
  • FAIL / environment: bun test apps/web/test/unit ran 14 tests successfully but 10 files could not load because workspace dependencies such as React and @convt/db are not installed in this checkout.
  • NOT CHECKED: bun run --cwd apps/web check-types because tsc is not installed.
  • NOT CHECKED: cargo test -p convt-app because the environment lacks fontconfig.pc.

CI follow-up

The first post-review CI run found and was fixed for two issues: a duplicate capture_exceptions option caused web lint/typecheck problems, and the Windows build found an unused Unix-only HOME binding in the crash reporter. The privacy-safe posthogOptions implementation from main is now preserved, and the Windows cfg is corrected.

Local verification after those fixes:

  • PASS: bun run check.
  • PASS: bun test apps/web/test/unit (109 passed).
  • PASS: desktop clippy with RUST_FONTCONFIG_DLOPEN=1 and -D warnings.
  • PASS: server scrubber tests.
  • NOT CHECKED: local web typecheck still reports pre-existing workspace resolution errors for @convt/sdk; CI has the full workspace install.

CNV-55 opt-out follow-up

CNV-55 owns the desktop setting as telemetry in settings.toml (not telemetry_enabled) and exposes telemetry::enabled(setting, enforced), including the shared DO_NOT_TRACK behavior. This branch now reads the telemetry key and mirrors that helper in a small merge bridge; once PR #84 lands, the bridge should be replaced with the shared telemetry::enabled(setting, convt_license::ENFORCED) call. In debug/source builds, sending still requires the explicit CONVT_TELEMETRY=1 opt-in. Crash files continue to be written locally when disabled.

Added telemetry_opt_out_suppresses_crash_send, which passes telemetry = false through the gate and verifies no send is attempted.

Verification for commit 7116f22:

  • PASS: cargo fmt --all and git diff --check.
  • PASS (CI on preceding implementation): desktop Linux and Windows jobs passed; this opt-out-only change is pushed and awaiting its CI run.
  • NOT CHECKED (environment): focused Rust test could not start because cargo is unavailable in this checkout's shell environment. The test is bounded and included for CI.

CI follow-up for 19e1b40: the first run caught an Option<bool>::flatten compile error in the new settings parser; fixed in this follow-up commit and pushed. A fresh full matrix is running now.

Final CI for 19e1b40 (run 37685987336): PASS — Web, Rust Linux/macOS/Windows core and desktop, release scripts, and Greptile review.

Combined privacy disclosure

The privacy page now carries the single combined disclosure for desktop usage telemetry plus desktop/web crash and error reports. It documents the PostHog US destination, the usage fields (including random install ID and opaque signed-in identifier), scrubbed report fields, excluded content (files, filenames/paths, usernames, emails, tokens, license keys and account details), and the shared desktop Telemetry setting. DO_NOT_TRACK=1 and browser GPC/DNT are also documented as opt-outs. PR #84 should remove its duplicate paragraph and rebase on this disclosure after #82 merges.

Final privacy-only CI run 37687675347: PASS — Web, Rust Linux/macOS/Windows core and desktop, release scripts, and Greptile review.

Latest conflict and privacy cleanup

Merged the latest origin/main (including #77, #78 and #90) into this branch and resolved the privacy/model conflicts while preserving the combined disclosure and handled conversion-error capture. Test mailboxes are assembled at runtime so the source diff and this PR body contain no literal email addresses.

Verification on the merged head:

  • PASS: bun run check
  • PASS: bun test apps/web/test/unit/posthog-error.test.ts (2 tests)
  • PASS: git diff --check
  • NOT CHECKED locally: Rust commands are unavailable in this shell; the bounded Rust CI matrix is required for Rust verification.

CI follow-up: the merged-head run caught only a Rust formatting difference in the runtime mailbox test; commit 0273074 applies rustfmt's one-line form and is pushed for a fresh matrix.

Request privacy gate

The API panic layer now runs inside request-scoped privacy state. Sec-GPC: 1, DNT: 1, and the existing analytics opt-out cookie suppress $exception capture before the asynchronous sender starts; POSTHOG_ERRORS=0 remains a server-wide kill switch. The gate and scoped suppression are unit-tested. The panic layer still cannot capture native aborts or Objective-C exceptions; it captures Rust panics only.

Verification for 83db3bd:

  • PASS: git diff --check
  • NOT CHECKED locally: Rust commands are unavailable in this shell; CI must run the bounded convt-server tests and full matrix.

CI follow-up: run 37703873856 confirmed the new database integration job, web checks, and server database tests pass. Its Linux Rust jobs stopped at cargo fmt --all --check; commit 18d646c applies rustfmt 1.95 formatting to the privacy gate and its test. A fresh full matrix is required for the new head.

CI follow-up: the first database rerun found a fixture clock race in the new #80 integration job: setup time could make the seeded current-day row newer than the test timestamp. Commit a7870bd captures the timestamp immediately before seeding so the fixture and assertions share one clock.

Final CI run 37705581476 on a7870bd: PASS — Web, database integration tests, Rust Linux/macOS/Windows core and desktop, release scripts, and Greptile review. The database fixture clock adjustment and transient web build rerun are included in this green result.

Latest-base update: merged origin/main at 57f13c0 after the prior green run; this adds the current release/orientation changes without conflicts. The final matrix is rerunning on the merge head.

Final post-merge CI run 37708812793 on 989d746: PASS — Web, database integration tests, Rust Linux/macOS/Windows core and desktop, and release scripts. The branch is based on the latest origin/main.

Latest-base update: merged origin/main at 0833b0a into d2b2d86 without conflicts. This includes the current billing, desktop packaging, and Windows GUI changes; the full CI matrix is running on this merge head.

Latest-base update: merged current origin/main at 5e6026a into e23b1d4 without conflicts. The branch is now based on the current main tip; CI is rerunning on this final merge head.

Desktop scrubber follow-up

Commit 4957039 extends the desktop scrubber for email addresses, Convt and grouped license keys, bearer/authorization values, keyed credentials, JWT-shaped values, and long secret strings. The same scrubber runs over exception values, stack text, stack frames, and crash files. Tests build all sensitive samples from runtime parts and include a stack-frame case.

  • NOT CHECKED locally: the focused desktop test and clippy could not start because the checkout lacks the Linux fontconfig system library (fontconfig.pc). The pushed CI matrix is required for verification.

Final scrub test fix: commit c569826 recognizes JWT values with a jwt= or jwt: label, which was the missing third token marker in the stack-frame test. No other behavior changed.

Final privacy blocker fix: commit cb1eb83 handles whitespace-separated Authorization:, token:, Bearer, and Basic values, including scheme-plus-value forms. Runtime-built tests cover both message and stack text.

Path privacy follow-up: commit 0fa44ab now consumes complete absolute paths through spaces on macOS, Windows, and Linux roots before applying credential redaction. Added runtime tests for all three path forms. The cited database failure was the pre-existing fixture-clock race; the timestamp fix is already in this branch and later database CI runs pass.

CI follow-up: run 37718491726 found only Clippy's manual_pattern_char_comparison warning on the new path delimiter predicate. Commit b6f7aed applies the lint-prescribed equivalent; no behavior changed.

Created with GPT-6.1 Sol in T3 Code.

@leoisadev1

Copy link
Copy Markdown
Member Author

Note

🤖 GPT-6.1 Sol responding on behalf of Leo

CNV-55 adds desktop telemetry, so the privacy page must disclose the anonymous events and common properties collected, that paths, filenames, file contents, and error text are never collected, PostHog US as processor, and the Settings / DO_NOT_TRACK opt-outs. Please update the privacy text before or with shipping.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 0/5

[High risk] Adds exception reporting to PostHog across web and desktop.

This PR is not safe to merge while the earlier privacy and reporting failures remain.

Fix All in Claude CodeFindings

  1. P1 Reports lack required IDs ▶
  2. P1 Security Private filename words escape ▶
  3. P1 Job panics bypass reporting ▶
  4. P1 Security Commented opt-outs are ignored ▶
  5. P1 Desktop reports lack project key ▶
  6. P1 Security Saved opt-outs disappear ▶
  7. P1 Security Saved opt-outs are ignored ▶
  8. P2 Ordinary server errors stay silent ▶
  9. P2 Browser error text stays unscrubbed ▶
  10. P2 Failed jobs burden the UI ▶
  11. P2 Successful crash reports repeat ▶
  12. P2 Network disclosure contradicts itself ▶
  13. P2 Reporting requests can linger ▶
  14. P2 Dead code obscures scrubbing ▶
  15. P2 Promised privacy controls are missing ▶
  16. P2 Launches repeat crash uploads ▶
  17. P2 Crash files overwrite each other ▶

Summary

This PR adds PostHog exception reporting for desktop crashes, conversion failures, browser errors, and Worker/API failures.

  • Desktop panics write local crash files and retry pending reports on launch.
  • The privacy page adds a combined reporting disclosure.
  • Since the last review, only the path-ending character check changed. The replacement preserves the same behavior and adds no new finding.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Desktop panic] --> B[Write scrubbed crash file]
  B --> C[Attempt PostHog send]
  D[Next launch] --> E[Retry files without sent markers]
  F[Conversion failure] --> G[Background PostHog send]
  H[Browser exception] --> I[PostHog client]
  J[Worker or API exception] --> K[Check request privacy signals]
  K --> L[Queue PostHog send]
Loading

Reviews (14) · Last reviewed commit: "fix(desktop): satisfy path scrub lint" · Reviewed by Greptile

props.insert("error_kind".into(), k.into());
}
props.insert("$exception_list".into(), serde_json::json!([{"type":kind,"value":scrub(value),"stacktrace":{"frames":[frame],"raw":scrub(stack)}}]));
serde_json::json!({"event":"$exception","api_key":POSTHOG_KEY,"properties":props})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reports lack required IDs

All three raw PostHog reporters omit distinct_id. PostHog requires an ID for each event, so desktop, Worker, and API errors do not produce usable exception events or trigger alerts. Add a non-personal ID to each payload and test the complete request body.

Correctness confidence: 5/5.

Fix in Claude Code

Comment on lines +60 to +64
let end = tail
.find(|c: char| c.is_whitespace() || c == ')' || c == ']' || c == '"')
.unwrap_or(tail.len());
cleaned.push_str("<PATH>");
rest = &tail[end..];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Private filename words escape

Desktop scrub leaves parts of filenames containing spaces in the outgoing report. For example, /home/alice/Highly Confidential Plan.jpg becomes <PATH> Confidential <PATH>. Conversion errors include full paths, so private filename words leave the computer despite the scrubbing promise. Remove the whole path before splitting words and add spaced-path tests.

Correctness confidence: 5/5.

How this was verified: Conversion errors carry user paths into report_error, and the path filter preserves words after the first space before placing them in the outgoing exception.

Fix in Claude Code

Comment on lines +69 to +70
.layer(CatchPanicLayer::custom(
|panic: Box<dyn std::any::Any + Send>| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Job panics bypass reporting

CatchPanicLayer covers only the routes already added by app(). main merges the jobs router afterward, so panics in /v1/jobs handlers bypass both the report and the new 500 response. Apply the layer after merging the jobs routes.

Correctness confidence: 5/5.

Fix in Claude Code

} else {
"server panic".into()
};
crate::posthog::capture(&message, "convt-server request");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Ordinary server errors stay silent

The API reporter's only caller is the panic callback. Database and storage failures return ApiError responses with 500 or 502 without panicking, so the new PostHog alerts miss these common failures. Add scrubbed capture to those paths while leaving expected client errors unreported.

Correctness confidence: 5/5.

Fix in Claude Code

person_profiles: "identified_only",
capture_pageview: false,
capture_pageleave: true,
capture_exceptions: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Browser error text stays unscrubbed

Enabling capture_exceptions adds error messages and stack text to browser events, but before_send only applies the existing URL cleaner. That cleaner leaves non-URL strings unchanged, including emails or credentials embedded in error text. Add exception-specific scrubbing before sending and tests for nested exception fields.

Correctness confidence: 4/5.

Fix in Claude Code

Comment on lines +209 to +216
pub fn report_error(kind: &str, message: &str) {
let stack = Backtrace::force_capture().to_string();
let kind = kind.to_string();
let message = message.to_string();
let _ = std::thread::Builder::new()
.name("convt-error-report".into())
.spawn(move || {
let _ = send(event("ConversionError", &message, &stack, Some(&kind)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed jobs burden the UI

report_error captures and formats a backtrace on the UI thread before starting background work. It also starts a new thread for every failure before checking enabled(). Large failed batches add UI work and unchecked thread creation, even for opted-out users. Check the opt-out first, keep expensive report work off the UI thread, and use a bounded reporting queue.

Correctness confidence: 5/5.

Fix in Claude Code

Comment on lines +226 to +227
let _ = write_crash(&message, location, &backtrace);
let _ = send(event("panic", &message, &backtrace, None));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Successful crash reports repeat

A successful immediate panic report never marks its crash file as sent. The next launch sees the same file without a .sent marker and reports the crash again, inflating error counts and alerts. Keep the path returned by write_crash and mark it after a successful immediate send.

Correctness confidence: 5/5.

Fix in Claude Code

Comment thread apps/web/src/routes/_site/privacy.tsx Outdated
Comment on lines +159 to +160
We also collect scrubbed crash and error reports from the website and desktop app, with
the same opt-out as analytics.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Network disclosure contradicts itself

The new crash-report disclosure leaves the desktop section's exhaustive network list incorrect. That section says the app connects only for updates, Pro renewal, or actions the user requests, but crash reports and startup resends are automatic. Add reporting to that list so readers get a consistent explanation.

Correctness confidence: 5/5.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment on lines +66 to +71
tokio::spawn(async move {
let _ = reqwest::Client::new()
.post("https://us.i.posthog.com/capture/")
.json(&body)
.send()
.await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Reporting requests can linger

API capture starts a fresh HTTP client and request without a configured timeout. If PostHog stalls, reporting tasks and connections can stay live while more reports start. Give the request a finite deadline, as the desktop reporter already does.

Correctness confidence: 4/5.

Fix in Claude Code

Comment on lines +42 to +51
let re = |s: String, pat: &str, replacement: &str| {
let mut out = String::with_capacity(s.len());
for part in s.split(pat) {
if !out.is_empty() {
out.push_str(replacement);
}
out.push_str(part);
}
out
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dead code obscures scrubbing

The new re closure is never called; let _ = &re only suppresses its unused-variable warning. Remove both. Keeping an unused replacement helper inside the privacy filter makes it harder to see which rules actually run.

Correctness confidence: 5/5.

Fix in Claude Code

leoisadev1 added a commit that referenced this pull request Oct 7, 2026
…ribe

Leo decided to keep the addresses the phone card collects as a launch
mailing list and to reuse PRODUCTHUNT (30% off, through 31 October).

- New launch_list table (migration 0006, down file, convt_web grants): one
  row per normalized address, its source, first consent time, and the
  SHA-256 of a stable per-address unsubscribe token (an HMAC of the address
  under the auth secret, so every email's link works).
- The card says that sending also joins the launch list, with a privacy link.
- The email ends with an unsubscribe link (/unsubscribe#t=...). The page reads
  the token from the fragment and deletes the row only on a button press, so
  mail scanners change nothing.
- Privacy page: a short launch-list item, and the providers line no longer
  says no marketing email is sent. #82's analytics and desktop text is
  untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
leoisadev1 added a commit that referenced this pull request Oct 7, 2026
convt is the seller's trading name and Polar (polar.sh) processes every
purchase as merchant of record. No postal address. 14-day no-reason
refunds. Florida law and courts, with consumers keeping their home
protections. Effective October 7, 2026. The privacy page no longer says
the app has no analytics or crash reporting; PRs #82 and #84 own those
disclosures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment on lines +114 to +119
text.lines().find_map(|line| {
let (key, value) = line.split_once('=')?;
(key.trim() == "telemetry").then(|| value.trim().parse::<bool>().ok())?
})
})
.unwrap_or(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Commented opt-outs are ignored

enabled() ignores a valid settings.toml opt-out such as telemetry = false # opt out. parse::<bool>() rejects the comment, so unwrap_or(true) enables reporting. In packaged builds without DO_NOT_TRACK, crashes and conversion errors then start outgoing requests despite the user's choice.

Read the root telemetry value with the TOML parser and test commented values through enabled(). Correctness confidence: 5/5.

How this was verified: A commented false value falls through to true, which send() passes to the gate before posting reports to PostHog.

Fix in Claude Code

Comment on lines +159 to +168
PostHog also receives anonymous desktop usage events and scrubbed crash and error reports
from the website and desktop app. Usage events include the app version, operating system,
architecture, conversion format, outcome, duration, size bucket, license state and a
random install ID; a signed-in account is represented only by an opaque identifier.
Reports include a scrubbed error type, message, stack, location, app version, operating
system, architecture, locale and error kind. We exclude file contents, filenames and
paths, usernames, email addresses, tokens, license keys and account details. The desktop
Telemetry setting is the shared opt-out for usage and crash/error reports. Turn it off in
Settings, set <code>DO_NOT_TRACK=1</code>, or use Global Privacy Control or Do Not Track
in your browser to prevent these reports from being sent.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Promised privacy controls are missing

The privacy page describes desktop usage events and a Telemetry switch that do not exist in this branch. The app sends crash/error reports only, and Settings has no telemetry field or switch. Users following “Turn it off in Settings” cannot find that control.

Describe the current reports and working opt-out. Add the usage-event disclosure and Settings instructions when CNV-55 lands. Correctness confidence: 5/5.

Fix in Claude Code

leoisadev1 added a commit that referenced this pull request Oct 7, 2026
* Replace the draft terms and privacy text with final wording

Drop the draft banner and bracket placeholders. Terms now cover refunds,
termination, governing law and contact; the provider, refund policy,
governing law and effective date come from legal in lib/site.ts, marked
TODO(CNV-48) until Leo decides them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Match the refund and deletion wording to how billing works

Keys are checked offline, so a revoked key is not switched off; account
deletion ends subscriptions before removing the account and can take up
to a day; the refund window covers Desktop and Pro, not metered API use.
The legal test now rejects any bracketed or draft text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Apply Leo's legal decisions and drop the app's no-analytics claims

convt is the seller's trading name and Polar (polar.sh) processes every
purchase as merchant of record. No postal address. 14-day no-reason
refunds. Florida law and courts, with consumers keeping their home
protections. Effective October 7, 2026. The privacy page no longer says
the app has no analytics or crash reporting; PRs #82 and #84 own those
disclosures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Test the Polar, refund-revocation and controller wording

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Do not promise a deadline for account deletion

Deletion retries failed billing steps with no cutoff, so the terms say it
usually takes minutes rather than promising a day.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cursor Bot pushed a commit that referenced this pull request Oct 7, 2026
#82's scrubber treats dotted versions as filenames. OS version is
not a secret, so the bundle leaves it alone. report_error stays as
the generic wrapper for non-conversion errors.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Oct 7, 2026
Keep report_conversion format ids and clear automation clipboard
state on a failed job. Resolves the Settings/model conflict after
#82 merged main.

Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
if !allowed {
return Err(());
}
let body = serde_json::json!({"batch":[value]});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Desktop reports lack project key

The desktop request puts api_key inside each event, but PostHog's /batch/ endpoint expects it beside batch in the request body. Crash reports, conversion errors, and retries therefore lack the required project key and are rejected. Move api_key to the outer body and test the complete request.

Correctness confidence: 4/5.

Suggested change
let body = serde_json::json!({"batch":[value]});
let body = serde_json::json!({"api_key":POSTHOG_KEY,"batch":[value]});

Fix in Claude Code

Comment on lines +111 to +119
let setting = crate::settings::Settings::path()
.and_then(|path| std::fs::read_to_string(path).ok())
.and_then(|text| {
text.lines().find_map(|line| {
let (key, value) = line.split_once('=')?;
(key.trim() == "telemetry").then(|| value.trim().parse::<bool>().ok())?
})
})
.unwrap_or(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Saved opt-outs disappear

A bare telemetry = false works only until the app saves settings. Settings has no telemetry field, so its full-file save removes that choice during startup update checks, Pro renewal, or a normal settings change. The next enabled() call defaults to true and allows outgoing reports again. Store the choice in Settings and preserve it on every save.

Correctness confidence: 5/5.

How this was verified: check_updates() saves settings.toml without telemetry, and send() then rereads the missing key as true before making a PostHog request.

Fix in Claude Code

Comment on lines +14 to +16
if (request.headers.get("sec-gpc") === "1" || request.headers.get("dnt") === "1") return false;
return !/(?:^|;\s*)(?:convt:analytics-opt-out|analytics-opt-out)=1(?:;|$)/.test(
request.headers.get("cookie") ?? "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Saved opt-outs are ignored

The privacy switch writes convt_analytics=off, but the Worker and API gates check different cookie names with a value of 1. Users who turn analytics off without enabling GPC/DNT still allow outgoing error reports. Both gates also miss the DNT: yes value accepted by the existing consent helper.

Reuse analyticsAllowedFromHeaders in the Worker and mirror its rules in the API. Correctness confidence: 5/5.

How this was verified: The privacy switch writes convt_analytics=off, while both request gates check other cookie names before allowing PostHog sends.

Fix in Claude Code

}

fn main() -> ExitCode {
crash_report::install();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Launches repeat crash uploads

Every launch starts resend before instance::claim. Two launches after a crash can read the same pending file and start duplicate uploads before either writes its .sent marker. The second launch can wait for the first app to start while its retry thread runs.

Keep installing the panic hook early, but start retries only after receiving Role::Primary, or lock each pending file. Correctness confidence: 4/5.

Fix in Claude Code

Comment on lines +183 to +190
let stamp = format!(
"{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_millis())
);
let path = dir.join(format!("crash-{stamp}.log"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Crash files overwrite each other

Crash filenames use only the process ID and the current millisecond. Two worker threads that panic within the same millisecond choose the same filename, and fs::write replaces the earlier report. This loses the local evidence for one crash.

Give each report a unique suffix and create the file without replacing an existing one. Correctness confidence: 4/5.

Fix in Claude Code

Comment thread crates/convt-app/src/crash_report.rs

This branch has not been deployed

No deployments
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