Skip to content

Add opt-in PostHog analytics with a fixed privacy boundary - #168

Merged
dantheuber merged 14 commits into
mainfrom
t3code/setup-privacy-safe-posthog
Sep 12, 2026
Merged

dantheuber merged 14 commits into
mainfrom
t3code/setup-privacy-safe-posthog

Conversation

@dantheuber

@dantheuber dantheuber commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Summary

Clipless now asks once in the main window whether to send optional metrics to improve the app. Reporting stays off until the user explicitly chooses Send analytics. No thanks and Escape decline, and either saved choice prevents future prompts. Existing consent is respected.

Added

  • A main-window consent dialog with clear privacy wording, keyboard focus on No thanks, and a way to continue if saving fails. Settings → General → Privacy retains the Send analytics toggle and short explanation.
  • Active-installation reporting and feature counts for Quick Look, history search, window clip copies, quick-clip hotkeys, template copies, and tool launch actions.
  • A fixed payload boundary: main-process validation permits only the six predefined feature categories. No clip content, tool/template names, search queries or patterns, URLs, IDs, dynamic properties, or configuration is accepted.
  • Installation-local consent and a random ID excluded from backups. Opt-out stops sending, aborts requests and removes the ID. Development builds never send or prompt.
  • Privacy and consent tests, real Electron prompt/layout tests, and documentation for PostHog adoption/frequency insights.

Changed

  • Direct PostHog capture API calls from main only; no analytics SDK, replay, autocapture, remote configuration, or error reporting.
  • Privacy panel layout keeps the explanation visible without scrolling at 900 × 600.
  • Version bumped to 2.3.3.

Validation

  • 1,097 unit tests pass.
  • Seven targeted Electron E2E tests pass, including accepting/declining the prompt, reload, keyboard focus, and both settings sizes.
  • Typecheck and production build pass. Lint has no errors.
  • Test transports are mocked; no test data is sent to PostHog.

Operational notes

Live ingestion has not been verified. Set the PostHog project to discard client IP data; direct connections still expose network IPs. Counts represent opted-in installations, not exact people or all users. Chart feature_used by feature with unique users for adoption and total events for frequency. Low adoption alone does not explain why a feature is unused. Previously received events are not deleted by opting out.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

📊 Coverage Report

Metric Coverage
Statements 93.77%
Branches 93.43%
Functions 94.18%
Lines 93.99%

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

🧪 E2E Test Results

38 passed, 0 failed, 0 skipped

Test Suite Result
Analytics consent
App Launch
Clipboard
Context Menu
Image Clipboard
Quick Clips
Quick look
Quick look — clipboard writes
Quick look — rendered html
Settings window
Theme
Settings — Tools tab
Quick look — pattern scanning
Quick look — clip templates

@cb-jeeves cb-jeeves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adds an opt-in, fixed-payload PostHog ping from the main process with consent stored per installation. The boundary itself is tight and well tested; the one thing I would fix before merge is the published docs page, which still promises no analytics at all.

Minor: the docstring on Application.tsx still lists the panel's controls without the new analytics row.

Comment thread README.md Outdated
Comment thread src/renderer/src/components/settings/general/Analytics.tsx Outdated
Comment thread src/preload/index.d.ts Outdated
… the docs privacy page

The docs site still claimed no analytics of any kind. Scope the local-only
bullet to clipboard data and describe the opt-in usage ping in the same
terms as the README, linking to the analytics README.

The settings row now uses the shared RowStatus slot instead of a generic
alert. A failed opt-out re-reads the stored preference so the switch shows
what is on disk, and the error message says reporting is paused now but
may resume next launch. The initial read failure keeps its own message.

Name the preference payload AnalyticsPreference in shared/types and use it
in preload, the main client and the settings component. Add the analytics
row to the Application panel docstring.
@cb-jeeves

Copy link
Copy Markdown
Collaborator

Responded to review round 1, pushed 269f05b:

  • Addressed docs privacy page: scoped the local-only bullet to clipboard data and replaced the no-analytics bullet with the README wording, linking to the analytics README.
  • Addressed Analytics.tsx error handling: the row uses the RowStatus slot, a failed opt-out re-reads the stored preference and says reporting is paused now but may resume next launch, and the initial read failure keeps a separate message. Added a test for the failed opt-out.
  • Addressed AnalyticsPreference type: added it to shared/types and used it in preload, the main client and the settings component.
  • Addressed Application.tsx docstring: lists Language label and Share usage counts.

@cb-jeeves cb-jeeves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since last round: the docs privacy page now matches the README, the preference payload is a shared AnalyticsPreference type, and the settings row reports through the shared status slot. That closes all three earlier threads. One blocking issue remains in the new failed-opt-out path, plus one minor lifecycle gap in the same component.

Comment thread src/renderer/src/components/settings/general/Analytics.tsx Outdated
Comment thread src/renderer/src/components/settings/general/Privacy.tsx
…t and clear the saved label

A failed opt-out re-read the preference from the main process, which had already dropped
the ID in memory, so the switch flipped off while the consent file still said on. Keep the
previous renderer value instead, which matches the file in both failure cases.

Clear the saved label after SAVED_LABEL_MS like every other row in the panel.
@cb-jeeves

Copy link
Copy Markdown
Collaborator

Responded to review round 2, pushed d02bc14:

  • Addressed Analytics.tsx failed opt-out re-read: removed the re-read and kept the previous renderer value, which matches the consent file in both failure cases. The test now returns enabled: false from the second preference read, as the main process does, and asserts no extra read happens.
  • Addressed Analytics.tsx saved status never clearing: a local effect clears it after SAVED_LABEL_MS from useSetting, with a fake-timer test.

@cb-jeeves cb-jeeves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since last round: the failed-opt-out path no longer re-reads the preference, so the switch stays on for the current mount, and the saved label now clears after the shared delay. That closes both round-two threads. One follow-on remains in the main-process side of the same failed-opt-out case; it is the last thing I would change before merge.

Comment thread src/main/analytics/client.ts Outdated
setEnabled clears the sending ID before writing so a failed opt-out never
keeps sending, but preference() derived the reported flag from that ID.
After a failed rename the file still said enabled while the settings
switch showed off on every remount, with no way to retry.

Track the persisted consent separately: it is set on load and only
flipped after the rename succeeds, and preference() reports it.
Add a client test for the failed-write case and remount the settings
component in the renderer test.
@cb-jeeves

Copy link
Copy Markdown
Collaborator

Responded to review round 3, pushed c052e79:

  • Addressed client.ts preference after a failed opt-out: added a persisted consent flag that is set on load and only flipped after the rename succeeds. preference() reports that flag instead of deriving it from the sending ID, which is still cleared before the write. New client test covers the failed write, and the renderer test remounts Analytics after the failure and expects the switch on.

@cb-jeeves cb-jeeves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since last round: preference() reports the on-disk consent through a separate persisted flag, so a failed opt-out keeps the switch on across remounts while the cleared sending ID keeps the process quiet, which closes the last open thread.

@cb-jeeves cb-jeeves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since last round: the analytics row moved out of Application into its own Privacy panel under Window, the docs and README point at the new location, and the unit and E2E panel counts and viewport assertions cover it. Nothing in the privacy boundary changed. One minor layout suggestion below; the PR is otherwise ready.

Comment thread src/renderer/src/components/settings/general/General.tsx Outdated
Analytics was the only settings panel framed by General.tsx. Rename it to
Privacy, move the Panel wrapper into the component, and drop the Panel
import from General so it is a list of self-contained panels again.
@cb-jeeves

Copy link
Copy Markdown
Collaborator

Responded to review round 5, pushed e75267c:

  • Addressed General.tsx Privacy panel wrapper: renamed the Analytics component to Privacy, moved the Panel title into the component, and dropped the Panel import from General.tsx so every panel there is self-contained. Test ids are unchanged; typecheck and tests pass.

@dantheuber
dantheuber merged commit 306da5d into main Sep 12, 2026
3 checks passed
@dantheuber
dantheuber deleted the t3code/setup-privacy-safe-posthog branch September 12, 2026 09:10
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.

2 participants