Skip to content

feat(telemetry): replace Loft analytics with PostHog and add Netlify update hosting#303

Merged
skevetter merged 3 commits into
mainfrom
c83b-ws-metrics-hosting
May 17, 2026
Merged

feat(telemetry): replace Loft analytics with PostHog and add Netlify update hosting#303
skevetter merged 3 commits into
mainfrom
c83b-ws-metrics-hosting

Conversation

@skevetter

@skevetter skevetter commented May 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Part A — Desktop Update Hosting on Netlify: Configures electron-builder for dual publish (generic on dl.devsy.sh + GitHub Releases fallback), adds Netlify site config, and adds a CI step to deploy update metadata (latest*.yml) to Netlify after each release build. The actual installers still download from GitHub Releases — only the metadata check goes through Netlify, separating update check traffic from download counts.

  • Part B — Replace Loft Analytics with PostHog: Replaces the custom HTTP client posting to analytics.loft.rocks/v1/insert with the official PostHog Go SDK (posthog-go). Removes the custom double-buffering layer since PostHog handles batching internally. Adds posthog-node to the desktop Electron app with event tracking for app lifecycle (open/close), update lifecycle (check/download/install), workspace CRUD, provider actions, page navigation, and error occurrences. All events respect the existing DEVSY_DISABLE_TELEMETRY=true opt-out. Updates telemetry documentation.

Setup required before first use:

  • Replace phc_PLACEHOLDER API key constants in pkg/telemetry/analytics/client.go and desktop/src/main/analytics.ts with a real PostHog project API key
  • Add NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID secrets to the GitHub repository
  • Create the Netlify site and configure dl.devsy.sh DNS to point to it

Summary by CodeRabbit

  • New Features

    • Desktop app now includes analytics tracking for app lifecycle, page views, and common actions.
  • Bug Fixes / Improvements

    • Desktop update metadata is now published and automatically deployed to the public download site for faster update distribution.
  • Documentation

    • Telemetry docs updated to explain analytics destination and what is (and isn’t) collected.
  • Chores

    • Added analytics-related runtime dependency and CI/build automation enhancements.

Review Change Stack

…update hosting

Part A: Desktop update hosting on Netlify via dl.devsy.sh
- Configure electron-builder for dual publish (generic + github)
- Add Netlify site config (sites/dl-devsy-sh/)
- Add CI step to deploy update metadata (latest*.yml) to Netlify

Part B: Replace Loft analytics with PostHog
- Replace analytics.loft.rocks HTTP client with PostHog Go SDK
- Remove custom double-buffering (PostHog SDK handles batching)
- Add posthog-node to desktop app with machine ID hashing
- Add IPC bridge for renderer → main analytics tracking
- Instrument app lifecycle, update lifecycle, workspace CRUD,
  provider actions, and page navigation events
- Update telemetry documentation for PostHog

BREAKING CHANGE: analytics.loft.rocks endpoint removed
@netlify

netlify Bot commented May 16, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit 12066c2
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/6a099c21c794410008451610

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Integrates PostHog telemetry across desktop and CLI, instruments desktop lifecycle, IPC, updater, and renderer page views, rewrites CLI telemetry client to use posthog-go, and adds release workflow steps to publish desktop update metadata to Netlify.

Changes

PostHog Analytics System Migration

Layer / File(s) Summary
Desktop analytics module and machine ID foundation
desktop/src/main/analytics.ts, desktop/src/main/machine-id.ts, desktop/package.json
New analytics module integrates PostHog SDK with HMAC-based stable distinctId, telemetry opt-out via env var, init/track/shutdown APIs, and adds posthog-node dependency.
Electron main process lifecycle & auto-updater
desktop/src/main/index.ts, desktop/src/main/updater.ts
App initializes analytics on app.whenReady() (app_open), records app_close on before-quit, and auto-updater emits checking-for-update, update-available, update_installed, and update_error events.
IPC handler instrumentation & renderer bridge
desktop/src/main/ipc.ts, desktop/src/renderer/src/lib/ipc/commands.ts, desktop/src/renderer/src/lib/ipc/mock.ts, desktop/src/renderer/src/App.svelte
Provider and workspace IPC handlers emit analytics; new analytics_track handler sanitizes renderer events; renderer exposes analyticsTrack() and records page_view with normalized paths.
CLI PostHog client and dependency migration
go.mod, pkg/telemetry/analytics/client.go
Adds posthog-go and supporting deps; rewrites client to construct PostHog client, map incoming events to PostHog captures (using machine_id/distinctId), enqueue captures, and close client on flush.
Release workflow and distribution configuration
.github/workflows/release.yml, desktop/electron-builder.yml, sites/dl-devsy-sh/netlify.toml
Build uploads desktop update YAMLs as artifacts; new job aggregates artifacts and deploys to Netlify using sites/dl-devsy-sh/netlify.toml; electron-builder adds generic provider for https://dl.devsy.sh/desktop; Netlify config sets publish dir and headers for /desktop/*.
Telemetry documentation and linter/gitleaks updates
docs/pages/other-topics/telemetry.mdx, .gitleaksignore, .golangci.yaml
Docs updated to state PostHog as backend and show JSON payload example; gitleaks and golangci configs add targeted ignores/exclusions for PostHog key patterns and the rewritten client file.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

size/l

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the two main changes: replacing Loft analytics with PostHog and adding Netlify update hosting, matching the primary work across the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
pkg/telemetry/analytics/client.go (1)

80-87: 💤 Low value

Flush() closes the client, making it unusable for subsequent events.

Close() terminates the PostHog client permanently. If RecordEvent is called after Flush(), it will fail because the underlying client is closed. Consider either:

  1. Renaming to Close() to clarify the terminal nature, or
  2. Adding a guard to prevent RecordEvent from panicking on a closed client.
♻️ Option: Add state tracking to guard against post-close usage
 type client struct {
 	phClient posthog.Client
+	closed   bool
 }

 func (c *client) RecordEvent(event Event) {
+	if c.closed {
+		return
+	}
 	eventData, ok := event["event"]
 	// ... rest of method
 }

 func (c *client) Flush() {
 	if Dry {
 		return
 	}
+	c.closed = true
 	if err := c.phClient.Close(); err != nil {
 		log.Debugf("error flushing PostHog client: %v", err)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/telemetry/analytics/client.go` around lines 80 - 87, Flush() calls
c.phClient.Close() which permanently closes the PostHog client and makes the
client unusable; update the client to track closed state and guard RecordEvent
(or rename Flush to Close if you prefer a terminal API). Specifically, add a
closed bool (or atomic/Mutex-protected flag) to the client struct, set it to
true after c.phClient.Close() inside Flush(), and have RecordEvent check that
flag and no-op or return early if closed to avoid panics; ensure any concurrent
access is synchronized (use sync.Mutex or atomic operations) and keep the
symbols: client struct, Flush(), RecordEvent(), and c.phClient.Close() to locate
the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 171-179: The deploy step currently gated by "if: runner.os ==
'Linux'" (the block that sets NETLIFY_AUTH_TOKEN/NETLIFY_SITE_ID, runs npm
install -g netlify-cli, creates netlify-deploy/desktop and cp
desktop/release/latest*.yml, then runs "netlify deploy --prod
--dir=netlify-deploy --site=\"$NETLIFY_SITE_ID\"") only runs on Linux and misses
metadata from other matrix runners; remove that OS guard and instead move this
logic into a dedicated final job (e.g., "deploy") that depends_on the matrix
jobs and uses actions/download-artifact to fetch each job's uploaded metadata
artifacts into netlify-deploy before running the same netlify-cli steps, leaving
NETLIFY_* env usage intact.
- Around line 175-179: The Netlify deploy step uses the root netlify.toml
instead of the desktop-specific config and wrong publish dir; update the netlify
deploy invocation to explicitly point to the desktop config and publish
directory by replacing the current netlify CLI call (the line invoking "netlify
deploy --prod --dir=netlify-deploy --site=\"$NETLIFY_SITE_ID\"") with one that
includes --config sites/dl-devsy-sh/netlify.toml and --dir
netlify-deploy/desktop (keep --prod and --site="$NETLIFY_SITE_ID").

In `@desktop/src/main/analytics.ts`:
- Around line 7-9: The code uses a hard-coded POSTHOG_API_KEY
("phc_PLACEHOLDER") and initializes PostHog unconditionally, which can silently
drop telemetry; change POSTHOG_API_KEY to be loaded from config/env and add a
guard before calling PostHog.init (or whatever initialize function is used) to
check that POSTHOG_API_KEY is non-empty and not equal to "phc_PLACEHOLDER"; if
the key is invalid, do not call PostHog.init, emit a clear warning/error via the
existing logger, and ensure downstream code (e.g., any sendEvent/track wrappers)
no-ops when analytics is disabled so release builds never attempt to send using
the placeholder key.

In `@desktop/src/main/ipc.ts`:
- Around line 649-657: The ipc handler for "analytics_track" currently forwards
arbitrary payloads to trackEvent; change the handler (ipcMain.handle
"analytics_track") to validate and constrain args before calling trackEvent:
enforce args.name is a string and whitelist it against an allowed eventNames set
(reject or map unknown names), ensure args.properties is an object of primitive
values (no nested objects/arrays) with a maximum number of keys (e.g., 20) and
maximum string length per value (e.g., 256 chars), strip or redact values
matching PII patterns, and return/ignore invalid keys rather than forwarding
them; on validation failure, log a sanitized warning and do not call trackEvent.

In `@desktop/src/renderer/src/App.svelte`:
- Around line 92-94: The subscription is sending raw route paths (in
unsubLocation / location.subscribe) to analyticsTrack("page_view", { path })
which can leak dynamic IDs; update the callback to sanitize or normalize the
path before sending by implementing a sanitizePath/normalizeRoute helper that
strips or replaces workspace/provider/machine ID segments (or maps them to
placeholders) and call analyticsTrack("page_view", { path: sanitizedPath })
instead so telemetry contains only safe, non-identifying route patterns.

In `@pkg/telemetry/analytics/client.go`:
- Around line 39-57: The machineID extracted with machineID, _ :=
eventData["machine_id"].(string) can be empty and PostHog will reject events;
update the send path in pkg/telemetry/analytics/client.go to validate machineID
after that assertion and handle empties before calling c.phClient.Enqueue (or
when building posthog.Capture): if machineID == "" then either log a warning
including eventType (and the raw eventData), and generate a stable fallback
distinct id (e.g., a UUID) or skip sending the event explicitly, then proceed to
call c.phClient.Enqueue with the validated non-empty DistinctId; ensure this
check happens regardless of Dry and reference the symbols machineID, eventType,
buildProperties, posthog.Capture, and c.phClient.Enqueue so the fix is applied
at the correct spot.

---

Nitpick comments:
In `@pkg/telemetry/analytics/client.go`:
- Around line 80-87: Flush() calls c.phClient.Close() which permanently closes
the PostHog client and makes the client unusable; update the client to track
closed state and guard RecordEvent (or rename Flush to Close if you prefer a
terminal API). Specifically, add a closed bool (or atomic/Mutex-protected flag)
to the client struct, set it to true after c.phClient.Close() inside Flush(),
and have RecordEvent check that flag and no-op or return early if closed to
avoid panics; ensure any concurrent access is synchronized (use sync.Mutex or
atomic operations) and keep the symbols: client struct, Flush(), RecordEvent(),
and c.phClient.Close() to locate the changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fdc0de1c-71eb-43a2-aab3-22956e1c521c

📥 Commits

Reviewing files that changed from the base of the PR and between 41c4da8 and 36e5144.

⛔ Files ignored due to path filters (2)
  • desktop/package-lock.json is excluded by !**/package-lock.json
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (18)
  • .github/workflows/release.yml
  • desktop/electron-builder.yml
  • desktop/package.json
  • desktop/src/main/analytics.ts
  • desktop/src/main/index.ts
  • desktop/src/main/ipc.ts
  • desktop/src/main/machine-id.ts
  • desktop/src/main/updater.ts
  • desktop/src/renderer/src/App.svelte
  • desktop/src/renderer/src/lib/ipc/commands.ts
  • desktop/src/renderer/src/lib/ipc/mock.ts
  • docs/pages/other-topics/telemetry.mdx
  • go.mod
  • pkg/telemetry/analytics/buffer.go
  • pkg/telemetry/analytics/client.go
  • pkg/telemetry/analytics/types.go
  • sites/dl-devsy-sh/netlify.toml
  • sites/dl-devsy-sh/public/desktop/.gitkeep
💤 Files with no reviewable changes (2)
  • pkg/telemetry/analytics/types.go
  • pkg/telemetry/analytics/buffer.go

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Comment thread desktop/src/main/analytics.ts Outdated
Comment thread desktop/src/main/ipc.ts
Comment thread desktop/src/renderer/src/App.svelte
Comment thread pkg/telemetry/analytics/client.go
- Move Netlify deploy to dedicated job collecting metadata from all
  matrix runners via upload/download-artifact pattern
- Use sites/dl-devsy-sh/netlify.toml config explicitly in deploy
- Guard against placeholder PostHog API key in both Go and TS clients
- Validate and constrain analytics_track IPC payloads (max 20 keys,
  64-char key limit, 256-char string value limit)
- Normalize route paths in page_view events to strip dynamic IDs
  (/workspaces/:id, /providers/:id, /machines/:id)
- Guard against empty machine_id in Go PostHog client
- Remove unused createHash import from analytics.ts
@github-actions github-actions Bot added size/xl and removed size/l labels May 16, 2026
…t token

PostHog project tokens (phc_...) are public write-only keys — safe to commit.
Add .gitleaksignore and golangci exclusion to suppress false-positive secret
detection for these entries.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
desktop/src/renderer/src/App.svelte (1)

85-88: ⚡ Quick win

Preserve static routes before dynamic normalization.

Current matching rewrites /workspaces/new and /providers/add as :id, which mixes create-flow traffic into detail-page metrics. Handle known static paths first, then apply the dynamic-ID regex.

Proposed patch
 function normalizeAnalyticsPath(path: string): string {
+  if (path === "/workspaces/new") return "/workspaces/new"
+  if (path === "/providers/add") return "/providers/add"
   if (/^\/workspaces\/[^/]+$/.test(path)) return "/workspaces/:id"
   if (/^\/providers\/[^/]+$/.test(path)) return "/providers/:id"
   if (/^\/machines\/[^/]+$/.test(path)) return "/machines/:id"
   return path
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/renderer/src/App.svelte` around lines 85 - 88, The
normalizeAnalyticsPath function currently normalizes dynamic ID routes before
checking static routes, causing static paths like "/workspaces/new" and
"/providers/add" to be rewritten as "/workspaces/:id" or "/providers/:id";
update normalizeAnalyticsPath to first check and return known static routes
(e.g., "/workspaces/new", "/providers/add", any other create/edit static paths)
before applying the ID regexes for "/workspaces/:id", "/providers/:id", and
"/machines/:id" so that create-flow and other static endpoints are preserved in
analytics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@desktop/src/renderer/src/App.svelte`:
- Around line 85-88: The normalizeAnalyticsPath function currently normalizes
dynamic ID routes before checking static routes, causing static paths like
"/workspaces/new" and "/providers/add" to be rewritten as "/workspaces/:id" or
"/providers/:id"; update normalizeAnalyticsPath to first check and return known
static routes (e.g., "/workspaces/new", "/providers/add", any other create/edit
static paths) before applying the ID regexes for "/workspaces/:id",
"/providers/:id", and "/machines/:id" so that create-flow and other static
endpoints are preserved in analytics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a7fce686-96f1-4adf-98af-28364076a64c

📥 Commits

Reviewing files that changed from the base of the PR and between 36e5144 and 12066c2.

📒 Files selected for processing (7)
  • .github/workflows/release.yml
  • .gitleaksignore
  • .golangci.yaml
  • desktop/src/main/analytics.ts
  • desktop/src/main/ipc.ts
  • desktop/src/renderer/src/App.svelte
  • pkg/telemetry/analytics/client.go
✅ Files skipped from review due to trivial changes (2)
  • .golangci.yaml
  • .gitleaksignore
🚧 Files skipped from review as they are similar to previous changes (3)
  • desktop/src/main/ipc.ts
  • desktop/src/main/analytics.ts
  • pkg/telemetry/analytics/client.go

@skevetter skevetter merged commit 07824bd into main May 17, 2026
54 checks passed
@skevetter skevetter deleted the c83b-ws-metrics-hosting branch May 17, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant