@@ -4,7 +4,12 @@ import * as fs from 'fs';
4
4
import * as path from 'path' ;
5
5
import * as vscode from 'vscode' ;
6
6
7
- import { AI_RECENT_PASTES_TIME_MS , COMMAND_DASHBOARD , LogLevel } from './constants' ;
7
+ import {
8
+ AI_RECENT_PASTES_TIME_MS ,
9
+ COMMAND_DASHBOARD ,
10
+ LogLevel ,
11
+ SEND_BUFFER_SECONDS ,
12
+ } from './constants' ;
8
13
import { Options , Setting } from './options' ;
9
14
10
15
import { Dependencies } from './dependencies' ;
@@ -21,6 +26,19 @@ interface FileSelectionMap {
21
26
[ key : string ] : FileSelection ;
22
27
}
23
28
29
+ interface Heartbeat {
30
+ time : number ;
31
+ entity : string ;
32
+ is_write : boolean ;
33
+ lineno : number ;
34
+ cursorpos : number ;
35
+ lines_in_file : number ;
36
+ alternate_project ?: string ;
37
+ project_folder ?: string ;
38
+ category ?: 'debugging' | 'ai coding' | 'building' | 'code reviewing' ;
39
+ is_unsaved_entity ?: boolean ;
40
+ }
41
+
24
42
export class WakaTime {
25
43
private agentName : string ;
26
44
private extension : any ;
@@ -59,6 +77,8 @@ export class WakaTime {
59
77
private resourcesLocation : string ;
60
78
private lastApiKeyPrompted : number = 0 ;
61
79
private isMetricsEnabled : boolean = false ;
80
+ private heartbeats : Heartbeat [ ] = [ ] ;
81
+ private lastSent : number = 0 ;
62
82
63
83
constructor ( extensionPath : string , logger : Logger ) {
64
84
this . extensionPath = extensionPath ;
@@ -97,6 +117,7 @@ export class WakaTime {
97
117
}
98
118
99
119
public dispose ( ) {
120
+ this . sendHeartbeats ( ) ;
100
121
this . statusBar ?. dispose ( ) ;
101
122
this . statusBarTeamYou ?. dispose ( ) ;
102
123
this . statusBarTeamOther ?. dispose ( ) ;
@@ -520,6 +541,10 @@ export class WakaTime {
520
541
}
521
542
522
543
private onEvent ( isWrite : boolean ) : void {
544
+ if ( Date . now ( ) - this . lastSent > SEND_BUFFER_SECONDS * 1000 ) {
545
+ this . sendHeartbeats ( ) ;
546
+ }
547
+
523
548
clearTimeout ( this . debounceTimeoutId ) ;
524
549
this . debounceTimeoutId = setTimeout ( ( ) => {
525
550
if ( this . disabled ) return ;
@@ -543,7 +568,7 @@ export class WakaTime {
543
568
this . lastCompile !== this . isCompiling ||
544
569
this . lastAICodeGenerating !== this . isAICodeGenerating
545
570
) {
546
- this . sendHeartbeat (
571
+ this . appendHeartbeat (
547
572
doc ,
548
573
time ,
549
574
editor . selection . start ,
@@ -564,32 +589,7 @@ export class WakaTime {
564
589
} , this . debounceMs ) ;
565
590
}
566
591
567
- private async sendHeartbeat (
568
- doc : vscode . TextDocument ,
569
- time : number ,
570
- selection : vscode . Position ,
571
- isWrite : boolean ,
572
- isCompiling : boolean ,
573
- isDebugging : boolean ,
574
- isAICoding : boolean ,
575
- ) : Promise < void > {
576
- const apiKey = await this . options . getApiKey ( ) ;
577
- if ( apiKey ) {
578
- await this . _sendHeartbeat (
579
- doc ,
580
- time ,
581
- selection ,
582
- isWrite ,
583
- isCompiling ,
584
- isDebugging ,
585
- isAICoding ,
586
- ) ;
587
- } else {
588
- await this . promptForApiKey ( ) ;
589
- }
590
- }
591
-
592
- private async _sendHeartbeat (
592
+ private async appendHeartbeat (
593
593
doc : vscode . TextDocument ,
594
594
time : number ,
595
595
selection : vscode . Position ,
@@ -610,25 +610,75 @@ export class WakaTime {
610
610
// prevent sending the same heartbeat (https://github.com/wakatime/vscode-wakatime/issues/163)
611
611
if ( isWrite && this . isDuplicateHeartbeat ( file , time , selection ) ) return ;
612
612
613
+ const now = Date . now ( ) ;
614
+
615
+ const heartbeat : Heartbeat = {
616
+ entity : file ,
617
+ time : now / 1000 ,
618
+ is_write : isWrite ,
619
+ lineno : selection . line + 1 ,
620
+ cursorpos : selection . character + 1 ,
621
+ lines_in_file : doc . lineCount ,
622
+ } ;
623
+
624
+ if ( isDebugging ) {
625
+ heartbeat . category = 'debugging' ;
626
+ } else if ( isCompiling ) {
627
+ heartbeat . category = 'building' ;
628
+ } else if ( isAICoding ) {
629
+ heartbeat . category = 'ai coding' ;
630
+ } else if ( Utils . isPullRequest ( doc . uri ) ) {
631
+ heartbeat . category = 'code reviewing' ;
632
+ }
633
+
634
+ const project = this . getProjectName ( doc . uri ) ;
635
+ if ( project ) heartbeat . alternate_project = project ;
636
+
637
+ const folder = this . getProjectFolder ( doc . uri ) ;
638
+ if ( folder ) heartbeat . project_folder = folder ;
639
+
640
+ if ( doc . isUntitled ) heartbeat . is_unsaved_entity = true ;
641
+
642
+ this . logger . debug ( `Appending heartbeat to local buffer: ${ JSON . stringify ( heartbeat , null , 2 ) } ` ) ;
643
+ this . heartbeats . push ( heartbeat ) ;
644
+
645
+ if ( now - this . lastSent > SEND_BUFFER_SECONDS * 1000 ) {
646
+ await this . sendHeartbeats ( ) ;
647
+ }
648
+ }
649
+
650
+ private async sendHeartbeats ( ) : Promise < void > {
651
+ const apiKey = await this . options . getApiKey ( ) ;
652
+ if ( apiKey ) {
653
+ await this . _sendHeartbeats ( ) ;
654
+ } else {
655
+ await this . promptForApiKey ( ) ;
656
+ }
657
+ }
658
+
659
+ private async _sendHeartbeats ( ) : Promise < void > {
660
+ if ( ! this . dependencies . isCliInstalled ( ) ) return ;
661
+
662
+ const heartbeat = this . heartbeats . shift ( ) ;
663
+ if ( ! heartbeat ) return ;
664
+
665
+ this . lastSent = Date . now ( ) ;
666
+
613
667
let args : string [ ] = [ ] ;
614
668
615
- args . push ( '--entity' , Utils . quote ( file ) ) ;
669
+ args . push ( '--entity' , Utils . quote ( heartbeat . entity ) ) ;
670
+
671
+ args . push ( '--time' , String ( heartbeat . time ) ) ;
616
672
617
673
let user_agent =
618
674
this . agentName + '/' + vscode . version + ' vscode-wakatime/' + this . extension . version ;
619
675
args . push ( '--plugin' , Utils . quote ( user_agent ) ) ;
620
676
621
- args . push ( '--lineno' , String ( selection . line + 1 ) ) ;
622
- args . push ( '--cursorpos' , String ( selection . character + 1 ) ) ;
623
- args . push ( '--lines-in-file' , String ( doc . lineCount ) ) ;
624
- if ( isDebugging ) {
625
- args . push ( '--category' , 'debugging' ) ;
626
- } else if ( isCompiling ) {
627
- args . push ( '--category' , 'building' ) ;
628
- } else if ( isAICoding ) {
629
- args . push ( '--category' , 'ai coding' ) ;
630
- } else if ( Utils . isPullRequest ( doc . uri ) ) {
631
- args . push ( '--category' , 'code reviewing' ) ;
677
+ args . push ( '--lineno' , String ( heartbeat . lineno ) ) ;
678
+ args . push ( '--cursorpos' , String ( heartbeat . cursorpos ) ) ;
679
+ args . push ( '--lines-in-file' , String ( heartbeat . lines_in_file ) ) ;
680
+ if ( heartbeat . category ) {
681
+ args . push ( '--category' , heartbeat . category ) ;
632
682
}
633
683
634
684
if ( this . isMetricsEnabled ) args . push ( '--metrics' ) ;
@@ -639,13 +689,15 @@ export class WakaTime {
639
689
const apiUrl = await this . options . getApiUrl ( ) ;
640
690
if ( apiUrl ) args . push ( '--api-url' , Utils . quote ( apiUrl ) ) ;
641
691
642
- const project = this . getProjectName ( doc . uri ) ;
643
- if ( project ) args . push ( '--alternate-project' , Utils . quote ( project ) ) ;
692
+ if ( heartbeat . alternate_project ) {
693
+ args . push ( '--alternate-project' , Utils . quote ( heartbeat . alternate_project ) ) ;
694
+ }
644
695
645
- const folder = this . getProjectFolder ( doc . uri ) ;
646
- if ( folder ) args . push ( '--project-folder' , Utils . quote ( folder ) ) ;
696
+ if ( heartbeat . project_folder ) {
697
+ args . push ( '--project-folder' , Utils . quote ( heartbeat . project_folder ) ) ;
698
+ }
647
699
648
- if ( isWrite ) args . push ( '--write' ) ;
700
+ if ( heartbeat . is_write ) args . push ( '--write' ) ;
649
701
650
702
if ( Desktop . isWindows ( ) || Desktop . isPortable ( ) ) {
651
703
args . push (
@@ -656,18 +708,32 @@ export class WakaTime {
656
708
) ;
657
709
}
658
710
659
- if ( doc . isUntitled ) args . push ( '--is-unsaved-entity' ) ;
711
+ if ( heartbeat . is_unsaved_entity ) args . push ( '--is-unsaved-entity' ) ;
712
+
713
+ const extraHeartbeats = this . getExtraHeartbeats ( ) ;
714
+ if ( extraHeartbeats . length > 0 ) args . push ( '--extra-heartbeats' ) ;
660
715
661
716
const binary = this . dependencies . getCliLocation ( ) ;
662
717
this . logger . debug ( `Sending heartbeat: ${ Utils . formatArguments ( binary , args ) } ` ) ;
663
- const options = Desktop . buildOptions ( ) ;
718
+ const options = Desktop . buildOptions ( extraHeartbeats . length > 0 ) ;
664
719
let proc = child_process . execFile ( binary , args , options , ( error , stdout , stderr ) => {
665
720
if ( error != null ) {
666
721
if ( stderr && stderr . toString ( ) != '' ) this . logger . error ( stderr . toString ( ) ) ;
667
722
if ( stdout && stdout . toString ( ) != '' ) this . logger . error ( stdout . toString ( ) ) ;
668
723
this . logger . error ( error . toString ( ) ) ;
669
724
}
670
725
} ) ;
726
+
727
+ // send any extra heartbeats
728
+ if ( proc . stdin ) {
729
+ proc . stdin . write ( JSON . stringify ( extraHeartbeats ) ) ;
730
+ proc . stdin . write ( '\n' ) ;
731
+ proc . stdin . end ( ) ;
732
+ } else if ( extraHeartbeats . length > 0 ) {
733
+ this . logger . error ( 'Unable to set stdio[0] to pipe' ) ;
734
+ this . heartbeats . push ( ...extraHeartbeats ) ;
735
+ }
736
+
671
737
proc . on ( 'close' , async ( code , _signal ) => {
672
738
if ( code == 0 ) {
673
739
if ( this . showStatusBar ) this . getCodingActivity ( ) ;
@@ -712,6 +778,15 @@ export class WakaTime {
712
778
} ) ;
713
779
}
714
780
781
+ private getExtraHeartbeats ( ) {
782
+ const heartbeats : Heartbeat [ ] = [ ] ;
783
+ while ( true ) {
784
+ const h = this . heartbeats . shift ( ) ;
785
+ if ( ! h ) return heartbeats ;
786
+ heartbeats . push ( h ) ;
787
+ }
788
+ }
789
+
715
790
private async getCodingActivity ( ) {
716
791
if ( ! this . showStatusBar ) return ;
717
792
0 commit comments