Skip to content

Commit 9627e72

Browse files
ga app_run
1 parent 1b99aa9 commit 9627e72

File tree

3 files changed

+148
-0
lines changed

3 files changed

+148
-0
lines changed

.github/workflows/electron-build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,16 @@ jobs:
3434
- name: Build app
3535
working-directory: electron
3636
run: npm run build
37+
env:
38+
GA_MEASUREMENT_ID: ${{ secrets.GA_MEASUREMENT_ID }}
39+
GA_API_SECRET: ${{ secrets.GA_API_SECRET }}
3740

3841
- name: Build installers
3942
working-directory: electron
4043
run: npm run dist
44+
env:
45+
GA_MEASUREMENT_ID: ${{ secrets.GA_MEASUREMENT_ID }}
46+
GA_API_SECRET: ${{ secrets.GA_API_SECRET }}
4147

4248
- name: Upload artifacts
4349
uses: actions/upload-artifact@v4

.vscode/launch.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Electron (dev)",
6+
"type": "node",
7+
"request": "launch",
8+
"runtimeExecutable": "npm",
9+
"runtimeArgs": ["run", "dev"],
10+
"cwd": "${workspaceFolder}/electron",
11+
"env": {
12+
"GA_MEASUREMENT_ID": "G-7D9M5LKKR7",
13+
"GA_API_SECRET": "n0_8jytOTxiC0bBKaQWLsg"
14+
}
15+
}
16+
]
17+
}

electron/src/main/main.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from "electron";
99
import * as path from "path";
1010
import * as fs from "fs";
11+
import { randomUUID } from "crypto";
12+
import * as https from "https";
1113
import { readGitRepo } from "../git/GitReader";
1214

1315
let mainWindow: BrowserWindow | null = null;
@@ -16,6 +18,128 @@ let gitWatcher: fs.FSWatcher | null = null;
1618
let workingDirWatcher: fs.FSWatcher | null = null;
1719
let debounceTimer: NodeJS.Timeout | null = null;
1820

21+
const analyticsConfig = {
22+
measurementId: process.env.GA_MEASUREMENT_ID || "",
23+
apiSecret: process.env.GA_API_SECRET || "",
24+
debug: process.env.GA_DEBUG === "1" || process.env.GA_DEBUG === "true",
25+
};
26+
27+
type AnalyticsEventParams = Record<string, string | number | boolean>;
28+
29+
function getAnalyticsClientId(): string {
30+
const filePath = path.join(app.getPath("userData"), "analytics.json");
31+
try {
32+
if (fs.existsSync(filePath)) {
33+
const raw = fs.readFileSync(filePath, "utf-8");
34+
const parsed = JSON.parse(raw) as { clientId?: string };
35+
if (parsed.clientId) return parsed.clientId;
36+
}
37+
} catch (e) {
38+
console.warn("Failed to read analytics client id:", e);
39+
}
40+
41+
const clientId = randomUUID();
42+
try {
43+
fs.writeFileSync(filePath, JSON.stringify({ clientId }));
44+
} catch (e) {
45+
console.warn("Failed to persist analytics client id:", e);
46+
}
47+
return clientId;
48+
}
49+
50+
function sendAnalyticsEvent(name: string, params: AnalyticsEventParams) {
51+
if (!analyticsConfig.measurementId || !analyticsConfig.apiSecret) {
52+
console.warn(
53+
"GA analytics not configured. Set GA_MEASUREMENT_ID and GA_API_SECRET.",
54+
);
55+
return;
56+
}
57+
58+
const payload = JSON.stringify({
59+
client_id: getAnalyticsClientId(),
60+
events: [
61+
{
62+
name,
63+
params: {
64+
...params,
65+
debug_mode: analyticsConfig.debug,
66+
engagement_time_msec: 1,
67+
},
68+
},
69+
],
70+
});
71+
72+
const sendRequest = (path: string, label: string) => {
73+
const request = https.request(
74+
{
75+
method: "POST",
76+
hostname: "www.google-analytics.com",
77+
path,
78+
headers: {
79+
"Content-Type": "application/json",
80+
"Content-Length": Buffer.byteLength(payload),
81+
},
82+
},
83+
(response) => {
84+
if (analyticsConfig.debug) {
85+
console.log(
86+
`GA ${label} response status: ${response.statusCode || "unknown"}`,
87+
);
88+
}
89+
if (label === "debug") {
90+
let body = "";
91+
response.on("data", (chunk) => {
92+
body += chunk.toString();
93+
});
94+
response.on("end", () => {
95+
if (body) console.log("GA debug response:", body);
96+
});
97+
}
98+
if (response.statusCode && response.statusCode >= 400) {
99+
console.warn(`GA event failed with status ${response.statusCode}.`);
100+
}
101+
response.resume();
102+
},
103+
);
104+
105+
request.on("error", (error) => {
106+
console.warn("GA event failed:", error);
107+
});
108+
109+
request.write(payload);
110+
request.end();
111+
};
112+
113+
const basePath = `/mp/collect?measurement_id=${encodeURIComponent(
114+
analyticsConfig.measurementId,
115+
)}&api_secret=${encodeURIComponent(analyticsConfig.apiSecret)}`;
116+
117+
sendRequest(basePath, "collect");
118+
119+
if (analyticsConfig.debug) {
120+
sendRequest(`/debug${basePath}`, "debug");
121+
}
122+
}
123+
124+
function getRegionFromLocale(locale: string): string {
125+
const normalized = locale.replace("_", "-");
126+
const parts = normalized.split("-");
127+
return parts[1]?.toUpperCase() || "unknown";
128+
}
129+
130+
function trackAppRun() {
131+
const locale = app.getLocale();
132+
const timeZone =
133+
Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
134+
135+
sendAnalyticsEvent("app_run", {
136+
app_region: getRegionFromLocale(locale),
137+
app_locale: locale,
138+
app_timezone: timeZone,
139+
app_version: app.getVersion(),
140+
});
141+
}
142+
19143
function buildAppMenu(): Menu {
20144
const isMac = process.platform === "darwin";
21145
const template: MenuItemConstructorOptions[] = [
@@ -148,6 +272,7 @@ function startWatcher(repoPath: string) {
148272

149273
app.whenReady().then(() => {
150274
createWindow();
275+
trackAppRun();
151276

152277
ipcMain.handle("read-git-repo", async (_event, repoPath: string) => {
153278
currentRepoPath = repoPath;

0 commit comments

Comments
 (0)