diff --git a/USAGE.md b/USAGE.md index 7df178f4..eb438dd5 100644 --- a/USAGE.md +++ b/USAGE.md @@ -102,7 +102,7 @@ some/submodule/name = new project name | status_bar_enabled | Turns on wakatime status bar for certain editors. | _bool_ | `true` | | status_bar_coding_activity | Enables displaying Today's code stats in the status bar of some editors. When false, only the WakaTime icon is displayed in the status bar. | _bool_ | `true` | | status_bar_hide_categories | When `true`, --today only displays the total code stats, never displaying Categories in the output. | _bool_ | `false` | -| offline | Enables saving code stats locally to ~/.wakatime.bdb when offline, and syncing to the dashboard later when back online. | _bool_ | `true` | +| offline | Enables saving code stats locally to ~/.wakatime/offline_heartbeats.bdb when offline, and syncing to the dashboard later when back online. | _bool_ | `true` | | proxy | Optional proxy configuration. Supports HTTPS, SOCKS and NTLM proxies. For ex: `https://user:pass@host:port`, `socks5://user:pass@host:port`, `domain\\user:pass` | _string_ | | | no_ssl_verify | Disables SSL certificate verification for HTTPS requests. By default, SSL certificates are verified. | _bool_ | `false` | | ssl_certs_file | Path to a CA certs file. By default, uses bundled Letsencrypt CA cert along with system ca certs. | _filepath_ | | diff --git a/cmd/offlinesync/offlinesync.go b/cmd/offlinesync/offlinesync.go index e29b1bf8..2f0e20c0 100644 --- a/cmd/offlinesync/offlinesync.go +++ b/cmd/offlinesync/offlinesync.go @@ -2,6 +2,7 @@ package offlinesync import ( "fmt" + "os" cmdapi "github.com/wakatime/wakatime-cli/cmd/api" "github.com/wakatime/wakatime-cli/cmd/params" @@ -25,8 +26,16 @@ func Run(v *viper.Viper) (int, error) { ) } - err = SyncOfflineActivity(v, queueFilepath) + queueFilepathLegacy, err := offline.QueueFilepathLegacy() if err != nil { + log.Warnf("legacy offline sync failed: failed to load offline queue filepath: %s", err) + } + + if err = syncOfflineActivityLegacy(v, queueFilepathLegacy); err != nil { + log.Warnf("legacy offline sync failed: %s", err) + } + + if err = SyncOfflineActivity(v, queueFilepath); err != nil { if errwaka, ok := err.(wakaerror.Error); ok { return errwaka.ExitCode(), fmt.Errorf("offline sync failed: %s", errwaka.Message()) } @@ -42,6 +51,49 @@ func Run(v *viper.Viper) (int, error) { return exitcode.Success, nil } +// syncOfflineActivityLegacy syncs the old offline activity by sending heartbeats +// from the legacy offline queue to the WakaTime API. +func syncOfflineActivityLegacy(v *viper.Viper, queueFilepath string) error { + if queueFilepath == "" { + return nil + } + + paramOffline := params.LoadOfflineParams(v) + + paramAPI, err := params.LoadAPIParams(v) + if err != nil { + return fmt.Errorf("failed to load API parameters: %w", err) + } + + apiClient, err := cmdapi.NewClientWithoutAuth(paramAPI) + if err != nil { + return fmt.Errorf("failed to initialize api client: %w", err) + } + + if paramOffline.QueueFileLegacy != "" { + queueFilepath = paramOffline.QueueFileLegacy + } + + handle := heartbeat.NewHandle(apiClient, + offline.WithSync(queueFilepath, paramOffline.SyncMax), + apikey.WithReplacing(apikey.Config{ + DefaultAPIKey: paramAPI.Key, + MapPatterns: paramAPI.KeyPatterns, + }), + ) + + _, err = handle(nil) + if err != nil { + return err + } + + if err := os.Remove(queueFilepath); err != nil { + log.Errorf("failed to delete legacy offline file: %s", err) + } + + return nil +} + // SyncOfflineActivity syncs offline activity by sending heartbeats // from the offline queue to the WakaTime API. func SyncOfflineActivity(v *viper.Viper, queueFilepath string) error { diff --git a/cmd/offlinesync/offlinesync_internal_test.go b/cmd/offlinesync/offlinesync_internal_test.go new file mode 100644 index 00000000..7f96baff --- /dev/null +++ b/cmd/offlinesync/offlinesync_internal_test.go @@ -0,0 +1,144 @@ +package offlinesync + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" +) + +func TestSyncOfflineActivityLegacy(t *testing.T) { + testServerURL, router, tearDown := setupTestServer() + defer tearDown() + + var ( + plugin = "plugin/0.0.1" + numCalls int + ) + + router.HandleFunc("/users/current/heartbeats.bulk", func(w http.ResponseWriter, req *http.Request) { + numCalls++ + + // check request + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, []string{"application/json"}, req.Header["Accept"]) + assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) + assert.True(t, strings.HasSuffix(req.Header["User-Agent"][0], plugin), fmt.Sprintf( + "%q should have suffix %q", + req.Header["User-Agent"][0], + plugin, + )) + + expectedBody, err := os.ReadFile("testdata/api_heartbeats_request_template.json") + require.NoError(t, err) + + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + assert.JSONEq(t, string(expectedBody), string(body)) + + // send response + w.WriteHeader(http.StatusCreated) + + f, err := os.Open("testdata/api_heartbeats_response.json") + require.NoError(t, err) + defer f.Close() + + _, err = io.Copy(w, f) + require.NoError(t, err) + }) + + // setup offline queue + f, err := os.CreateTemp(t.TempDir(), "") + require.NoError(t, err) + + db, err := bolt.Open(f.Name(), 0600, nil) + require.NoError(t, err) + + dataGo, err := os.ReadFile("testdata/heartbeat_go.json") + require.NoError(t, err) + + dataPy, err := os.ReadFile("testdata/heartbeat_py.json") + require.NoError(t, err) + + dataJs, err := os.ReadFile("testdata/heartbeat_js.json") + require.NoError(t, err) + + insertHeartbeatRecords(t, db, "heartbeats", []heartbeatRecord{ + { + ID: "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", + Heartbeat: string(dataGo), + }, + { + ID: "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", + Heartbeat: string(dataPy), + }, + { + ID: "1592868394.084354-file-building-wakatime-todaygoal-/tmp/main.js-false", + Heartbeat: string(dataJs), + }, + }) + + err = db.Close() + require.NoError(t, err) + + v := viper.New() + v.Set("api-url", testServerURL) + v.Set("key", "00000000-0000-4000-8000-000000000000") + v.Set("sync-offline-activity", 100) + v.Set("plugin", plugin) + + err = syncOfflineActivityLegacy(v, f.Name()) + require.NoError(t, err) + + assert.NoFileExists(t, f.Name()) + + assert.Eventually(t, func() bool { return numCalls == 1 }, time.Second, 50*time.Millisecond) +} + +func setupTestServer() (string, *http.ServeMux, func()) { + router := http.NewServeMux() + srv := httptest.NewServer(router) + + return srv.URL, router, func() { srv.Close() } +} + +type heartbeatRecord struct { + ID string + Heartbeat string +} + +func insertHeartbeatRecords(t *testing.T, db *bolt.DB, bucket string, hh []heartbeatRecord) { + for _, h := range hh { + insertHeartbeatRecord(t, db, bucket, h) + } +} + +func insertHeartbeatRecord(t *testing.T, db *bolt.DB, bucket string, h heartbeatRecord) { + t.Helper() + + err := db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte(bucket)) + if err != nil { + return fmt.Errorf("failed to create bucket: %s", err) + } + + err = b.Put([]byte(h.ID), []byte(h.Heartbeat)) + if err != nil { + return fmt.Errorf("failed put heartbeat: %s", err) + } + + return nil + }) + require.NoError(t, err) +} diff --git a/cmd/params/params.go b/cmd/params/params.go index acadc0a6..bf818dd3 100644 --- a/cmd/params/params.go +++ b/cmd/params/params.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/url" "os" "os/exec" @@ -129,10 +130,11 @@ type ( // Offline contains offline related parameters. Offline struct { - Disabled bool - QueueFile string - PrintMax int - SyncMax int + Disabled bool + QueueFile string + QueueFileLegacy string + PrintMax int + SyncMax int } // ProjectParams params for project name sanitization. @@ -653,10 +655,11 @@ func LoadOfflineParams(v *viper.Viper) Offline { } return Offline{ - Disabled: disabled, - QueueFile: vipertools.GetString(v, "offline-queue-file"), - PrintMax: v.GetInt("print-offline-heartbeats"), - SyncMax: syncMax, + Disabled: disabled, + QueueFile: vipertools.GetString(v, "offline-queue-file"), + QueueFileLegacy: vipertools.GetString(v, "offline-queue-file-legacy"), + PrintMax: v.GetInt("print-offline-heartbeats"), + SyncMax: syncMax, } } @@ -730,7 +733,7 @@ func readExtraHeartbeats() ([]heartbeat.Heartbeat, error) { in := bufio.NewReader(os.Stdin) input, err := in.ReadString('\n') - if err != nil { + if err != nil && err != io.EOF { log.Debugf("failed to read data from stdin: %s", err) } diff --git a/cmd/params/params_test.go b/cmd/params/params_test.go index 846d4079..f9e14870 100644 --- a/cmd/params/params_test.go +++ b/cmd/params/params_test.go @@ -1687,6 +1687,15 @@ func TestLoad_OfflineQueueFile(t *testing.T) { assert.Equal(t, "/path/to/file", params.QueueFile) } +func TestLoad_OfflineQueueFileLegacy(t *testing.T) { + v := viper.New() + v.Set("offline-queue-file-legacy", "/path/to/file") + + params := paramscmd.LoadOfflineParams(v) + + assert.Equal(t, "/path/to/file", params.QueueFileLegacy) +} + func TestLoad_OfflineSyncMax(t *testing.T) { v := viper.New() v.Set("sync-offline-activity", 42) diff --git a/cmd/root.go b/cmd/root.go index 71f56079..ee0b5c1c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -181,6 +181,11 @@ func setFlags(cmd *cobra.Command, v *viper.Viper) { "", "(internal) Specify an offline queue file, which will be used instead of the default one.", ) + flags.String( + "offline-queue-file-legacy", + "", + "(internal) Specify the legacy offline queue file, which will be used instead of the default one.", + ) flags.String( "output", "", @@ -216,7 +221,7 @@ func setFlags(cmd *cobra.Command, v *viper.Viper) { flags.Int( "sync-offline-activity", offline.SyncMaxDefault, - fmt.Sprintf("Amount of offline activity to sync from your local ~/.wakatime.bdb bolt"+ + fmt.Sprintf("Amount of offline activity to sync from your local ~/.wakatime/offline_heartbeats.bdb bolt"+ " file to your WakaTime Dashboard before exiting. Can be zero or"+ " a positive integer. Defaults to %d, meaning after sending a heartbeat"+ " while online, all queued offline heartbeats are sent to WakaTime API, up"+ @@ -259,6 +264,7 @@ func setFlags(cmd *cobra.Command, v *viper.Viper) { // hide internal flags _ = flags.MarkHidden("offline-queue-file") + _ = flags.MarkHidden("offline-queue-file-legacy") _ = flags.MarkHidden("user-agent") err := v.BindPFlags(flags) diff --git a/go.mod b/go.mod index 3aa50884..7195f9ad 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/wakatime/wakatime-cli -go 1.22.4 +go 1.22.5 require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 diff --git a/go.sum b/go.sum index db24544b..a3d6fc8f 100644 --- a/go.sum +++ b/go.sum @@ -296,8 +296,6 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -370,8 +368,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -435,16 +431,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -454,8 +447,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/main_test.go b/main_test.go index d7272753..2cb8e24e 100644 --- a/main_test.go +++ b/main_test.go @@ -28,6 +28,7 @@ import ( "github.com/gandarez/go-realpath" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" ) // nolint:gochecknoinits @@ -109,6 +110,11 @@ func testSendHeartbeats(t *testing.T, projectFolder, entity, p string) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg") require.NoError(t, err) @@ -129,6 +135,7 @@ func testSendHeartbeats(t *testing.T, projectFolder, entity, p string) { "--entity", entity, "--cursorpos", "12", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--line-additions", "123", "--line-deletions", "456", "--lineno", "42", @@ -200,6 +207,11 @@ func TestSendHeartbeats_SecondaryApiKey(t *testing.T) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + tmpInternalConfigFile, err := os.CreateTemp(tmpDir, "wakatime-internal.cfg") require.NoError(t, err) @@ -215,6 +227,7 @@ func TestSendHeartbeats_SecondaryApiKey(t *testing.T) { "--entity", "testdata/main.go", "--cursorpos", "12", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--line-additions", "123", "--line-deletions", "456", "--lineno", "42", @@ -245,8 +258,17 @@ func TestSendHeartbeats_ExtraHeartbeats(t *testing.T) { assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) assert.Equal(t, []string{heartbeat.UserAgent("")}, req.Header["User-Agent"]) + var filename string + + switch numCalls { + case 1: + filename = "testdata/api_heartbeats_response_extra_heartbeats.json" + case 2: + filename = "testdata/api_heartbeats_response_extra_heartbeats_extra.json" + } + // write response - f, err := os.Open("testdata/api_heartbeats_response_extra_heartbeats.json") + f, err := os.Open(filename) require.NoError(t, err) w.WriteHeader(http.StatusCreated) @@ -261,6 +283,11 @@ func TestSendHeartbeats_ExtraHeartbeats(t *testing.T) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg") require.NoError(t, err) @@ -286,8 +313,9 @@ func TestSendHeartbeats_ExtraHeartbeats(t *testing.T) { "--entity", "testdata/main.go", "--extra-heartbeats", "true", "--cursorpos", "12", - "--sync-offline-activity", "1", + "--sync-offline-activity", "2", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--lineno", "42", "--lines-in-file", "100", "--time", "1585598059", @@ -299,11 +327,139 @@ func TestSendHeartbeats_ExtraHeartbeats(t *testing.T) { offlineCount, err := offline.CountHeartbeats(offlineQueueFile.Name()) require.NoError(t, err) - assert.Equal(t, 1, offlineCount) + assert.Zero(t, offlineCount) assert.Eventually(t, func() bool { return numCalls == 2 }, time.Second, 50*time.Millisecond) } +func TestSendHeartbeats_ExtraHeartbeats_SyncLegacyOfflineActivity(t *testing.T) { + apiURL, router, close := setupTestServer() + defer close() + + var numCalls int + + router.HandleFunc("/users/current/heartbeats.bulk", func(w http.ResponseWriter, req *http.Request) { + numCalls++ + + // check headers + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, []string{"application/json"}, req.Header["Accept"]) + assert.Equal(t, []string{"application/json"}, req.Header["Content-Type"]) + assert.Equal(t, []string{"Basic MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw"}, req.Header["Authorization"]) + assert.Equal(t, []string{heartbeat.UserAgent("")}, req.Header["User-Agent"]) + + var filename string + + switch numCalls { + case 1: + filename = "testdata/api_heartbeats_response_extra_heartbeats.json" + case 2: + filename = "testdata/api_heartbeats_response_extra_heartbeats_legacy_offline.json" + case 3: + filename = "testdata/api_heartbeats_response_extra_heartbeats_extra.json" + } + + // write response + f, err := os.Open(filename) + require.NoError(t, err) + + w.WriteHeader(http.StatusCreated) + _, err = io.Copy(w, f) + require.NoError(t, err) + }) + + tmpDir := t.TempDir() + + // create legacy offline queue file and add some heartbeats + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + + db, err := bolt.Open(offlineQueueFileLegacy.Name(), 0600, nil) + require.NoError(t, err) + + dataGo, err := os.ReadFile("testdata/heartbeat_go.json") + require.NoError(t, err) + + dataPy, err := os.ReadFile("testdata/heartbeat_py.json") + require.NoError(t, err) + + dataJs, err := os.ReadFile("testdata/heartbeat_js.json") + require.NoError(t, err) + + insertHeartbeatRecords(t, db, "heartbeats", []heartbeatRecord{ + { + ID: "1592868367.219124-file-coding-wakatime-cli-heartbeat-/tmp/main.go-true", + Heartbeat: string(dataGo), + }, + { + ID: "1592868386.079084-file-debugging-wakatime-summary-/tmp/main.py-false", + Heartbeat: string(dataPy), + }, + { + ID: "1592868394.084354-file-building-wakatime-todaygoal-/tmp/main.js-false", + Heartbeat: string(dataJs), + }, + }) + + err = db.Close() + require.NoError(t, err) + + 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", + "--sync-offline-activity", "0", + "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), + "--lineno", "42", + "--lines-in-file", "100", + "--time", "1585598059", + "--hide-branch-names", ".*", + "--write", + "--verbose", + ) + + // windows locks the file, so we safely bypass it + if runtime.GOOS != "windows" { + assert.NoFileExists(t, offlineQueueFileLegacy.Name()) + } + + offlineCount, err := offline.CountHeartbeats(offlineQueueFile.Name()) + require.NoError(t, err) + + assert.Zero(t, offlineCount) + + assert.Eventually(t, func() bool { return numCalls == 3 }, time.Second, 50*time.Millisecond) +} + func TestSendHeartbeats_Err(t *testing.T) { apiURL, router, close := setupTestServer() defer close() @@ -357,6 +513,11 @@ func TestSendHeartbeats_Err(t *testing.T) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg") require.NoError(t, err) @@ -377,6 +538,7 @@ func TestSendHeartbeats_Err(t *testing.T) { "--entity", "testdata/main.go", "--cursorpos", "12", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--line-additions", "123", "--line-deletions", "456", "--lineno", "42", @@ -410,6 +572,11 @@ func TestSendHeartbeats_ErrAuth_InvalidAPIKEY(t *testing.T) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg") require.NoError(t, err) @@ -430,6 +597,7 @@ func TestSendHeartbeats_ErrAuth_InvalidAPIKEY(t *testing.T) { "--entity", "testdata/main.go", "--cursorpos", "12", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--lineno", "42", "--lines-in-file", "100", "--time", "1585598059", @@ -462,6 +630,11 @@ func TestSendHeartbeats_MalformedConfig(t *testing.T) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + out := runWakatimeCliExpectErr( t, exitcode.ErrConfigFileParse, @@ -469,6 +642,7 @@ func TestSendHeartbeats_MalformedConfig(t *testing.T) { "--config", "./testdata/malformed.cfg", "--internal-config", tmpInternalConfigFile.Name(), "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--verbose", ) @@ -488,6 +662,11 @@ func TestSendHeartbeats_MalformedInternalConfig(t *testing.T) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg") require.NoError(t, err) @@ -500,6 +679,7 @@ func TestSendHeartbeats_MalformedInternalConfig(t *testing.T) { "--config", tmpConfigFile.Name(), "--internal-config", "./testdata/internal-malformed.cfg", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--verbose", ) @@ -512,8 +692,6 @@ func TestSendHeartbeats_MalformedInternalConfig(t *testing.T) { } func TestFileExperts(t *testing.T) { - t.Skip() - apiURL, router, close := setupTestServer() defer close() @@ -715,6 +893,11 @@ func TestOfflineCount(t *testing.T) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg") require.NoError(t, err) @@ -735,6 +918,7 @@ func TestOfflineCount(t *testing.T) { "--entity", "testdata/main.go", "--cursorpos", "12", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--lineno", "42", "--lines-in-file", "100", "--time", "1585598059", @@ -752,6 +936,7 @@ func TestOfflineCount(t *testing.T) { "--config", tmpConfigFile.Name(), "--internal-config", tmpInternalConfigFile.Name(), "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--offline-count", "--verbose", ) @@ -760,16 +945,24 @@ func TestOfflineCount(t *testing.T) { } func TestOfflineCountEmpty(t *testing.T) { - offlineQueueFile, err := os.CreateTemp(t.TempDir(), "") + tmpDir := t.TempDir() + + offlineQueueFile, err := os.CreateTemp(tmpDir, "") require.NoError(t, err) defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + out := runWakatimeCli( t, &bytes.Buffer{}, "--key", "00000000-0000-4000-8000-000000000000", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--offline-count", "--verbose", ) @@ -794,6 +987,11 @@ func TestPrintOfflineHeartbeats(t *testing.T) { defer offlineQueueFile.Close() + offlineQueueFileLegacy, err := os.CreateTemp(tmpDir, "") + require.NoError(t, err) + + defer offlineQueueFileLegacy.Close() + tmpConfigFile, err := os.CreateTemp(tmpDir, "wakatime.cfg") require.NoError(t, err) @@ -814,6 +1012,7 @@ func TestPrintOfflineHeartbeats(t *testing.T) { "--entity", "testdata/main.go", "--cursorpos", "12", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--lineno", "42", "--lines-in-file", "100", "--time", "1585598059", @@ -830,6 +1029,7 @@ func TestPrintOfflineHeartbeats(t *testing.T) { &bytes.Buffer{}, "--key", "00000000-0000-4000-8000-000000000000", "--offline-queue-file", offlineQueueFile.Name(), + "--offline-queue-file-legacy", offlineQueueFileLegacy.Name(), "--print-offline-heartbeats", "10", "--verbose", ) @@ -841,8 +1041,6 @@ func TestPrintOfflineHeartbeats(t *testing.T) { entity = windows.FormatFilePath(entity) } - t.Logf("entity: %s", entity) - projectFolder, err := filepath.Abs(".") require.NoError(t, err) @@ -987,7 +1185,8 @@ func runCmd(cmd *exec.Cmd, buffer *bytes.Buffer) string { fmt.Println(stdout.String()) fmt.Println(stderr.String()) fmt.Printf("failed to run command %s: %s\n", cmd, err) - os.Exit(1) + + return "" } return stdout.String() @@ -1007,7 +1206,8 @@ func runCmdExpectErr(cmd *exec.Cmd) (string, int) { fmt.Println(stdout.String()) fmt.Println(stderr.String()) fmt.Printf("ran command successfully, but was expecting error: %s\n", cmd) - os.Exit(1) + + return "", -1 } if exitcode, ok := err.(*exec.ExitError); ok { @@ -1027,3 +1227,33 @@ func setupTestServer() (string, *http.ServeMux, func()) { return srv.URL, router, func() { srv.Close() } } + +type heartbeatRecord struct { + ID string + Heartbeat string +} + +func insertHeartbeatRecords(t *testing.T, db *bolt.DB, bucket string, hh []heartbeatRecord) { + for _, h := range hh { + insertHeartbeatRecord(t, db, bucket, h) + } +} + +func insertHeartbeatRecord(t *testing.T, db *bolt.DB, bucket string, h heartbeatRecord) { + t.Helper() + + err := db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte(bucket)) + if err != nil { + return fmt.Errorf("failed to create bucket: %s", err) + } + + err = b.Put([]byte(h.ID), []byte(h.Heartbeat)) + if err != nil { + return fmt.Errorf("failed put heartbeat: %s", err) + } + + return nil + }) + require.NoError(t, err) +} diff --git a/pkg/offline/legacy.go b/pkg/offline/legacy.go new file mode 100644 index 00000000..1f64ebc6 --- /dev/null +++ b/pkg/offline/legacy.go @@ -0,0 +1,24 @@ +package offline + +import ( + "fmt" + "path/filepath" + + "github.com/wakatime/wakatime-cli/pkg/ini" +) + +// dbLegacyFilename is the legacy bolt db filename. +const dbLegacyFilename = ".wakatime.bdb" + +// QueueFilepathLegacy returns the legacy path for offline queue db file. If +// the waka's resource directory cannot be detected, it defaults to the +// current directory. +// This is used to support the old db file name and will be removed in the future. +func QueueFilepathLegacy() (string, error) { + home, _, err := ini.WakaHomeDir() + if err != nil { + return dbFilename, fmt.Errorf("failed getting user's home directory, defaulting to current directory: %s", err) + } + + return filepath.Join(home, dbLegacyFilename), nil +} diff --git a/pkg/offline/legacy_test.go b/pkg/offline/legacy_test.go new file mode 100644 index 00000000..86787452 --- /dev/null +++ b/pkg/offline/legacy_test.go @@ -0,0 +1,49 @@ +package offline_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/wakatime/wakatime-cli/pkg/offline" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQueueFilepathLegacy(t *testing.T) { + home, err := os.UserHomeDir() + require.NoError(t, err) + + tests := map[string]struct { + ViperValue string + EnvVar string + Expected string + }{ + "default": { + Expected: filepath.Join(home, ".wakatime.bdb"), + }, + "env_trailing_slash": { + EnvVar: "~/path2/", + Expected: filepath.Join(home, "path2", ".wakatime.bdb"), + }, + "env_without_trailing_slash": { + EnvVar: "~/path2", + Expected: filepath.Join(home, "path2", ".wakatime.bdb"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := os.Setenv("WAKATIME_HOME", test.EnvVar) + require.NoError(t, err) + + defer os.Unsetenv("WAKATIME_HOME") + + queueFilepath, err := offline.QueueFilepathLegacy() + require.NoError(t, err) + + assert.Equal(t, test.Expected, queueFilepath) + }) + } +} diff --git a/pkg/offline/offline.go b/pkg/offline/offline.go index 0f55338d..61719f2a 100644 --- a/pkg/offline/offline.go +++ b/pkg/offline/offline.go @@ -19,7 +19,7 @@ import ( const ( // dbFilename is the default bolt db filename. - dbFilename = ".wakatime.bdb" + dbFilename = "offline_heartbeats.bdb" // dbBucket is the standard bolt db bucket name. dbBucket = "heartbeats" // maxRequeueAttempts defines the maximum number of attempts to requeue heartbeats, @@ -87,15 +87,15 @@ func WithQueue(filepath string) heartbeat.HandleOption { } // QueueFilepath returns the path for offline queue db file. If -// the user's $HOME folder cannot be detected, it defaults to the +// the waka's resource directory cannot be detected, it defaults to the // current directory. func QueueFilepath() (string, error) { - home, _, err := ini.WakaHomeDir() + folder, err := ini.WakaResourcesDir() if err != nil { - return dbFilename, fmt.Errorf("failed getting user's home directory, defaulting to current directory: %s", err) + return dbFilename, fmt.Errorf("failed getting waka's resource directory, defaulting to current directory: %s", err) } - return filepath.Join(home, dbFilename), nil + return filepath.Join(folder, dbFilename), nil } // WithSync initializes and returns a heartbeat handle option, which diff --git a/pkg/offline/offline_test.go b/pkg/offline/offline_test.go index 7610b2d4..70f9842c 100644 --- a/pkg/offline/offline_test.go +++ b/pkg/offline/offline_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/wakatime/wakatime-cli/pkg/heartbeat" + "github.com/wakatime/wakatime-cli/pkg/ini" "github.com/wakatime/wakatime-cli/pkg/offline" "github.com/stretchr/testify/assert" @@ -20,24 +21,15 @@ import ( ) func TestQueueFilepath(t *testing.T) { - home, err := os.UserHomeDir() - require.NoError(t, err) - tests := map[string]struct { - ViperValue string - EnvVar string - Expected string + EnvVar string }{ - "default": { - Expected: filepath.Join(home, ".wakatime.bdb"), - }, + "default": {}, "env_trailing_slash": { - EnvVar: "~/path2/", - Expected: filepath.Join(home, "path2", ".wakatime.bdb"), + EnvVar: "~/path2/", }, "env_without_trailing_slash": { - EnvVar: "~/path2", - Expected: filepath.Join(home, "path2", ".wakatime.bdb"), + EnvVar: "~/path2", }, } @@ -48,10 +40,15 @@ func TestQueueFilepath(t *testing.T) { defer os.Unsetenv("WAKATIME_HOME") + folder, err := ini.WakaResourcesDir() + require.NoError(t, err) + queueFilepath, err := offline.QueueFilepath() require.NoError(t, err) - assert.Equal(t, test.Expected, queueFilepath) + expected := filepath.Join(folder, "offline_heartbeats.bdb") + + assert.Equal(t, expected, queueFilepath) }) } } @@ -76,7 +73,8 @@ func TestWithQueue(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) opt := offline.WithQueue(f.Name()) @@ -135,7 +133,8 @@ func TestWithQueue(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) require.Len(t, stored, 1) @@ -211,7 +210,8 @@ func TestWithQueue_ApiError(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) dataGo, err := os.ReadFile("testdata/heartbeat_go.json") require.NoError(t, err) @@ -296,7 +296,8 @@ func TestWithQueue_InvalidResults(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) dataPy, err := os.ReadFile("testdata/heartbeat_py.json") require.NoError(t, err) @@ -364,7 +365,8 @@ func TestWithQueue_HandleLeftovers(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) dataPy, err := os.ReadFile("testdata/heartbeat_py.json") require.NoError(t, err) @@ -408,7 +410,8 @@ func TestWithSync(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) opt := offline.WithSync(f.Name(), offline.SyncMaxDefault) @@ -452,7 +455,8 @@ func TestWithSync(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) require.Len(t, stored, 0) } @@ -477,7 +481,8 @@ func TestSync_MultipleRequests(t *testing.T) { }) } - db.Close() + err = db.Close() + require.NoError(t, err) syncFn := offline.Sync(f.Name(), 1000) @@ -540,7 +545,8 @@ func TestSync_MultipleRequests(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) require.Len(t, stored, 0) @@ -574,7 +580,8 @@ func TestSync_APIError(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) syncFn := offline.Sync(f.Name(), 10) @@ -613,7 +620,8 @@ func TestSync_APIError(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) require.Len(t, stored, 2) @@ -660,7 +668,8 @@ func TestSync_InvalidResults(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) syncFn := offline.Sync(f.Name(), 1000) @@ -733,7 +742,8 @@ func TestSync_InvalidResults(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) require.Len(t, stored, 0) @@ -767,7 +777,8 @@ func TestSync_SyncLimit(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) syncFn := offline.Sync(f.Name(), 1) @@ -808,7 +819,8 @@ func TestSync_SyncLimit(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) require.Len(t, stored, 1) @@ -845,7 +857,8 @@ func TestSync_SyncUnlimited(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) syncFn := offline.Sync(f.Name(), 0) @@ -890,7 +903,8 @@ func TestSync_SyncUnlimited(t *testing.T) { }) require.NoError(t, err) - db.Close() + err = db.Close() + require.NoError(t, err) require.Len(t, stored, 0) @@ -922,7 +936,8 @@ func TestCountHeartbeats(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) count, err := offline.CountHeartbeats(f.Name()) require.NoError(t, err) @@ -970,7 +985,8 @@ func TestReadHeartbeats(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) hh, err := offline.ReadHeartbeats(f.Name(), offline.PrintMaxDefault) require.NoError(t, err) @@ -1005,7 +1021,8 @@ func TestReadHeartbeats_WithLimit(t *testing.T) { }, }) - db.Close() + err = db.Close() + require.NoError(t, err) hh, err := offline.ReadHeartbeats(f.Name(), 1) require.NoError(t, err) @@ -1096,7 +1113,10 @@ func TestQueue_PopMany(t *testing.T) { db, err := bolt.Open(f.Name(), 0600, nil) require.NoError(t, err) - defer db.Close() + defer func() { + err = db.Close() + require.NoError(t, err) + }() dataGo, err := os.ReadFile("testdata/heartbeat_go.json") require.NoError(t, err) @@ -1240,7 +1260,10 @@ func TestQueue_ReadMany(t *testing.T) { db, err := bolt.Open(f.Name(), 0600, nil) require.NoError(t, err) - defer db.Close() + defer func() { + err = db.Close() + require.NoError(t, err) + }() dataGo, err := os.ReadFile("testdata/heartbeat_go.json") require.NoError(t, err) @@ -1320,7 +1343,10 @@ func TestQueue_ReadMany_Empty(t *testing.T) { db, err := bolt.Open(f.Name(), 0600, nil) require.NoError(t, err) - defer db.Close() + defer func() { + err = db.Close() + require.NoError(t, err) + }() tx, err := db.Begin(true) require.NoError(t, err) @@ -1349,7 +1375,10 @@ func initDB(t *testing.T) (*bolt.DB, func()) { return db, func() { defer f.Close() - defer db.Close() + defer func() { + err = db.Close() + require.NoError(t, err) + }() } } diff --git a/testdata/api_heartbeats_response_extra_heartbeats.json b/testdata/api_heartbeats_response_extra_heartbeats.json index 624ab0cf..33b08de1 100644 --- a/testdata/api_heartbeats_response_extra_heartbeats.json +++ b/testdata/api_heartbeats_response_extra_heartbeats.json @@ -599,31 +599,6 @@ } }, 201 - ], - [ - { - "data": { - "branch": "heartbeat", - "category": "coding", - "created_at": "2020-04-14T21:27:15Z", - "cursorpos": 12, - "dependencies": ["dep1", "dep2"], - "entity": "/tmp/main.go", - "id": "845a922e-9e65-4775-bd68-bb3196d2e06a", - "is_write": true, - "language": "Go", - "lineno": 42, - "lines": 100, - "machine_name_id": null, - "project": "vscode-wakatime", - "type": "file", - "time": 1585598059.0, - "user_agent_id": null, - "user_agent": "wakatime/13.0.6", - "user_id": "9c4a41c0-eb11-4cf5-84b8-d5b7f5e91bea" - } - }, - 201 ] ] } diff --git a/testdata/api_heartbeats_response_extra_heartbeats_extra.json b/testdata/api_heartbeats_response_extra_heartbeats_extra.json new file mode 100644 index 00000000..fc2fe8cf --- /dev/null +++ b/testdata/api_heartbeats_response_extra_heartbeats_extra.json @@ -0,0 +1,54 @@ +{ + "responses": [ + [ + { + "data": { + "branch": "heartbeat", + "category": "coding", + "created_at": "2020-04-14T21:27:15Z", + "cursorpos": 12, + "dependencies": ["dep1", "dep2"], + "entity": "/tmp/main.go", + "id": "845a922e-9e65-4775-bd68-bb3196d2e06a", + "is_write": true, + "language": "Go", + "lineno": 42, + "lines": 100, + "machine_name_id": null, + "project": "vscode-wakatime", + "type": "file", + "time": 1585598059.0, + "user_agent_id": null, + "user_agent": "wakatime/13.0.6", + "user_id": "9c4a41c0-eb11-4cf5-84b8-d5b7f5e91bea" + } + }, + 201 + ], + [ + { + "data": { + "branch": "heartbeat", + "category": "coding", + "created_at": "2020-04-14T21:27:15Z", + "cursorpos": 12, + "dependencies": ["dep1", "dep2"], + "entity": "/tmp/main.go", + "id": "845a922e-9e65-4775-bd68-bb3196d2e06a", + "is_write": true, + "language": "Go", + "lineno": 42, + "lines": 100, + "machine_name_id": null, + "project": "vscode-wakatime", + "type": "file", + "time": 1585598059.0, + "user_agent_id": null, + "user_agent": "wakatime/13.0.6", + "user_id": "9c4a41c0-eb11-4cf5-84b8-d5b7f5e91bea" + } + }, + 201 + ] + ] +} diff --git a/testdata/api_heartbeats_response_extra_heartbeats_legacy_offline.json b/testdata/api_heartbeats_response_extra_heartbeats_legacy_offline.json new file mode 100644 index 00000000..87b9a5ed --- /dev/null +++ b/testdata/api_heartbeats_response_extra_heartbeats_legacy_offline.json @@ -0,0 +1,74 @@ +{ + "responses": [ + [ + { + "data": { + "branch": "heartbeat", + "category": "coding", + "created_at": "2020-04-14T21:27:15Z", + "cursorpos": 12, + "dependencies": ["dep1", "dep2"], + "entity": "/tmp/main.go", + "id": "845a922e-9e65-4775-bd68-bb3196d2e06a", + "is_write": true, + "language": "Go", + "lineno": 42, + "lines": 100, + "machine_name_id": null, + "project": "wakatime-cli", + "type": "file", + "time": 1592868367.219124, + "user_agent_id": null, + "user_agent": "wakatime/13.0.6", + "user_id": "9c4a41c0-eb11-4cf5-84b8-d5b7f5e91bea" + } + }, + 201 + ], + [ + { + "data": { + "branch": "summary", + "category": "debugging", + "created_at": "2020-04-14T21:27:15Z", + "cursorpos": 13, + "dependencies": ["dep3", "dep4"], + "entity": "/tmp/main.py", + "id": "845a922e-9e65-4775-bd68-bb3196d2e06a", + "is_write": false, + "language": "Python", + "lineno": 43, + "lines": 101, + "machine_name_id": null, + "project": "wakatime", + "type": "file", + "time": 1592868386.079084, + "user_agent_id": null, + "user_agent": "wakatime/13.0.7", + "user_id": "9c4a41c0-eb11-4cf5-84b8-d5b7f5e91bea" + } + }, + 201 + ], + [ + { + "data": { + "branch": "todaygoal", + "category": "building", + "cursorpos": 14, + "dependencies": ["dep5", "dep6"], + "entity": "/tmp/main.js", + "is_write": false, + "language": "JavaScript", + "lineno": 44, + "lines": 102, + "project": "wakatime", + "type": "file", + "time": 1592868394.084354, + "user_agent": "wakatime/13.0.8" + } + }, + 201 + ] + ] +} diff --git a/testdata/heartbeat_go.json b/testdata/heartbeat_go.json new file mode 100644 index 00000000..59ac1d1f --- /dev/null +++ b/testdata/heartbeat_go.json @@ -0,0 +1,15 @@ +{ + "branch": "heartbeat", + "category": "coding", + "cursorpos": 12, + "dependencies": ["dep1", "dep2"], + "entity": "/tmp/main.go", + "is_write": true, + "language": "Go", + "lineno": 42, + "lines": 100, + "project": "wakatime-cli", + "type": "file", + "time": 1592868367.219124, + "user_agent": "wakatime/13.0.6" +} diff --git a/testdata/heartbeat_js.json b/testdata/heartbeat_js.json new file mode 100644 index 00000000..7e51135b --- /dev/null +++ b/testdata/heartbeat_js.json @@ -0,0 +1,15 @@ +{ + "branch": "todaygoal", + "category": "building", + "cursorpos": 14, + "dependencies": ["dep5", "dep6"], + "entity": "/tmp/main.js", + "is_write": false, + "language": "JavaScript", + "lineno": 44, + "lines": 102, + "project": "wakatime", + "type": "file", + "time": 1592868394.084354, + "user_agent": "wakatime/13.0.8" +} diff --git a/testdata/heartbeat_py.json b/testdata/heartbeat_py.json new file mode 100644 index 00000000..0411583b --- /dev/null +++ b/testdata/heartbeat_py.json @@ -0,0 +1,15 @@ +{ + "branch": "summary", + "category": "debugging", + "cursorpos": 13, + "dependencies": ["dep3", "dep4"], + "entity": "/tmp/main.py", + "is_write": false, + "language": "Python", + "lineno": 43, + "lines": 101, + "project": "wakatime", + "type": "file", + "time": 1592868386.079084, + "user_agent": "wakatime/13.0.7" +}