diff --git a/cmd/heartbeat/heartbeat.go b/cmd/heartbeat/heartbeat.go index ef8847b7..6bde717d 100644 --- a/cmd/heartbeat/heartbeat.go +++ b/cmd/heartbeat/heartbeat.go @@ -105,6 +105,8 @@ func SendHeartbeats(v *viper.Viper, queueFilepath string) error { } handleOpts = append(handleOpts, offline.WithQueue(queueFilepath)) + } else if params.Offline.OfflineOnly { + return errors.New("--offline-only can NOT be used with --disable-offline") } handleOpts = append(handleOpts, backoff.WithBackoff(backoff.Config{ diff --git a/cmd/offline/offline.go b/cmd/offline/offline.go index 5680e4f9..8de134ee 100644 --- a/cmd/offline/offline.go +++ b/cmd/offline/offline.go @@ -18,9 +18,9 @@ import ( "github.com/spf13/viper" ) -// SaveHeartbeats saves heartbeats to the offline db, when we haven't -// tried sending them to the API. If we tried sending to API already, -// to the API. Used when we have heartbeats unsent to API. +// SaveHeartbeats saves heartbeats to the offline db without trying to send to the API. +// Used when we have more heartbeats than `offline.SendLimit`, when --offline-only enabled, +// when we couldn't send heartbeats to the API, or the API returned an auth error. func SaveHeartbeats(v *viper.Viper, heartbeats []heartbeat.Heartbeat, queueFilepath string) error { params, err := loadParams(v) if err != nil { diff --git a/cmd/params/params.go b/cmd/params/params.go index 07416d9a..f01dde0f 100644 --- a/cmd/params/params.go +++ b/cmd/params/params.go @@ -131,9 +131,10 @@ type ( // Offline contains offline related parameters. Offline struct { Disabled bool + OfflineOnly bool + PrintMax int QueueFile string QueueFileLegacy string - PrintMax int SyncMax int } @@ -656,9 +657,10 @@ func LoadOfflineParams(v *viper.Viper) Offline { return Offline{ Disabled: disabled, + OfflineOnly: v.GetBool("offline-only"), + PrintMax: v.GetInt("print-offline-heartbeats"), QueueFile: vipertools.GetString(v, "offline-queue-file"), QueueFileLegacy: vipertools.GetString(v, "offline-queue-file-legacy"), - PrintMax: v.GetInt("print-offline-heartbeats"), SyncMax: syncMax, } } @@ -1039,8 +1041,9 @@ func (p Heartbeat) String() string { // String implements fmt.Stringer interface. func (p Offline) String() string { return fmt.Sprintf( - "disabled: %t, print max: %d, queue file: '%s', queue file legacy: '%s', num sync max: %d", + "disabled: %t, offline only: %t, print max: %d, queue file: '%s', queue file legacy: '%s', num sync max: %d", p.Disabled, + p.OfflineOnly, p.PrintMax, p.QueueFile, p.QueueFileLegacy, diff --git a/cmd/params/params_test.go b/cmd/params/params_test.go index a7286bc4..55b07080 100644 --- a/cmd/params/params_test.go +++ b/cmd/params/params_test.go @@ -2505,7 +2505,7 @@ func TestOffline_String(t *testing.T) { assert.Equal( t, - "disabled: true, print max: 6, queue file: '/path/to/queue.file',"+ + "disabled: true, offline only: false, print max: 6, queue file: '/path/to/queue.file',"+ " queue file legacy: '/path/to/legacy.file', num sync max: 12", offline.String(), ) diff --git a/cmd/root.go b/cmd/root.go index ee0b5c1c..99eb9a42 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -230,6 +230,7 @@ func setFlags(cmd *cobra.Command, v *viper.Viper) { " new heartbeats.", offline.SyncMaxDefault), ) flags.Bool("offline-count", false, "Prints the number of heartbeats in the offline db, then exits.") + flags.Bool("offline-only", false, "Saves the heartbeat(s) to the offline db, then exits.") flags.Int( "timeout", api.DefaultTimeoutSecs, diff --git a/cmd/run.go b/cmd/run.go index 9c7baa9a..5b08a9c0 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -134,7 +134,13 @@ func Run(cmd *cobra.Command, v *viper.Viper) { if v.IsSet("entity") { log.Debugln("command: heartbeat") - RunCmdWithOfflineSync(v, logFileParams.Verbose, logFileParams.SendDiagsOnErrors, cmdheartbeat.Run, shutdown) + if v.GetBool("offline-only") { + saveHeartbeats(v) + shutdown() + os.Exit(exitcode.Success) + } else { + RunCmdWithOfflineSync(v, logFileParams.Verbose, logFileParams.SendDiagsOnErrors, cmdheartbeat.Run, shutdown) + } } if v.IsSet("sync-offline-activity") { diff --git a/main_test.go b/main_test.go index 029deedf..4dc22092 100644 --- a/main_test.go +++ b/main_test.go @@ -458,6 +458,62 @@ func TestSendHeartbeats_ExtraHeartbeats_SyncLegacyOfflineActivity(t *testing.T) assert.Eventually(t, func() bool { return numCalls == 3 }, time.Second, 50*time.Millisecond) } +func TestSendHeartbeats_OfflineOnly(t *testing.T) { + apiURL, router, close := setupTestServer() + defer close() + + router.HandleFunc("/users/current/heartbeats.bulk", func(w http.ResponseWriter, req *http.Request) { + require.FailNow(t, "Should not make any API request") + }) + + tmpDir := t.TempDir() + + offlineQueueFile, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFile.Close() + + tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg") + require.NoError(t, err) + + defer tmpConfigFile.Close() + + tmpInternalConfigFile, err := os.CreateTemp(tmpDir, "wakatime-internal.cfg") + require.NoError(t, err) + + defer tmpInternalConfigFile.Close() + + data, err := os.ReadFile("testdata/extra_heartbeats.json") + require.NoError(t, err) + + buffer := bytes.NewBuffer(data) + + runWakatimeCli( + t, + buffer, + "--api-url", apiURL, + "--key", "00000000-0000-4000-8000-000000000000", + "--config", tmpConfigFile.Name(), + "--internal-config", tmpInternalConfigFile.Name(), + "--entity", "testdata/main.go", + "--extra-heartbeats", "true", + "--cursorpos", "12", + "--offline-queue-file", offlineQueueFile.Name(), + "--offline-only", + "--lineno", "42", + "--lines-in-file", "100", + "--time", "1585598059", + "--hide-branch-names", ".*", + "--write", + "--verbose", + ) + + offlineCount, err := offline.CountHeartbeats(offlineQueueFile.Name()) + require.NoError(t, err) + + assert.Equal(t, 27, offlineCount) +} + func TestSendHeartbeats_Err(t *testing.T) { apiURL, router, close := setupTestServer() defer close() diff --git a/pkg/heartbeat/heartbeat.go b/pkg/heartbeat/heartbeat.go index 17892717..32918b5c 100644 --- a/pkg/heartbeat/heartbeat.go +++ b/pkg/heartbeat/heartbeat.go @@ -113,8 +113,14 @@ func (h Heartbeat) ID() string { isWrite = *h.IsWrite } - return fmt.Sprintf("%f-%s-%s-%s-%s-%s-%t", + cursorPos := "nil" + if h.CursorPosition != nil { + cursorPos = fmt.Sprint(*h.CursorPosition) + } + + return fmt.Sprintf("%f-%s-%s-%s-%s-%s-%s-%t", h.Time, + cursorPos, h.EntityType, h.Category, project, diff --git a/pkg/heartbeat/heartbeat_test.go b/pkg/heartbeat/heartbeat_test.go index c987d1f3..135972a4 100644 --- a/pkg/heartbeat/heartbeat_test.go +++ b/pkg/heartbeat/heartbeat_test.go @@ -75,7 +75,7 @@ func TestHeartbeat_ID(t *testing.T) { Project: heartbeat.PointerTo("wakatime"), Time: 1592868313.541149, } - assert.Equal(t, "1592868313.541149-file-coding-wakatime-heartbeat-/tmp/main.go-true", h.ID()) + assert.Equal(t, "1592868313.541149-nil-file-coding-wakatime-heartbeat-/tmp/main.go-true", h.ID()) } func TestHeartbeat_ID_NilFields(t *testing.T) { @@ -85,7 +85,7 @@ func TestHeartbeat_ID_NilFields(t *testing.T) { EntityType: heartbeat.FileType, Time: 1592868313.541149, } - assert.Equal(t, "1592868313.541149-file-coding-unset-unset-/tmp/main.go-false", h.ID()) + assert.Equal(t, "1592868313.541149-nil-file-coding-unset-unset-/tmp/main.go-false", h.ID()) } func TestHeartbeat_JSON(t *testing.T) { diff --git a/pkg/offline/offline.go b/pkg/offline/offline.go index 9a9b12e0..59de11c9 100644 --- a/pkg/offline/offline.go +++ b/pkg/offline/offline.go @@ -62,14 +62,13 @@ func WithQueue(filepath string) heartbeat.HandleOption { results, err := next(hh) if err != nil { - log.Debugf("pushing %d heartbeat(s) to queue due to error", len(hh)) + log.Debugf("pushing %d heartbeat(s) to queue after error: %s", len(hh), err) requeueErr := pushHeartbeatsWithRetry(filepath, hh) if requeueErr != nil { return nil, fmt.Errorf( - "failed to push heartbeats to queue after api error: %w. error: %w", + "failed to push heartbeats to queue: %s", requeueErr, - err, ) } diff --git a/pkg/offline/offline_test.go b/pkg/offline/offline_test.go index 70f9842c..7acac098 100644 --- a/pkg/offline/offline_test.go +++ b/pkg/offline/offline_test.go @@ -221,10 +221,10 @@ func TestWithQueue_ApiError(t *testing.T) { require.Len(t, stored, 2) - assert.Equal(t, "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", stored[0].ID) + assert.Equal(t, "1592868367.219124-12-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", stored[0].ID) assert.JSONEq(t, string(dataGo), stored[0].Heartbeat) - assert.Equal(t, "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", stored[1].ID) + assert.Equal(t, "1592868386.079084-13-file-debugging-wakatime-summary-/tmp/main.py-false", stored[1].ID) assert.JSONEq(t, string(dataPy), stored[1].Heartbeat) } @@ -307,10 +307,10 @@ func TestWithQueue_InvalidResults(t *testing.T) { assert.Len(t, stored, 2) - assert.Equal(t, "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", stored[0].ID) + assert.Equal(t, "1592868386.079084-13-file-debugging-wakatime-summary-/tmp/main.py-false", stored[0].ID) assert.JSONEq(t, string(dataPy), stored[0].Heartbeat) - assert.Equal(t, "1592868394.084354-file-building-wakatime-todaygoal-/tmp/main.js-false", stored[1].ID) + assert.Equal(t, "1592868394.084354-14-file-building-wakatime-todaygoal-/tmp/main.js-false", stored[1].ID) assert.JSONEq(t, string(dataJs), stored[1].Heartbeat) } @@ -376,10 +376,10 @@ func TestWithQueue_HandleLeftovers(t *testing.T) { require.Len(t, stored, 2) - assert.Equal(t, "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", stored[0].ID) + assert.Equal(t, "1592868386.079084-13-file-debugging-wakatime-summary-/tmp/main.py-false", stored[0].ID) assert.JSONEq(t, string(dataPy), stored[0].Heartbeat) - assert.Equal(t, "1592868394.084354-file-building-wakatime-todaygoal-/tmp/main.js-false", stored[1].ID) + assert.Equal(t, "1592868394.084354-14-file-building-wakatime-todaygoal-/tmp/main.js-false", stored[1].ID) assert.JSONEq(t, string(dataJs), stored[1].Heartbeat) } @@ -401,11 +401,11 @@ func TestWithSync(t *testing.T) { insertHeartbeatRecords(t, db, "heartbeats", []heartbeatRecord{ { - ID: "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", + ID: "1592868367.219124-12-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", Heartbeat: string(dataGo), }, { - ID: "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", + ID: "1592868386.079084-13-file-debugging-wakatime-summary-/tmp/main.py-false", Heartbeat: string(dataPy), }, }) @@ -571,11 +571,11 @@ func TestSync_APIError(t *testing.T) { insertHeartbeatRecords(t, db, "heartbeats", []heartbeatRecord{ { - ID: "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", + ID: "1592868367.219124-12-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", Heartbeat: string(dataGo), }, { - ID: "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", + ID: "1592868386.079084-13-file-debugging-wakatime-summary-/tmp/main.py-false", Heartbeat: string(dataPy), }, }) @@ -625,10 +625,10 @@ func TestSync_APIError(t *testing.T) { require.Len(t, stored, 2) - assert.Equal(t, "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", stored[0].ID) + assert.Equal(t, "1592868367.219124-12-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", stored[0].ID) assert.JSONEq(t, string(dataGo), stored[0].Heartbeat) - assert.Equal(t, "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", stored[1].ID) + assert.Equal(t, "1592868386.079084-13-file-debugging-wakatime-summary-/tmp/main.py-false", stored[1].ID) assert.JSONEq(t, string(dataPy), stored[1].Heartbeat) assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) @@ -1189,7 +1189,7 @@ func TestQueue_PushMany(t *testing.T) { require.NoError(t, err) insertHeartbeatRecord(t, db, "test_bucket", heartbeatRecord{ - ID: "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", + ID: "1592868367.219124-1-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", Heartbeat: string(dataGo), }) @@ -1240,13 +1240,13 @@ func TestQueue_PushMany(t *testing.T) { assert.Len(t, stored, 3) - assert.Equal(t, "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", stored[0].ID) + assert.Equal(t, "1592868367.219124-1-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", stored[0].ID) assert.JSONEq(t, string(dataGo), stored[0].Heartbeat) - assert.Equal(t, "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", stored[1].ID) + assert.Equal(t, "1592868386.079084-13-file-debugging-wakatime-summary-/tmp/main.py-false", stored[1].ID) assert.JSONEq(t, string(dataPy), stored[1].Heartbeat) - assert.Equal(t, "1592868394.084354-file-building-wakatime-todaygoal-/tmp/main.js-false", stored[2].ID) + assert.Equal(t, "1592868394.084354-14-file-building-wakatime-todaygoal-/tmp/main.js-false", stored[2].ID) assert.JSONEq(t, string(dataJs), stored[2].Heartbeat) } diff --git a/testdata/extra_heartbeats.json b/testdata/extra_heartbeats.json index af299b03..7fd1dfed 100644 --- a/testdata/extra_heartbeats.json +++ b/testdata/extra_heartbeats.json @@ -1 +1 @@ -[{ "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "line_additions": 123, "line_deletions": 456, "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "vscode-wakatime", "time": 1585598059 }] +[{ "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 1, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "line_additions": 123, "line_deletions": 456, "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 2, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 3, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 4, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 5, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 6, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 7, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 8, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 9, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 10, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 11, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 12, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 13, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 14, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 15, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 16, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 17, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 18, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 19, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 20, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 21, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 22, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 23, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 24, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 25, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "wakatime-cli", "time": 1585598059 }, { "alternate_language": "Golang", "alternate_project": "billing", "category": "coding", "cursorpos": 26, "entity": "testdata/main.go", "entity_type": "file", "is_write": true, "language": "Go", "lineno": 42, "lines": 45, "project": "vscode-wakatime", "time": 1585598059 }]