88} from "electron" ;
99import * as path from "path" ;
1010import * as fs from "fs" ;
11+ import { randomUUID } from "crypto" ;
12+ import * as https from "https" ;
1113import { readGitRepo } from "../git/GitReader" ;
1214
1315let mainWindow : BrowserWindow | null = null ;
@@ -16,6 +18,128 @@ let gitWatcher: fs.FSWatcher | null = null;
1618let workingDirWatcher : fs . FSWatcher | null = null ;
1719let 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+
19143function buildAppMenu ( ) : Menu {
20144 const isMac = process . platform === "darwin" ;
21145 const template : MenuItemConstructorOptions [ ] = [
@@ -148,6 +272,7 @@ function startWatcher(repoPath: string) {
148272
149273app . whenReady ( ) . then ( ( ) => {
150274 createWindow ( ) ;
275+ trackAppRun ( ) ;
151276
152277 ipcMain . handle ( "read-git-repo" , async ( _event , repoPath : string ) => {
153278 currentRepoPath = repoPath ;
0 commit comments