From 4a42955e3de0ef93e8963cad4cdb2090d54fcf42 Mon Sep 17 00:00:00 2001 From: Victor Conner Date: Thu, 25 Apr 2024 07:22:05 +0200 Subject: [PATCH] fix: Change time format --- buffer.go | 20 ++++---- buffer_stack.go | 2 +- clock.go | 11 ----- coding_session.go | 38 +++++++-------- coding_session_test.go | 39 ++++++++++------ disk/disk.go | 4 +- disk/disk_test.go | 24 +++++----- file.go | 14 +++--- repository.go | 2 +- server/handlers.go | 10 ++-- server/heartbeat.go | 12 ++--- server/server.go | 18 ++++---- server/server_test.go | 46 +++++++++---------- session.go | 13 +++--- sessions.go | 13 +++--- .../2024-01-13/05:38:11.023-06:18:19.371.json | 4 +- .../2024-04-01/20:10:31.011-20:20:19.381.json | 4 +- .../2024-04-19/08:35:34.344-08:37:50.659.json | 7 +-- .../2024-04-19/08:38:35.772-08:42:53.405.json | 7 +-- .../2024-04-19/08:44:03.882-08:46:29.692.json | 7 +-- .../2024-04-19/08:46:39.647-08:46:41.398.json | 7 +-- .../2024-04-19/08:46:49.311-08:47:08.712.json | 7 +-- .../2024-04-19/08:47:16.467-08:47:31.311.json | 7 +-- truncate/truncate.go | 6 +-- 24 files changed, 166 insertions(+), 156 deletions(-) diff --git a/buffer.go b/buffer.go index ea7c6ec..37b6a5d 100644 --- a/buffer.go +++ b/buffer.go @@ -1,8 +1,10 @@ package pulse +import "time" + // Buffer represents a buffer that has been opened during an active coding session. type Buffer struct { - OpenedClosed []int64 + OpenedClosed []time.Time Filename string Repository string Filepath string @@ -10,9 +12,9 @@ type Buffer struct { } // NewBuffer creates a new buffer. -func NewBuffer(filename, repo, filetype, filepath string, openedAt int64) Buffer { +func NewBuffer(filename, repo, filetype, filepath string, openedAt time.Time) Buffer { return Buffer{ - OpenedClosed: []int64{openedAt}, + OpenedClosed: []time.Time{openedAt}, Filename: filename, Repository: repo, Filetype: filetype, @@ -21,7 +23,7 @@ func NewBuffer(filename, repo, filetype, filepath string, openedAt int64) Buffer } // Open should be called when a buffer is opened. -func (b *Buffer) Open(time int64) { +func (b *Buffer) Open(time time.Time) { b.OpenedClosed = append(b.OpenedClosed, time) } @@ -31,20 +33,20 @@ func (b *Buffer) IsOpen() bool { } // LastOpened returns the last time the buffer was opened. -func (b *Buffer) LastOpened() int64 { +func (b *Buffer) LastOpened() time.Time { return b.OpenedClosed[len(b.OpenedClosed)-1] } // Close should be called when a buffer is closed. -func (b *Buffer) Close(time int64) { +func (b *Buffer) Close(time time.Time) { b.OpenedClosed = append(b.OpenedClosed, time) } // Duration returns the total duration that the buffer has been open. -func (b *Buffer) Duration() int64 { - var duration int64 +func (b *Buffer) Duration() time.Duration { + var duration time.Duration for i := 0; i < len(b.OpenedClosed); i += 2 { - duration += b.OpenedClosed[i+1] - b.OpenedClosed[i] + duration += b.OpenedClosed[i+1].Sub(b.OpenedClosed[i]) } return duration } diff --git a/buffer_stack.go b/buffer_stack.go index 2efaa87..6cc4d43 100644 --- a/buffer_stack.go +++ b/buffer_stack.go @@ -36,7 +36,7 @@ func (b *bufferStack) files() Files { sortOrder = append(sortOrder, buffer.Filepath) pathFile[buffer.Filepath] = fileFromBuffer(buffer) } else { - file.DurationMs += buffer.Duration() + file.Duration += buffer.Duration() pathFile[buffer.Filepath] = file } } diff --git a/clock.go b/clock.go index 3ef9b9c..b455a49 100644 --- a/clock.go +++ b/clock.go @@ -11,7 +11,6 @@ type Clock interface { Now() time.Time NewTicker(d time.Duration) (<-chan time.Time, func()) NewTimer(d time.Duration) (<-chan time.Time, func() bool) - GetTime() int64 } type RealClock struct{} @@ -34,10 +33,6 @@ func (c *RealClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) { return t.C, t.Stop } -func (c *RealClock) GetTime() int64 { - return time.Now().UTC().UnixMilli() -} - type testTimer struct { deadline time.Time ch chan time.Time @@ -109,12 +104,6 @@ func (c *TestClock) Now() time.Time { return c.time } -func (c *TestClock) GetTime() int64 { - c.mu.Lock() - defer c.mu.Unlock() - return c.time.UnixMilli() -} - func (c *TestClock) NewTicker(d time.Duration) (<-chan time.Time, func()) { c.mu.Lock() defer c.mu.Unlock() diff --git a/coding_session.go b/coding_session.go index 61296b6..8242267 100644 --- a/coding_session.go +++ b/coding_session.go @@ -1,22 +1,24 @@ package pulse +import "time" + // CodingSession represents an ongoing coding session. type CodingSession struct { // startStops is a slice of timestamps representing the start and stop times of // an ongoing coding session. This ensures accurate time tracking when switching // between multiple editor processes. We only count time for one at a time. - startStops []int64 + startStops []time.Time bufStack *bufferStack EditorID string - StartedAt int64 + StartedAt time.Time OS string Editor string } // StartSession creates a new coding session. -func StartSession(editorID string, startedAt int64, os, editor string) *CodingSession { +func StartSession(editorID string, startedAt time.Time, os, editor string) *CodingSession { return &CodingSession{ - startStops: []int64{startedAt}, + startStops: []time.Time{startedAt}, bufStack: newBufferStack(), EditorID: editorID, StartedAt: startedAt, @@ -26,7 +28,7 @@ func StartSession(editorID string, startedAt int64, os, editor string) *CodingSe } // Pause should be called when another editor process gains focus. -func (s *CodingSession) Pause(time int64) { +func (s *CodingSession) Pause(time time.Time) { if currentBuffer := s.bufStack.peek(); currentBuffer != nil { currentBuffer.Close(time) } @@ -34,15 +36,15 @@ func (s *CodingSession) Pause(time int64) { } // PauseTime is used to determine which coding session to resume. -func (s *CodingSession) PauseTime() int64 { +func (s *CodingSession) PauseTime() time.Time { if len(s.startStops) == 0 { - return 0 + return time.Time{} } return s.startStops[len(s.startStops)-1] } // Resume should be called when the editor regains focus. -func (s *CodingSession) Resume(time int64) { +func (s *CodingSession) Resume(time time.Time) { if currentBuffer := s.bufStack.peek(); currentBuffer != nil { currentBuffer.Open(time) } @@ -64,10 +66,10 @@ func (s *CodingSession) HasBuffers() bool { } // Duration returns the total duration of the coding session. -func (s *CodingSession) Duration() int64 { - var duration int64 +func (s *CodingSession) Duration() time.Duration { + var duration time.Duration for i := 0; i < len(s.startStops); i += 2 { - duration += s.startStops[i+1] - s.startStops[i] + duration += s.startStops[i+1].Sub(s.startStops[i]) } return duration } @@ -79,7 +81,7 @@ func (s *CodingSession) Active() bool { // End ends the active coding sessions. It sets the total duration in // milliseconds, and turns the stack of buffers into a slice of files. -func (s *CodingSession) End(endedAt int64) Session { +func (s *CodingSession) End(endedAt time.Time) Session { if currentBuffer := s.bufStack.peek(); currentBuffer != nil && currentBuffer.IsOpen() { currentBuffer.Close(endedAt) } @@ -89,11 +91,11 @@ func (s *CodingSession) End(endedAt int64) Session { } return Session{ - StartedAt: s.StartedAt, - EndedAt: endedAt, - DurationMs: s.Duration(), - OS: s.OS, - Editor: s.Editor, - Files: s.bufStack.files(), + StartedAt: s.StartedAt, + EndedAt: endedAt, + Duration: s.Duration(), + OS: s.OS, + Editor: s.Editor, + Files: s.bufStack.files(), } } diff --git a/coding_session_test.go b/coding_session_test.go index 62f0509..8418373 100644 --- a/coding_session_test.go +++ b/coding_session_test.go @@ -2,51 +2,58 @@ package pulse_test import ( "testing" + "time" "github.com/creativecreature/pulse" ) func TestActiveSession(t *testing.T) { + t.Parallel() + + mockClock := pulse.NewTestClock(time.Now()) + // Start a new coding session - activeSession := pulse.StartSession("1337", 100, "linux", "nvim") + activeSession := pulse.StartSession("1337", mockClock.Now(), "linux", "nvim") - // Open the first buffer + // Open the first buffer, and wait 400ms. bufferOne := pulse.NewBuffer( "init.lua", "dotfiles", "lua", "dotfiles/editors/nvim/init.lua", - 101, + mockClock.Now(), ) activeSession.PushBuffer(bufferOne) + mockClock.Add(400 * time.Millisecond) - // Open a second buffer. + // Open a second buffer, and wait 200ms. bufferTwo := pulse.NewBuffer( "plugins.lua", "dotfiles", "lua", "dotfiles/editors/nvim/plugins.lua", - 301, + mockClock.Now(), ) activeSession.PushBuffer(bufferTwo) + mockClock.Add(200 * time.Millisecond) - // Open the same file as buffer one. The total duration for these + // Open the first buffer again. The total duration for these // buffers should be merged when we end the coding session. bufferThree := pulse.NewBuffer( "init.lua", "dotfiles", "lua", "dotfiles/editors/nvim/init.lua", - 611, + mockClock.Now(), ) activeSession.PushBuffer(bufferThree) + mockClock.Add(time.Millisecond * 100) - endedAt := int64(700) - finishedSession := activeSession.End(endedAt) + finishedSession := activeSession.End(mockClock.Now()) // Assert that the duration of the session was set correctly. - if finishedSession.DurationMs != 600 { - t.Errorf("Expected the session duration to be 600, got %d", finishedSession.DurationMs) + if finishedSession.Duration.Milliseconds() != 700 { + t.Errorf("Expected the session duration to be 600, got %d", finishedSession.Duration.Milliseconds()) } // Assert that the buffers have been merged into files. @@ -55,11 +62,13 @@ func TestActiveSession(t *testing.T) { } // Assert that the merged buffers has both durations. - if finishedSession.Files[0].DurationMs != 289 { - t.Errorf("Expected the merged duration for init.lua to be 289, got %d", finishedSession.Files[0].DurationMs) + initLuaDuration := finishedSession.Files[0].Duration.Milliseconds() + if initLuaDuration != 500 { + t.Errorf("Expected the merged duration for init.lua to be 500, got %d", initLuaDuration) } - if finishedSession.Files[1].DurationMs != 310 { - t.Errorf("Expected the duration for plugins.lua to be 310, got %d", finishedSession.Files[1].DurationMs) + pluginsLuaDuration := finishedSession.Files[1].Duration.Milliseconds() + if pluginsLuaDuration != 200 { + t.Errorf("Expected the duration for plugins.lua to be 200, got %d", pluginsLuaDuration) } } diff --git a/disk/disk.go b/disk/disk.go index fcc70da..ecb1c9b 100644 --- a/disk/disk.go +++ b/disk/disk.go @@ -55,8 +55,8 @@ func (s *Storage) dayDir() (string, error) { // filename returns the name we'll use when writing the session to disk. func (s *Storage) filename(session pulse.Session) string { - startTime := time.UnixMilli(session.StartedAt).Format("15:04:05.000") - endTime := time.UnixMilli(session.EndedAt).Format("15:04:05.000") + startTime := session.StartedAt.Format("15:04:05.000") + endTime := session.EndedAt.Format("15:04:05.000") return fmt.Sprintf("%s-%s.json", startTime, endTime) } diff --git a/disk/disk_test.go b/disk/disk_test.go index 78771c7..1db5521 100644 --- a/disk/disk_test.go +++ b/disk/disk_test.go @@ -47,34 +47,34 @@ func TestStorageReadWriteClean(t *testing.T) { now := time.Now() sessions := pulse.Sessions{ pulse.Session{ - StartedAt: now.UnixMilli(), - EndedAt: now.Add(time.Hour).UnixMilli(), - DurationMs: time.Hour.Milliseconds(), - OS: "linux", - Editor: "nvim", + StartedAt: now, + EndedAt: now.Add(time.Hour), + Duration: time.Hour, + OS: "linux", + Editor: "nvim", Files: pulse.Files{ pulse.File{ Name: "main.go", Path: "/cmd/main.go", Repository: "pulse", Filetype: "go", - DurationMs: time.Hour.Milliseconds(), + Duration: time.Hour, }, }, }, pulse.Session{ - StartedAt: now.Add(time.Minute).UnixMilli(), - EndedAt: now.Add(time.Minute * 11).UnixMilli(), - DurationMs: time.Hour.Milliseconds(), - OS: "linux", - Editor: "nvim", + StartedAt: now.Add(time.Minute), + EndedAt: now.Add(time.Minute * 11), + Duration: time.Hour, + OS: "linux", + Editor: "nvim", Files: pulse.Files{ pulse.File{ Name: "main.go", Path: "/cmd/main.go", Repository: "pulse", Filetype: "go", - DurationMs: time.Minute.Milliseconds() * 10, + Duration: time.Minute * 10, }, }, }, diff --git a/file.go b/file.go index 85f42a0..8195878 100644 --- a/file.go +++ b/file.go @@ -1,12 +1,14 @@ package pulse +import "time" + // File represents a file that has been opened during a coding session. type File struct { - Name string `json:"name"` - Path string `json:"path"` - Repository string `json:"repository"` - Filetype string `json:"filetype"` - DurationMs int64 `json:"duration_ms"` + Name string `json:"name"` + Path string `json:"path"` + Repository string `json:"repository"` + Filetype string `json:"filetype"` + Duration time.Duration `json:"duration"` } // fileFromBuffer turns a code buffer into a file. @@ -16,7 +18,7 @@ func fileFromBuffer(b Buffer) File { Path: b.Filepath, Repository: b.Repository, Filetype: b.Filetype, - DurationMs: b.Duration(), + Duration: b.Duration(), } } diff --git a/repository.go b/repository.go index 3221bf7..7a3ee9a 100644 --- a/repository.go +++ b/repository.go @@ -32,7 +32,7 @@ func repositoryPathFile(sessions []Session) map[string]map[string]AggregatedFile Name: file.Name, Path: file.Path, Filetype: file.Filetype, - DurationMs: file.DurationMs, + DurationMs: file.Duration.Milliseconds(), } // Check if it is the first time we're seeing a repository diff --git a/server/handlers.go b/server/handlers.go index a0c4959..d7e1de0 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -17,7 +17,7 @@ func (s *Server) FocusGained(event pulse.Event, reply *string) { s.checkHeartbeat() s.mutex.Lock() defer s.mutex.Unlock() - s.lastHeartbeat = s.clock.GetTime() + s.lastHeartbeat = s.clock.Now() // The FocusGained event will be triggered when I switch back to an active // editor from another TMUX split. However, the intent is to only terminate @@ -49,7 +49,7 @@ func (s *Server) FocusGained(event pulse.Event, reply *string) { // Check to see if we have another instance of neovim that is running in a different tmux // pane. If so, we'll stop recording time for that session before creating a new one. if s.activeEditorID != "" { - s.activeSessions[s.activeEditorID].Pause(s.clock.GetTime()) + s.activeSessions[s.activeEditorID].Pause(s.clock.Now()) s.log.Debug("Pausing session.", "editor_id", s.activeEditorID, "editor", s.activeSessions[s.activeEditorID].Editor, @@ -65,7 +65,7 @@ func (s *Server) FocusGained(event pulse.Event, reply *string) { "editor", event.Editor, "os", event.OS, ) - session.Resume(s.clock.GetTime()) + session.Resume(s.clock.Now()) return } @@ -82,7 +82,7 @@ func (s *Server) OpenFile(event pulse.Event, reply *string) { s.checkHeartbeat() s.mutex.Lock() defer s.mutex.Unlock() - s.lastHeartbeat = s.clock.GetTime() + s.lastHeartbeat = s.clock.Now() // The editor could have been inactive, while focused, for 10 minutes. // That would end the session, and we could get a OpenFile event without @@ -122,7 +122,7 @@ func (s *Server) SendHeartbeat(event pulse.Event, reply *string) { s.checkHeartbeat() s.mutex.Lock() defer s.mutex.Unlock() - s.lastHeartbeat = s.clock.GetTime() + s.lastHeartbeat = s.clock.Now() // This is to handle the case where the server would have ended the clients // session due to inactivity. When a session ends it is written to disk and diff --git a/server/heartbeat.go b/server/heartbeat.go index 9f00d21..d99c2c9 100644 --- a/server/heartbeat.go +++ b/server/heartbeat.go @@ -16,7 +16,7 @@ func (s *Server) checkHeartbeat() { s.log.Debug("Checking heartbeat.", "active_editor_id", s.activeEditorID, "last_heartbeat", s.lastHeartbeat, - "time_now", s.clock.GetTime(), + "time_now", s.clock.Now().UnixMilli(), ) if s.activeEditorID == "" { return @@ -25,19 +25,19 @@ func (s *Server) checkHeartbeat() { s.mutex.Lock() defer s.mutex.Unlock() - if s.lastHeartbeat+HeartbeatTTL.Milliseconds() < s.clock.GetTime() { + if s.clock.Now().After(s.lastHeartbeat.Add(HeartbeatTTL)) { s.log.Info( "Ending all active sessions due to inactivity", - "last_heartbeat", strconv.FormatInt(s.lastHeartbeat, 10), - "current_time", strconv.FormatInt(s.clock.GetTime(), 10), - "end_time", strconv.FormatInt(s.lastHeartbeat+int64(HeartbeatTTL), 10), + "last_heartbeat", strconv.FormatInt(s.lastHeartbeat.UnixMilli(), 10), + "current_time", strconv.FormatInt(s.clock.Now().UnixMilli(), 10), + "end_time", strconv.FormatInt(s.lastHeartbeat.Add(HeartbeatTTL).UnixMilli(), 10), ) // The machine may have entered sleep mode, potentially stopping the heartbeat // check from executing at its scheduled interval. To mitigate this, the session // will be terminated based on the time of the last recorded heartbeat plus the // TTL. This prevents the creation of inaccurately long sessions. - s.saveAllSessions(s.lastHeartbeat + int64(HeartbeatTTL/time.Millisecond)) + s.saveAllSessions(s.lastHeartbeat.Add(HeartbeatTTL)) s.activeEditorID = "" } } diff --git a/server/server.go b/server/server.go index 9ae9fa5..42b4302 100644 --- a/server/server.go +++ b/server/server.go @@ -22,7 +22,7 @@ type Server struct { name string activeEditorID string activeSessions map[string]*pulse.CodingSession - lastHeartbeat int64 + lastHeartbeat time.Time stopHeartbeatChecks chan struct{} clock pulse.Clock fileReader FileReader @@ -53,12 +53,12 @@ func New(serverName string, opts ...Option) (*Server, error) { // createSession creates a new session and sets it as the current session. func (s *Server) createSession(id, os, editor string) { s.log.Debug("Creating a new session.", "editor_id", id, "editor", editor, "os", os) - s.activeSessions[id] = pulse.StartSession(id, s.clock.GetTime(), os, editor) + s.activeSessions[id] = pulse.StartSession(id, s.clock.Now(), os, editor) } // setActiveBuffer updates the current buffer in the current session. func (s *Server) setActiveBuffer(gitFile pulse.GitFile) { - openedAt := s.clock.GetTime() + openedAt := s.clock.Now() buf := pulse.NewBuffer( gitFile.Name, gitFile.Repository, @@ -78,7 +78,7 @@ func (s *Server) setActiveBuffer(gitFile pulse.GitFile) { ) } -func (s *Server) saveAllSessions(endedAt int64) { +func (s *Server) saveAllSessions(endedAt time.Time) { s.log.Debug("Saving all sessions.") for _, session := range s.activeSessions { @@ -118,7 +118,7 @@ func (s *Server) saveActiveSession() { "editor", s.activeSessions[s.activeEditorID].Editor, "os", s.activeSessions[s.activeEditorID].OS, ) - now := s.clock.GetTime() + now := s.clock.Now() finishedSession := s.activeSessions[s.activeEditorID].End(now) err := s.storage.Write(finishedSession) if err != nil { @@ -134,9 +134,9 @@ func (s *Server) saveActiveSession() { } var editorToResume string - var mostRecentPause int64 + var mostRecentPause time.Time for _, session := range s.activeSessions { - if session.PauseTime() > mostRecentPause { + if session.PauseTime().After(mostRecentPause) { editorToResume = session.EditorID mostRecentPause = session.PauseTime() } @@ -144,7 +144,7 @@ func (s *Server) saveActiveSession() { if editorToResume != "" { s.activeEditorID = editorToResume - s.activeSessions[s.activeEditorID].Resume(s.clock.GetTime()) + s.activeSessions[s.activeEditorID].Resume(s.clock.Now()) } } @@ -198,7 +198,7 @@ func (s *Server) Start(port string) error { } // Save the all sessions before shutting down. - s.saveAllSessions(s.clock.GetTime()) + s.saveAllSessions(s.clock.Now()) s.log.Info("Shutting down.") return nil diff --git a/server/server_test.go b/server/server_test.go index f71f2ef..99bc9ef 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -42,7 +42,7 @@ func TestServerMergesFiles(t *testing.T) { reply := "" s, err := server.New("TestApp", - server.WithLog(log.New(io.Discard)), + server.WithLog(log.New(os.Stdout)), server.WithStorage(mockStorage), server.WithClock(mockClock), ) @@ -111,17 +111,17 @@ func TestServerMergesFiles(t *testing.T) { if len(storedSessions) != 1 { t.Errorf("expected sessions %d; got %d", 1, len(storedSessions)) } - if storedSessions[0].DurationMs != 190 { - t.Errorf("expected the sessions duration to be 190; got %d", storedSessions[0].DurationMs) + if storedSessions[0].Duration.Milliseconds() != 190 { + t.Errorf("expected the sessions duration to be 190; got %d", storedSessions[0].Duration.Milliseconds()) } if len(storedSessions[0].Files) != 2 { t.Errorf("expected files %d; got %d", 2, len(storedSessions[0].Files)) } - if storedSessions[0].Files[0].DurationMs != 130 { - t.Errorf("expected file duration 130; got %d", storedSessions[0].Files[0].DurationMs) + if storedSessions[0].Files[0].Duration.Milliseconds() != 130 { + t.Errorf("expected file duration 130; got %d", storedSessions[0].Files[0].Duration.Milliseconds()) } - if storedSessions[0].Files[1].DurationMs != 50 { - t.Errorf("expected file duration 50; got %d", storedSessions[0].Files[1].DurationMs) + if storedSessions[0].Files[1].Duration.Milliseconds() != 50 { + t.Errorf("expected file duration 50; got %d", storedSessions[0].Files[1].Duration.Milliseconds()) } if storedSessions[0].Files[0].Path != "sturdyc/cmd/main.go" { t.Errorf("expected file path sturdyc/cmd/main.go; got %s", storedSessions[0].Files[0].Path) @@ -272,11 +272,11 @@ func TestTimeGetsAddedToTheCorrectSession(t *testing.T) { if len(storedSessions) != 2 { t.Errorf("expected sessions %d; got %d", 2, len(storedSessions)) } - if storedSessions[0].DurationMs != 110 { - t.Errorf("expected the sessions duration to be 110; got %d", storedSessions[0].DurationMs) + if storedSessions[0].Duration.Milliseconds() != 110 { + t.Errorf("expected the sessions duration to be 110; got %d", storedSessions[0].Duration.Milliseconds()) } - if storedSessions[1].DurationMs != 130 { - t.Errorf("expected the sessions duration to be 130; got %d", storedSessions[1].DurationMs) + if storedSessions[1].Duration.Milliseconds() != 130 { + t.Errorf("expected the sessions duration to be 130; got %d", storedSessions[1].Duration.Milliseconds()) } } @@ -364,11 +364,11 @@ func TestResumesThePreviousSession(t *testing.T) { if len(storedSessions) != 2 { t.Errorf("expected sessions %d; got %d", 2, len(storedSessions)) } - if storedSessions[0].DurationMs != 180 { - t.Errorf("expected the sessions duration to be 180; got %d", storedSessions[0].DurationMs) + if storedSessions[0].Duration.Milliseconds() != 180 { + t.Errorf("expected the sessions duration to be 180; got %d", storedSessions[0].Duration.Milliseconds()) } - if storedSessions[1].DurationMs != 50 { - t.Errorf("expected the sessions duration to be 50; got %d", storedSessions[1].DurationMs) + if storedSessions[1].Duration.Milliseconds() != 50 { + t.Errorf("expected the sessions duration to be 50; got %d", storedSessions[1].Duration.Milliseconds()) } } @@ -440,20 +440,20 @@ func TestNoActivityShouldEndSession(t *testing.T) { t.Errorf("expected sessions %d; got %d", 2, len(storedSessions)) } - if storedSessions[0].DurationMs != 100 { - t.Errorf("expected the sessions duration to be 100; got %d", storedSessions[0].DurationMs) + if storedSessions[0].Duration.Milliseconds() != 100 { + t.Errorf("expected the sessions duration to be 100; got %d", storedSessions[0].Duration.Milliseconds()) } - if storedSessions[0].Files[0].DurationMs != 100 { - t.Errorf("expected the file duration to be 100; got %d", storedSessions[0].Files[0].DurationMs) + if storedSessions[0].Files[0].Duration.Milliseconds() != 100 { + t.Errorf("expected the file duration to be 100; got %d", storedSessions[0].Files[0].Duration.Milliseconds()) } // The second session should have been terminated by the // heartbeat check after 10 minutes of inactivity. dur := int64(10 * time.Minute / time.Millisecond) - if storedSessions[1].DurationMs != dur { - t.Errorf("expected the sessions duration to be %d; got %d", dur, storedSessions[1].DurationMs) + if storedSessions[1].Duration.Milliseconds() != dur { + t.Errorf("expected the sessions duration to be %d; got %d", dur, storedSessions[1].Duration.Milliseconds()) } - if storedSessions[1].Files[0].DurationMs != dur { - t.Errorf("expected the file duration to be %d; got %d", dur, storedSessions[1].Files[0].DurationMs) + if storedSessions[1].Files[0].Duration.Milliseconds() != dur { + t.Errorf("expected the file duration to be %d; got %d", dur, storedSessions[1].Files[0].Duration.Milliseconds()) } } diff --git a/session.go b/session.go index 52a3abd..61d2c46 100644 --- a/session.go +++ b/session.go @@ -2,17 +2,18 @@ package pulse import ( "encoding/json" + "time" ) // Session is the raw representation of a past coding session. These sessions are // stored temporarily on disk, and are later aggregated and and moved to a database. type Session struct { - StartedAt int64 `json:"started_at"` - EndedAt int64 `json:"ended_at"` - DurationMs int64 `json:"duration_ms"` - OS string `json:"os"` - Editor string `json:"editor"` - Files Files `json:"files"` + StartedAt time.Time `json:"started_at"` + EndedAt time.Time `json:"ended_at"` + Duration time.Duration `json:"duration"` + OS string `json:"os"` + Editor string `json:"editor"` + Files Files `json:"files"` } // Serialize serializes the session to a JSON byte slice. diff --git a/sessions.go b/sessions.go index 02b76a3..e678abf 100644 --- a/sessions.go +++ b/sessions.go @@ -18,14 +18,14 @@ func (s Sessions) Swap(i, j int) { } func (s Sessions) Less(i, j int) bool { - return s[i].StartedAt < s[j].StartedAt + return s[i].StartedAt.Before(s[j].StartedAt) } // groupByDay groups a slice of sessions by day. func groupByDay(session []Session) map[int64][]Session { buckets := make(map[int64][]Session) for _, s := range session { - d := truncate.Day(s.StartedAt) + d := truncate.Day(s.StartedAt.UnixMilli()) buckets[d] = append(buckets[d], s) } return buckets @@ -37,16 +37,15 @@ func (s Sessions) Aggregate() AggregatedSessions { aggregatedSessions := make(AggregatedSessions, 0) for date, tempSessions := range sessionsPerDay { - dateString := time.Unix(0, date*int64(time.Millisecond)).Format("2006-01-02") - var totalTime int64 + var totalTimeMs int64 for _, tempSession := range tempSessions { - totalTime += tempSession.DurationMs + totalTimeMs += tempSession.Duration.Milliseconds() } session := AggregatedSession{ Period: Day, Date: date, - DateString: dateString, - TotalTimeMs: totalTime, + DateString: time.UnixMilli(date).Format("2006-01-02"), + TotalTimeMs: totalTimeMs, Repositories: repositories(tempSessions), } aggregatedSessions = append(aggregatedSessions, session) diff --git a/testdata/.pulse/tmp/2024-01-13/05:38:11.023-06:18:19.371.json b/testdata/.pulse/tmp/2024-01-13/05:38:11.023-06:18:19.371.json index 4328f0c..bbdbdc8 100644 --- a/testdata/.pulse/tmp/2024-01-13/05:38:11.023-06:18:19.371.json +++ b/testdata/.pulse/tmp/2024-01-13/05:38:11.023-06:18:19.371.json @@ -1,6 +1,6 @@ { - "started_at": 1705124291023, - "ended_at": 1705126699371, + "started_at": "2024-01-13T06:38:11.023Z", + "ended_at": "2024-01-13T07:18:19.371Z", "duration_ms": 2408348, "os": "darwin", "editor": "nvim", diff --git a/testdata/.pulse/tmp/2024-04-01/20:10:31.011-20:20:19.381.json b/testdata/.pulse/tmp/2024-04-01/20:10:31.011-20:20:19.381.json index 9d59798..f68bb9f 100644 --- a/testdata/.pulse/tmp/2024-04-01/20:10:31.011-20:20:19.381.json +++ b/testdata/.pulse/tmp/2024-04-01/20:10:31.011-20:20:19.381.json @@ -1,6 +1,6 @@ { - "started_at": 1712002231011, - "ended_at": 1712002819381, + "started_at": "2024-04-01T22:10:31.011Z", + "ended_at": "2024-04-01T22:20:19.381Z", "duration_ms": 588370, "os": "darwin", "editor": "nvim", diff --git a/testdata/.pulse/tmp/2024-04-19/08:35:34.344-08:37:50.659.json b/testdata/.pulse/tmp/2024-04-19/08:35:34.344-08:37:50.659.json index 9162235..5353d3d 100644 --- a/testdata/.pulse/tmp/2024-04-19/08:35:34.344-08:37:50.659.json +++ b/testdata/.pulse/tmp/2024-04-19/08:35:34.344-08:37:50.659.json @@ -1,6 +1,6 @@ { - "started_at": 1713508534344, - "ended_at": 1713508670659, + "started_at": "2024-04-19T08:35:34.344Z", + "ended_at": "2024-04-19T08:37:50.659Z", "duration_ms": 136315, "os": "darwin", "editor": "nvim", @@ -20,4 +20,5 @@ "duration_ms": 120686 } ] -} \ No newline at end of file +} + diff --git a/testdata/.pulse/tmp/2024-04-19/08:38:35.772-08:42:53.405.json b/testdata/.pulse/tmp/2024-04-19/08:38:35.772-08:42:53.405.json index a4cb70f..54a7ea6 100644 --- a/testdata/.pulse/tmp/2024-04-19/08:38:35.772-08:42:53.405.json +++ b/testdata/.pulse/tmp/2024-04-19/08:38:35.772-08:42:53.405.json @@ -1,6 +1,6 @@ { - "started_at": 1713508715772, - "ended_at": 1713508973405, + "started_at": "2024-04-19T08:38:35.772Z", + "ended_at": "2024-04-19T08:42:53.405Z", "duration_ms": 257633, "os": "darwin", "editor": "nvim", @@ -69,4 +69,5 @@ "duration_ms": 43194 } ] -} \ No newline at end of file +} + diff --git a/testdata/.pulse/tmp/2024-04-19/08:44:03.882-08:46:29.692.json b/testdata/.pulse/tmp/2024-04-19/08:44:03.882-08:46:29.692.json index ddbb847..879aeb8 100644 --- a/testdata/.pulse/tmp/2024-04-19/08:44:03.882-08:46:29.692.json +++ b/testdata/.pulse/tmp/2024-04-19/08:44:03.882-08:46:29.692.json @@ -1,6 +1,6 @@ { - "started_at": 1713509043882, - "ended_at": 1713509189692, + "started_at": "2024-04-19T08:44:03.882Z", + "ended_at": "2024-04-19T08:46:29.692Z", "duration_ms": 145810, "os": "darwin", "editor": "nvim", @@ -62,4 +62,5 @@ "duration_ms": 50190 } ] -} \ No newline at end of file +} + diff --git a/testdata/.pulse/tmp/2024-04-19/08:46:39.647-08:46:41.398.json b/testdata/.pulse/tmp/2024-04-19/08:46:39.647-08:46:41.398.json index b6889f0..661adef 100644 --- a/testdata/.pulse/tmp/2024-04-19/08:46:39.647-08:46:41.398.json +++ b/testdata/.pulse/tmp/2024-04-19/08:46:39.647-08:46:41.398.json @@ -1,6 +1,6 @@ { - "started_at": 1713509199647, - "ended_at": 1713509201398, + "started_at": "2024-04-19T08:46:39.647Z", + "ended_at": "2024-04-19T08:46:41.398Z", "duration_ms": 1751, "os": "darwin", "editor": "nvim", @@ -13,4 +13,5 @@ "duration_ms": 1751 } ] -} \ No newline at end of file +} + diff --git a/testdata/.pulse/tmp/2024-04-19/08:46:49.311-08:47:08.712.json b/testdata/.pulse/tmp/2024-04-19/08:46:49.311-08:47:08.712.json index 3e62ab7..149dc9b 100644 --- a/testdata/.pulse/tmp/2024-04-19/08:46:49.311-08:47:08.712.json +++ b/testdata/.pulse/tmp/2024-04-19/08:46:49.311-08:47:08.712.json @@ -1,6 +1,6 @@ { - "started_at": 1713509209311, - "ended_at": 1713509228712, + "started_at": "2024-04-19T08:46:49.311Z", + "ended_at": "2024-04-19T08:47:08.712Z", "duration_ms": 19401, "os": "darwin", "editor": "nvim", @@ -27,4 +27,5 @@ "duration_ms": 7883 } ] -} \ No newline at end of file +} + diff --git a/testdata/.pulse/tmp/2024-04-19/08:47:16.467-08:47:31.311.json b/testdata/.pulse/tmp/2024-04-19/08:47:16.467-08:47:31.311.json index e02bc96..737656b 100644 --- a/testdata/.pulse/tmp/2024-04-19/08:47:16.467-08:47:31.311.json +++ b/testdata/.pulse/tmp/2024-04-19/08:47:16.467-08:47:31.311.json @@ -1,6 +1,6 @@ { - "started_at": 1713509236467, - "ended_at": 1713509251311, + "started_at": "2024-04-19T08:47:16.467Z", + "ended_at": "2024-04-19T08:47:31.311Z", "duration_ms": 14844, "os": "darwin", "editor": "nvim", @@ -34,4 +34,5 @@ "duration_ms": 1631 } ] -} \ No newline at end of file +} + diff --git a/truncate/truncate.go b/truncate/truncate.go index 6d2e55e..bd3a107 100644 --- a/truncate/truncate.go +++ b/truncate/truncate.go @@ -16,19 +16,19 @@ func Week(timestamp int64) int64 { t = t.AddDate(0, 0, -1) } t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) - return t.UnixNano() / int64(time.Millisecond) + return t.UnixMilli() } // Month truncates the timestamp to the start of the month. func Month(timestamp int64) int64 { t := time.Unix(0, timestamp*int64(time.Millisecond)) t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) - return t.UnixNano() / int64(time.Millisecond) + return t.UnixMilli() } // Year truncates the timestamp to the start of the year. func Year(timestamp int64) int64 { t := time.Unix(0, timestamp*int64(time.Millisecond)) t = time.Date(t.Year(), time.January, 1, 0, 0, 0, 0, t.Location()) - return t.UnixNano() / int64(time.Millisecond) + return t.UnixMilli() }