@@ -19,6 +19,7 @@ type InternalSkillSource = "curated" | "managed" | "custom";
1919
2020type AnalyticsState = {
2121 sessionStartSent : boolean ;
22+ firstConversationDistinctId : string | null ;
2223 sentUserMessageIds : string [ ] ;
2324 sentSkillUseIds : string [ ] ;
2425} ;
@@ -65,10 +66,16 @@ type ResolvedSkillInfo = {
6566 source : string | null ;
6667} ;
6768
69+ type AnalyticsDistinctIdResolution =
70+ | { status : "ready" ; distinctId : string }
71+ | { status : "missing" }
72+ | { status : "error" } ;
73+
6874const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com" ;
6975
7076const EMPTY_ANALYTICS_STATE : AnalyticsState = {
7177 sessionStartSent : false ,
78+ firstConversationDistinctId : null ,
7279 sentUserMessageIds : [ ] ,
7380 sentSkillUseIds : [ ] ,
7481} ;
@@ -169,11 +176,41 @@ export class AnalyticsService {
169176 return ;
170177 }
171178
179+ const analyticsDistinctId = await this . resolveAnalyticsDistinctId ( ) ;
180+ if ( analyticsDistinctId . status === "error" ) {
181+ return ;
182+ }
183+
184+ const shouldSendAnalytics = analyticsDistinctId . status === "ready" ;
185+ const currentDistinctId =
186+ analyticsDistinctId . status === "ready"
187+ ? analyticsDistinctId . distinctId
188+ : null ;
189+ let stateChanged = false ;
190+
172191 await this . ensureStateLoaded ( ) ;
173- const profile = await this . configStore . getLocalProfile ( ) ;
192+ if (
193+ currentDistinctId &&
194+ this . state . sessionStartSent &&
195+ this . state . firstConversationDistinctId === null
196+ ) {
197+ this . state . sessionStartSent = false ;
198+ stateChanged = true ;
199+ }
200+
201+ if (
202+ currentDistinctId &&
203+ this . state . sessionStartSent &&
204+ this . state . firstConversationDistinctId &&
205+ this . state . firstConversationDistinctId !== currentDistinctId
206+ ) {
207+ this . state . sessionStartSent = false ;
208+ this . state . firstConversationDistinctId = null ;
209+ stateChanged = true ;
210+ }
211+
174212 const sessions = await this . sessionsRuntime . listSessions ( ) ;
175213 const skillLedger = await this . readSkillLedgerSources ( ) ;
176- let stateChanged = false ;
177214 let firstSessionCandidate : UserMessageCandidate | null = null ;
178215
179216 for ( const session of sessions ) {
@@ -214,16 +251,18 @@ export class AnalyticsService {
214251 continue ;
215252 }
216253
217- await this . sendAnalyticsEvent (
218- profile . id ,
219- "user_message_sent" ,
220- {
221- channel : userMessage . channel ,
222- model_provider : userMessage . providerName ,
223- state : userMessage . state ,
224- } ,
225- userMessage . timestampMs ,
226- ) ;
254+ if ( shouldSendAnalytics ) {
255+ await this . sendAnalyticsEvent (
256+ analyticsDistinctId . distinctId ,
257+ "user_message_sent" ,
258+ {
259+ channel : userMessage . channel ,
260+ model_provider : userMessage . providerName ,
261+ state : userMessage . state ,
262+ } ,
263+ userMessage . timestampMs ,
264+ ) ;
265+ }
227266 this . sentUserMessageIds . add ( userMessage . id ) ;
228267 stateChanged = true ;
229268 }
@@ -236,34 +275,39 @@ export class AnalyticsService {
236275 continue ;
237276 }
238277
239- await this . sendAnalyticsEvent (
240- profile . id ,
241- "skill_use" ,
242- {
243- skill_name : skillUse . skillName ,
244- skill_source : skillUse . skillSource ,
245- channel : skillUse . channel ,
246- model_provider : skillUse . providerName ,
247- } ,
248- skillUse . timestampMs ,
249- ) ;
278+ if ( shouldSendAnalytics ) {
279+ await this . sendAnalyticsEvent (
280+ analyticsDistinctId . distinctId ,
281+ "skill_use" ,
282+ {
283+ skill_name : skillUse . skillName ,
284+ skill_source : skillUse . skillSource ,
285+ channel : skillUse . channel ,
286+ model_provider : skillUse . providerName ,
287+ } ,
288+ skillUse . timestampMs ,
289+ ) ;
290+ }
250291 this . sentSkillUseIds . add ( skillUse . id ) ;
251292 stateChanged = true ;
252293 }
253294 }
254295
255296 if ( ! this . state . sessionStartSent && firstSessionCandidate ?. providerName ) {
256- await this . sendAnalyticsEvent (
257- profile . id ,
258- "nexu_first_conversation_start" ,
259- {
260- channel : firstSessionCandidate . channel ,
261- model_provider : firstSessionCandidate . providerName ,
262- } ,
263- firstSessionCandidate . timestampMs ,
264- ) ;
265- this . state . sessionStartSent = true ;
266- stateChanged = true ;
297+ if ( shouldSendAnalytics ) {
298+ await this . sendAnalyticsEvent (
299+ analyticsDistinctId . distinctId ,
300+ "nexu_first_conversation_start" ,
301+ {
302+ channel : firstSessionCandidate . channel ,
303+ model_provider : firstSessionCandidate . providerName ,
304+ } ,
305+ firstSessionCandidate . timestampMs ,
306+ ) ;
307+ this . state . firstConversationDistinctId = analyticsDistinctId . distinctId ;
308+ this . state . sessionStartSent = true ;
309+ stateChanged = true ;
310+ }
267311 }
268312
269313 if ( stateChanged ) {
@@ -281,6 +325,10 @@ export class AnalyticsService {
281325 const parsed = JSON . parse ( raw ) as Partial < AnalyticsState > ;
282326 this . state = {
283327 sessionStartSent : parsed . sessionStartSent === true ,
328+ firstConversationDistinctId :
329+ typeof parsed . firstConversationDistinctId === "string"
330+ ? parsed . firstConversationDistinctId
331+ : null ,
284332 sentUserMessageIds : Array . isArray ( parsed . sentUserMessageIds )
285333 ? parsed . sentUserMessageIds . filter (
286334 ( value ) : value is string => typeof value === "string" ,
@@ -600,6 +648,29 @@ export class AnalyticsService {
600648 return `${ host . replace ( / \/ + $ / , "" ) } /i/v0/e/` ;
601649 }
602650
651+ private async resolveAnalyticsDistinctId ( ) : Promise < AnalyticsDistinctIdResolution > {
652+ try {
653+ const cloudStatus = await this . configStore . getDesktopCloudStatus ( ) ;
654+ const userId =
655+ typeof cloudStatus ?. userId === "string"
656+ ? cloudStatus . userId . trim ( )
657+ : "" ;
658+ if ( ! userId || userId === "desktop-local-user" ) {
659+ return { status : "missing" } ;
660+ }
661+
662+ return { status : "ready" , distinctId : userId } ;
663+ } catch ( error ) {
664+ logger . warn (
665+ {
666+ error : error instanceof Error ? error . message : String ( error ) ,
667+ } ,
668+ "failed_to_resolve_analytics_distinct_id" ,
669+ ) ;
670+ return { status : "error" } ;
671+ }
672+ }
673+
603674 private async sendAnalyticsEvent (
604675 distinctId : string ,
605676 eventType : string ,
0 commit comments