Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}

- name: upload update metadata artifact
uses: actions/upload-artifact@v7
with:
name: update-metadata-${{ matrix.os }}
path: desktop/release/latest*.yml
if-no-files-found: ignore

- name: upload cli assets
uses: softprops/action-gh-release@v3
if: runner.os == 'Linux'
Expand Down Expand Up @@ -226,9 +233,33 @@ jobs:
files: |
./Devsy.flatpak

deploy-update-metadata:
name: Deploy Update Metadata to Netlify
needs: [build-desktop]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6

- uses: actions/download-artifact@v8
with:
pattern: update-metadata-*
merge-multiple: true
path: sites/dl-devsy-sh/public/desktop

- name: deploy metadata
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
run: |
npm install -g netlify-cli
netlify deploy --prod \
--config=sites/dl-devsy-sh/netlify.toml \
--dir=sites/dl-devsy-sh/public \
--site="$NETLIFY_SITE_ID"

prerelease:
name: Publish Prerelease
needs: [build-desktop, build-flatpak]
needs: [build-desktop, build-flatpak, deploy-update-metadata]
if: github.event.release.prerelease
runs-on: ubuntu-latest
steps:
Expand Down
4 changes: 4 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# PostHog project tokens are public write-only keys safe to commit.
# See: https://posthog.com/docs/libraries/js#config
pkg/telemetry/analytics/client.go:generic-api-key:9
desktop/src/main/analytics.ts:generic-api-key:7
6 changes: 6 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ linters:
- unparam
- unused
- whitespace
exclusions:
rules:
- linters:
- gosec
text: "G101"
path: "pkg/telemetry/analytics/client.go"
settings:
cyclop:
max-complexity: 8
Expand Down
9 changes: 6 additions & 3 deletions desktop/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ protocols:
- devsy

publish:
provider: github
owner: devsy-org
repo: devsy
- provider: generic
url: https://dl.devsy.sh/desktop
useMultipleRangeRequest: false
- provider: github
owner: devsy-org
repo: devsy
36 changes: 36 additions & 0 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"chokidar": "^4.0.0",
"electron-updater": "^6.8.3",
"node-pty": "^1.0.0",
"posthog-node": "^5.34.2",
"svelte-spa-router": "^5.0.1"
},
"devDependencies": {
Expand Down
62 changes: 62 additions & 0 deletions desktop/src/main/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { createHmac } from "node:crypto"
import { homedir, platform, arch } from "node:os"
import { PostHog } from "posthog-node"
import { app } from "electron"
import { machineIdSync } from "./machine-id.js"

const POSTHOG_API_KEY = "phc_u3TY39zxfrRcyXJoqZ5WRFVTr75gZBHi2AUrfqJ6GCj2"
const POSTHOG_HOST = "https://us.i.posthog.com"

let client: PostHog | null = null
let distinctId = ""

function getDistinctId(): string {
const id = machineIdSync()
const home = homedir()
const mac = createHmac("sha256", id)
mac.update(home)
return mac.digest("hex")
}

function isTelemetryDisabled(): boolean {
return process.env.DEVSY_DISABLE_TELEMETRY === "true"
}

export function initAnalytics(): void {
if (isTelemetryDisabled()) return
if (!POSTHOG_API_KEY || POSTHOG_API_KEY === "phc_PLACEHOLDER") {
console.warn("[telemetry] PostHog API key not configured; analytics disabled")
return
}

distinctId = getDistinctId()
client = new PostHog(POSTHOG_API_KEY, {
host: POSTHOG_HOST,
flushAt: 20,
flushInterval: 30_000,
})
}

export function trackEvent(
name: string,
properties?: Record<string, unknown>,
): void {
if (!client || isTelemetryDisabled()) return

client.capture({
distinctId,
event: name,
properties: {
app_version: app.getVersion(),
os_name: platform(),
os_arch: arch(),
...properties,
},
})
}

export async function shutdownAnalytics(): Promise<void> {
if (!client) return
await client.shutdown()
client = null
}
6 changes: 6 additions & 0 deletions desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { join } from "node:path"
import { app, BrowserWindow, session } from "electron"
import { initAnalytics, shutdownAnalytics, trackEvent } from "./analytics.js"
import { CliRunner } from "./cli.js"
import { registerIpcHandlers } from "./ipc.js"
import { LogStore } from "./log-store.js"
Expand Down Expand Up @@ -90,6 +91,9 @@ function createWindow(): void {
}

app.whenReady().then(() => {
initAnalytics()
trackEvent("app_open")

// Register devsy:// as the default protocol handler for this app.
app.setAsDefaultProtocolClient(PROTOCOL)

Expand Down Expand Up @@ -136,6 +140,8 @@ app.whenReady().then(() => {

app.on("before-quit", () => {
;(app as typeof app & { isQuitting?: boolean }).isQuitting = true
trackEvent("app_close")
shutdownAnalytics().catch(() => {})
ptyManager.destroyAll()
})

Expand Down
39 changes: 39 additions & 0 deletions desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { join } from "node:path"
import { promisify } from "node:util"
import type { BrowserWindow } from "electron"
import { ipcMain } from "electron"
import { trackEvent } from "./analytics.js"
import type { CliRunner } from "./cli.js"
import type { LogStore } from "./log-store.js"
import type { PtyManager } from "./pty.js"
Expand Down Expand Up @@ -71,6 +72,7 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
ipcMain.handle(
"provider_add",
async (_event, args: { name: string; source?: string }) => {
trackEvent("provider_add")
const src = args.source ?? args.name
const cliArgs = ["provider", "add", src, "--use=false"]
if (args.source) {
Expand All @@ -81,6 +83,7 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
)

ipcMain.handle("provider_delete", async (_event, args: { name: string }) => {
trackEvent("provider_remove")
await cli.runRaw(["provider", "delete", args.name])
})

Expand Down Expand Up @@ -261,6 +264,7 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
debug?: boolean
},
) => {
trackEvent("workspace_create", { provider: args.provider })
const cliArgs = ["up", args.source]
if (args.workspaceId) cliArgs.push("--id", args.workspaceId)
if (args.provider) cliArgs.push("--provider", args.provider)
Expand Down Expand Up @@ -304,6 +308,7 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
ipcMain.handle(
"workspace_stop",
async (_event, args: { workspaceId: string; debug?: boolean }) => {
trackEvent("workspace_stop")
const cmdId = crypto.randomUUID()
const logPath = logStore.createLogFile(args.workspaceId)
const win = deps.getMainWindow()
Expand Down Expand Up @@ -343,6 +348,7 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
ipcMain.handle(
"workspace_delete",
async (_event, args: { workspaceId: string; debug?: boolean }) => {
trackEvent("workspace_delete")
const cmdId = crypto.randomUUID()
const logPath = logStore.createLogFile(args.workspaceId)
const win = deps.getMainWindow()
Expand Down Expand Up @@ -383,6 +389,7 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
ipcMain.handle(
"workspace_rebuild",
async (_event, args: { workspaceId: string; debug?: boolean }) => {
trackEvent("workspace_rebuild")
const cmdId = crypto.randomUUID()
const logPath = logStore.createLogFile(args.workspaceId)
const win = deps.getMainWindow()
Expand Down Expand Up @@ -422,6 +429,7 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
ipcMain.handle(
"workspace_reset",
async (_event, args: { workspaceId: string; debug?: boolean }) => {
trackEvent("workspace_reset")
const cmdId = crypto.randomUUID()
const logPath = logStore.createLogFile(args.workspaceId)
const win = deps.getMainWindow()
Expand Down Expand Up @@ -636,4 +644,35 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
return key
},
)

// ── Analytics ──
ipcMain.handle(
"analytics_track",
async (
_event,
args: { name: string; properties?: Record<string, unknown> },
) => {
if (
!args?.name ||
typeof args.name !== "string" ||
args.name.length > 64
) {
return
}
trackEvent(args.name, sanitizeAnalyticsProperties(args.properties))
},
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

function sanitizeAnalyticsProperties(
input?: Record<string, unknown>,
): Record<string, unknown> | undefined {
if (!input || typeof input !== "object") return undefined
const entries = Object.entries(input).slice(0, 20)
const out: Record<string, unknown> = {}
for (const [k, v] of entries) {
if (!k || k.length > 64) continue
out[k] = typeof v === "string" ? v.slice(0, 256) : v
}
return out
}
37 changes: 37 additions & 0 deletions desktop/src/main/machine-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { execSync } from "node:child_process"
import { createHash } from "node:crypto"
import { platform } from "node:os"

export function machineIdSync(): string {
try {
const os = platform()
let raw: string

if (os === "linux") {
raw = execSync("cat /etc/machine-id || cat /var/lib/dbus/machine-id", {
encoding: "utf-8",
timeout: 3000,
}).trim()
} else if (os === "darwin") {
const output = execSync(
"ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'",
{ encoding: "utf-8", timeout: 3000 },
)
const match = output.match(/"([^"]+)"$/)
raw = match?.[1] ?? "unknown"
} else if (os === "win32") {
const output = execSync(
"reg query HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
{ encoding: "utf-8", timeout: 3000 },
)
const match = output.match(/REG_SZ\s+(.+)/)
raw = match?.[1]?.trim() ?? "unknown"
} else {
raw = "unknown"
}

return createHash("sha256").update(raw).digest("hex")
} catch {
return "unknown"
}
}
Loading
Loading