Skip to content

Commit

Permalink
Merge pull request #1062 from wakatime/develop
Browse files Browse the repository at this point in the history
Release v1.94.1
  • Loading branch information
alanhamlett authored Jul 10, 2024
2 parents ffbeec2 + b8816ca commit 6f35021
Show file tree
Hide file tree
Showing 28 changed files with 828 additions and 131 deletions.
2 changes: 1 addition & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_ | |
Expand Down
5 changes: 4 additions & 1 deletion cmd/heartbeat/heartbeat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,10 @@ func TestSendHeartbeats_ExtraHeartbeats_Sanitize(t *testing.T) {
db, err := bolt.Open(offlineQueueFile.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)
Expand Down
7 changes: 2 additions & 5 deletions cmd/logfile/logfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import (
"github.com/spf13/viper"
)

const (
defaultFile = "wakatime.log"
defaultFolder = ".wakatime"
)
const defaultFile = "wakatime.log"

// Params contains log file parameters.
type Params struct {
Expand Down Expand Up @@ -60,7 +57,7 @@ func LoadParams(v *viper.Viper) (Params, error) {

folder, err := ini.WakaResourcesDir()
if err != nil {
return Params{}, fmt.Errorf("failed getting user's home directory: %s", err)
return Params{}, fmt.Errorf("failed getting resource directory: %s", err)
}

params.File = filepath.Join(folder, defaultFile)
Expand Down
7 changes: 5 additions & 2 deletions cmd/offlinecount/offlinecount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ func TestOfflineCount_Empty(t *testing.T) {
require.NoError(t, err)

insertHeartbeatRecords(t, db, "heartbeats", []heartbeatRecord{})
db.Close()

err = db.Close()
require.NoError(t, err)

v := viper.New()
v.Set("verbose", true)
Expand Down Expand Up @@ -89,7 +91,8 @@ func TestOfflineCount(t *testing.T) {
},
})

db.Close()
err = db.Close()
require.NoError(t, err)

v := viper.New()
v.Set("offline-count", true)
Expand Down
3 changes: 2 additions & 1 deletion cmd/offlineprint/offlineprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ func TestPrintOfflineHeartbeats(t *testing.T) {
},
})

db.Close()
err = db.Close()
require.NoError(t, err)

v := viper.New()
v.Set("print-offline-heartbeats", 10)
Expand Down
54 changes: 53 additions & 1 deletion cmd/offlinesync/offlinesync.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package offlinesync

import (
"fmt"
"os"

cmdapi "github.com/wakatime/wakatime-cli/cmd/api"
"github.com/wakatime/wakatime-cli/cmd/params"
Expand All @@ -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())
}
Expand All @@ -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.Warnf("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 {
Expand Down
147 changes: 147 additions & 0 deletions cmd/offlinesync/offlinesync_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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)

// early close to avoid file locking in Windows
f.Close()

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)
}
6 changes: 4 additions & 2 deletions cmd/offlinesync/offlinesync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ func TestSyncOfflineActivity(t *testing.T) {
},
})

db.Close()
err = db.Close()
require.NoError(t, err)

v := viper.New()
v.Set("api-url", testServerURL)
Expand Down Expand Up @@ -181,7 +182,8 @@ func TestSyncOfflineActivity_MultipleApiKey(t *testing.T) {
},
})

db.Close()
err = db.Close()
require.NoError(t, err)

v := viper.New()
v.Set("api-url", testServerURL)
Expand Down
24 changes: 14 additions & 10 deletions cmd/params/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"os"
"os/exec"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -1036,10 +1039,11 @@ 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', num sync max: %d",
"disabled: %t, print max: %d, queue file: '%s', queue file legacy: '%s', num sync max: %d",
p.Disabled,
p.PrintMax,
p.QueueFile,
p.QueueFileLegacy,
p.SyncMax,
)
}
Expand Down
Loading

0 comments on commit 6f35021

Please sign in to comment.