Skip to content

Commit 7a04682

Browse files
authored
Add duration and type to request (#2)
* Add type and duration to requests * Add tests
1 parent 2a73fdd commit 7a04682

File tree

5 files changed

+135
-15
lines changed

5 files changed

+135
-15
lines changed

internal/funnel/funnel.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ type Funnel struct {
1515
Requests *RequestList
1616
}
1717

18-
1918
// ID returns the unique identifier for the funnel.
2019
func (f *Funnel) ID() string {
2120
if f.HTTPFunnel == nil {
@@ -87,4 +86,3 @@ func (f *Funnel) Destroy() error {
8786

8887
return nil
8988
}
90-

internal/funnel/http.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func (s *HttpServer) handleRequest(w http.ResponseWriter, r *http.Request) {
147147
proxy.Director = func(req *http.Request) {
148148
originalDirector(req)
149149

150-
// read the request body, the plan is to expose this in the UI somehow, but that comes
150+
// read the request body, the plan is to expose this in the UI somehow, but that comes
151151
// at the expense of increased memory usage... make this better
152152
var reqBodyBytes []byte
153153
var err error

internal/funnel/types.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package funnel
22

33
import (
4+
"fmt"
45
"net/url"
56
"strings"
67
"sync"
@@ -101,3 +102,45 @@ func (r *CaptureRequestResponse) Path() string {
101102
func (r *CaptureRequestResponse) StatusCode() int {
102103
return r.Response.StatusCode
103104
}
105+
106+
func (r *CaptureRequestResponse) Type() string {
107+
// use the response content-type header to determine the type of the request
108+
contentType := r.Response.Headers["Content-Type"]
109+
if contentType == "" {
110+
return ""
111+
}
112+
113+
// handle some explicit cases, json, html, xml, css, js, etc.
114+
if strings.HasPrefix(contentType, "application/json") {
115+
return "json"
116+
}
117+
if strings.HasPrefix(contentType, "text/html") {
118+
return "html"
119+
}
120+
if strings.HasPrefix(contentType, "text/xml") {
121+
return "xml"
122+
}
123+
if strings.HasPrefix(contentType, "text/css") {
124+
return "css"
125+
}
126+
if strings.HasPrefix(contentType, "text/javascript") {
127+
return "js"
128+
}
129+
if strings.HasPrefix(contentType, "text/plain") {
130+
return "txt"
131+
}
132+
// split the content type by "/" and take the first part
133+
parts := strings.Split(contentType, "/")
134+
if len(parts) > 0 {
135+
return parts[0]
136+
}
137+
return ""
138+
}
139+
140+
func (r *CaptureRequestResponse) RoundedDuration() string {
141+
if r.Duration.Seconds() >= 1 {
142+
// we want one decimal place
143+
return fmt.Sprintf("%.1fs", r.Duration.Seconds())
144+
}
145+
return fmt.Sprintf("%dms", r.Duration.Round(time.Millisecond).Milliseconds())
146+
}

internal/funnel/types_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,67 @@ func TestRequestList_Add(t *testing.T) {
200200

201201
})
202202
}
203+
204+
func TestCaptureRequestResponse_RoundedDuration(t *testing.T) {
205+
testCases := []struct {
206+
name string
207+
duration time.Duration
208+
expected string
209+
}{
210+
{"Zero duration", 0, "0ms"},
211+
{"Milliseconds", 123 * time.Millisecond, "123ms"},
212+
{"Half second", 500 * time.Millisecond, "500ms"},
213+
{"Just under 1 second", 999 * time.Millisecond, "999ms"},
214+
{"Exactly 1 second", 1 * time.Second, "1.0s"},
215+
{"1.2 seconds", 1200 * time.Millisecond, "1.2s"},
216+
{"1.23 seconds (rounds)", 1234 * time.Millisecond, "1.2s"}, // Should round based on fmt.Sprintf
217+
{"Long duration", 5*time.Minute + 30*time.Second + 456*time.Millisecond, "330.5s"},
218+
}
219+
220+
for _, tc := range testCases {
221+
t.Run(tc.name, func(t *testing.T) {
222+
crr := CaptureRequestResponse{Duration: tc.duration}
223+
if got := crr.RoundedDuration(); got != tc.expected {
224+
t.Errorf("Expected RoundedDuration() to be %q, got %q", tc.expected, got)
225+
}
226+
})
227+
}
228+
}
229+
230+
func TestCaptureRequestResponse_Type(t *testing.T) {
231+
testCases := []struct {
232+
name string
233+
contentType string
234+
expected string
235+
}{
236+
{"No Content-Type", "", ""},
237+
{"JSON", "application/json", "json"},
238+
{"JSON with charset", "application/json; charset=utf-8", "json"},
239+
{"HTML", "text/html", "html"},
240+
{"XML", "text/xml", "xml"},
241+
{"CSS", "text/css", "css"},
242+
{"JavaScript", "text/javascript", "js"},
243+
{"Plain Text", "text/plain", "txt"},
244+
{"Image PNG", "image/png", "image"},
245+
{"Video MP4", "video/mp4", "video"},
246+
{"Application Octet Stream", "application/octet-stream", "application"},
247+
{"Weird format", "foo/bar", "foo"},
248+
{"Only main type", "audio", "audio"},
249+
{"Empty string after split", "/", ""}, // Invalid Content-Type
250+
}
251+
252+
for _, tc := range testCases {
253+
t.Run(tc.name, func(t *testing.T) {
254+
headers := make(map[string]string)
255+
if tc.contentType != "" {
256+
headers["Content-Type"] = tc.contentType
257+
}
258+
crr := CaptureRequestResponse{
259+
Response: CaptureResponse{Headers: headers},
260+
}
261+
if got := crr.Type(); got != tc.expected {
262+
t.Errorf("Expected Type() to be %q for Content-Type %q, got %q", tc.expected, tc.contentType, got)
263+
}
264+
})
265+
}
266+
}

internal/tui/tui.go

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,11 @@ func createInitialTable() table.Model {
240240
func createRequestTable() table.Model {
241241
columns := []table.Column{
242242
{Title: "Timestamp"},
243-
{Title: "Method"},
244243
{Title: "Status"},
245-
{Title: "URL"},
244+
{Title: "Method"},
245+
{Title: "Path"},
246+
{Title: "Type"},
247+
{Title: "Duration"},
246248
{Title: "ID"}, // Hidden column for request ID
247249
}
248250

@@ -433,14 +435,24 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
433435
}
434436
m.table.SetColumns(newColumns)
435437

436-
urlWidth := tableWidth - 12 - 8 - 8 - 20
438+
timestampWidth := 12
439+
statusWidth := 7
440+
methodWidth := 7
441+
typeWidth := 8
442+
durationWidth := 10
443+
444+
bufferWidth := 10
445+
446+
urlWidth := tableWidth - timestampWidth - statusWidth - methodWidth - typeWidth - durationWidth - bufferWidth
437447

438448
requestColumns := []table.Column{
439-
{Title: "Timestamp", Width: 12},
440-
{Title: "Method", Width: 8},
441-
{Title: "Status", Width: 8},
442-
{Title: "URL", Width: urlWidth},
443-
{}, // Hidden column, no title or width needed
449+
{Title: "Timestamp", Width: timestampWidth},
450+
{Title: "Status", Width: statusWidth},
451+
{Title: "Method", Width: methodWidth},
452+
{Title: "Path", Width: urlWidth},
453+
{Title: "Type", Width: typeWidth},
454+
{Title: "Duration", Width: durationWidth},
455+
{}, // hiddend column that will store the id of the request
444456
}
445457
m.requestTable.SetColumns(requestColumns)
446458

@@ -721,10 +733,10 @@ func (m model) updateDetailView(msg tea.Msg) (tea.Model, tea.Cmd) {
721733
case "enter":
722734
if m.detailTabIndex == 1 {
723735
selectedRow := m.requestTable.SelectedRow()
724-
if len(selectedRow) < 5 { // Ensure row and ID exist (index 4)
736+
if len(selectedRow) < 6 { // Ensure row and ID exist (index 5)
725737
return m, nil // Or handle error
726738
}
727-
selectedRequestID := selectedRow[4] // Get ID from the hidden column
739+
selectedRequestID := selectedRow[len(selectedRow)-1] // id is always the last column
728740

729741
funnel, err := m.funnelRegistry.GetFunnel(m.detailedFunnelID)
730742
if err == nil {
@@ -981,9 +993,11 @@ func (m *model) populateRequestTable() {
981993
for node != nil {
982994
rows = append(rows, table.Row{
983995
node.Request.Timestamp.Format("15:04:05"),
984-
node.Request.Method(),
985996
strconv.Itoa(node.Request.StatusCode()),
997+
node.Request.Method(),
986998
node.Request.Path(),
999+
node.Request.Type(),
1000+
node.Request.RoundedDuration(),
9871001
node.Request.ID,
9881002
})
9891003
node = node.Next
@@ -1133,10 +1147,11 @@ func (m model) viewRequestDetailView(contentHeight int) string {
11331147
}
11341148

11351149
requestInfo := fmt.Sprintf(
1136-
"URL: %s\nMethod: %s\nStatus: %d",
1150+
"URL: %s\nMethod: %s\nStatus: %d\nDuration: %s",
11371151
m.selectedRequest.Path(),
11381152
m.selectedRequest.Method(),
11391153
m.selectedRequest.StatusCode(),
1154+
m.selectedRequest.RoundedDuration(),
11401155
)
11411156

11421157
formatHeaders := func(headers map[string]string) string {

0 commit comments

Comments
 (0)