From 82d5e3024464defba95191bff523db626a37c8bd Mon Sep 17 00:00:00 2001 From: Lukas Trombach Date: Fri, 21 Aug 2026 15:35:35 +0200 Subject: [PATCH 1/8] feat(collector): add NewBodyFromBytes for pre-populated bodies --- collector/body.go | 11 +++++++++++ collector/body_test.go | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/collector/body.go b/collector/body.go index 5c41230..b8087b4 100644 --- a/collector/body.go +++ b/collector/body.go @@ -37,6 +37,17 @@ func NewBody(rc io.ReadCloser, limit int) *Body { return b } +// NewBodyFromBytes creates a Body pre-populated with the given content, for +// callers that already hold the complete payload. +func NewBodyFromBytes(content []byte, limit int) *Body { + b := &Body{ + buffer: NewLimitedBuffer(limit), + } + b.buffer.Write(content) + b.isFullyCaptured = !b.buffer.IsTruncated() + return b +} + func (b *Body) Read(p []byte) (n int, err error) { b.mu.Lock() defer b.mu.Unlock() diff --git a/collector/body_test.go b/collector/body_test.go index 5dbb5cd..1eddbf5 100644 --- a/collector/body_test.go +++ b/collector/body_test.go @@ -44,6 +44,29 @@ func TestBody_PartialRead(t *testing.T) { assert.True(t, body.IsFullyCaptured()) } +func TestNewBodyFromBytes(t *testing.T) { + content := []byte("This is test data for a synthetic body") + + body := collector.NewBodyFromBytes(content, 100) + + assert.Equal(t, content, body.Bytes()) + assert.Equal(t, string(content), body.String()) + assert.Equal(t, uint64(len(content)), body.Size()) + assert.False(t, body.IsTruncated()) + assert.True(t, body.IsFullyCaptured()) +} + +func TestNewBodyFromBytes_Truncated(t *testing.T) { + content := []byte("This is test data that exceeds the configured limit") + + body := collector.NewBodyFromBytes(content, 10) + + assert.Equal(t, content[:10], body.Bytes()) + assert.Equal(t, uint64(10), body.Size()) + assert.True(t, body.IsTruncated()) + assert.False(t, body.IsFullyCaptured()) +} + // Fix for TestBody_ReadAfterClose func TestBody_ReadAfterClose(t *testing.T) { // Create test data From ad1cde4a9c7d90ec23494317f0eddebeabceae88 Mon Sep 17 00:00:00 2001 From: Lukas Trombach Date: Fri, 21 Aug 2026 15:36:53 +0200 Subject: [PATCH 2/8] fix(collector): set isFullyCaptured for NewBody(nil, ...) --- collector/body.go | 3 +++ collector/body_test.go | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/collector/body.go b/collector/body.go index b8087b4..af946ba 100644 --- a/collector/body.go +++ b/collector/body.go @@ -34,6 +34,9 @@ func NewBody(rc io.ReadCloser, limit int) *Body { reader: rc, buffer: NewLimitedBuffer(limit), } + if rc == nil { + b.isFullyCaptured = true + } return b } diff --git a/collector/body_test.go b/collector/body_test.go index 1eddbf5..492b113 100644 --- a/collector/body_test.go +++ b/collector/body_test.go @@ -67,6 +67,12 @@ func TestNewBodyFromBytes_Truncated(t *testing.T) { assert.False(t, body.IsFullyCaptured()) } +func TestNewBody_NilReaderIsFullyCaptured(t *testing.T) { + body := collector.NewBody(nil, 100) + + assert.True(t, body.IsFullyCaptured()) +} + // Fix for TestBody_ReadAfterClose func TestBody_ReadAfterClose(t *testing.T) { // Create test data From 11c4267557631f8947f533cc852617935c34e07e Mon Sep 17 00:00:00 2001 From: Lukas Trombach Date: Fri, 21 Aug 2026 15:41:00 +0200 Subject: [PATCH 3/8] feat(collector): add HTTPClientCollector.Collect and MaxBodySize --- collector/http_client.go | 38 ++++++ collector/http_client_test.go | 240 ++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) diff --git a/collector/http_client.go b/collector/http_client.go index 1306670..5576c6f 100644 --- a/collector/http_client.go +++ b/collector/http_client.go @@ -5,6 +5,8 @@ import ( "net/http" "strconv" "time" + + "github.com/gofrs/uuid" ) // HTTPClientOptions configures the HTTP client collector @@ -87,6 +89,42 @@ func (c *HTTPClientCollector) Add(req HTTPClientRequest) { c.notifier.Notify(req) } +// Collect records an HTTP client request that was not performed through the +// transport returned by Transport, e.g. a response served from a local cache. +// +// In contrast to Add, it honours capture sessions and groups the request under +// the current event group taken from ctx. +func (c *HTTPClientCollector) Collect(ctx context.Context, req HTTPClientRequest) { + if c.eventAggregator != nil && !c.eventAggregator.ShouldCapture(ctx) { + return + } + + if req.ID == uuid.Nil { + req.ID = generateID() + } + if req.RequestTime.IsZero() { + req.RequestTime = time.Now() + } + if req.ResponseTime.IsZero() { + req.ResponseTime = req.RequestTime + } + + for _, transformer := range c.options.Transformers { + req = transformer(req) + } + + c.notifier.Notify(req) + if c.eventAggregator != nil { + c.eventAggregator.CollectEvent(ctx, req) + } +} + +// MaxBodySize returns the configured maximum body size for this collector, +// for callers building a *Body via NewBodyFromBytes for use with Collect. +func (c *HTTPClientCollector) MaxBodySize() int { + return c.options.MaxBodySize +} + // Close releases resources used by the collector func (c *HTTPClientCollector) Close() { c.notifier.Close() diff --git a/collector/http_client_test.go b/collector/http_client_test.go index 8564aeb..10bd7ec 100644 --- a/collector/http_client_test.go +++ b/collector/http_client_test.go @@ -1,13 +1,16 @@ package collector_test import ( + "context" "io" "net/http" "net/http/httptest" "strconv" "strings" "testing" + "time" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -70,6 +73,243 @@ func TestHTTPClientCollector_UnreadResponseBody(t *testing.T) { assert.True(t, req.ResponseBody.IsFullyCaptured()) } +func TestHTTPClientCollector_Collect_NoCapture_NoEventNoNotify(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + options := collector.DefaultHTTPClientOptions() + options.EventAggregator = aggregator + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + collect := Collect(t, httpCollector.Subscribe) + + // No storage registered, so ShouldCapture is false for any context + httpCollector.Collect(context.Background(), collector.HTTPClientRequest{ + Method: http.MethodGet, + URL: "https://example.com", + }) + + // Give the notifier a chance to deliver, if it were going to + time.Sleep(20 * time.Millisecond) + + assert.Empty(t, collect.Stop()) +} + +func TestHTTPClientCollector_Collect_DispatchesToStorage(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + options := collector.DefaultHTTPClientOptions() + options.EventAggregator = aggregator + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + httpCollector.Collect(context.Background(), collector.HTTPClientRequest{ + Method: http.MethodGet, + URL: "https://example.com", + }) + + events := storage.GetEvents(10) + require.Len(t, events, 1) + + req, ok := events[0].Data.(collector.HTTPClientRequest) + require.True(t, ok) + assert.Equal(t, "https://example.com", req.URL) +} + +func TestHTTPClientCollector_Collect_GroupsUnderParentEvent(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + options := collector.DefaultHTTPClientOptions() + options.EventAggregator = aggregator + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + parentCtx := aggregator.StartEvent(context.Background()) + + httpCollector.Collect(parentCtx, collector.HTTPClientRequest{ + Method: http.MethodGet, + URL: "https://example.com/child", + }) + + aggregator.EndEvent(parentCtx, "parent event") + + events := storage.GetEvents(10) + require.Len(t, events, 1, "the child request must not be dispatched as a top-level event") + + parent := events[0] + require.Len(t, parent.Children, 1) + + req, ok := parent.Children[0].Data.(collector.HTTPClientRequest) + require.True(t, ok) + assert.Equal(t, "https://example.com/child", req.URL) +} + +func TestHTTPClientCollector_Collect_NotifiesSubscribers(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + options := collector.DefaultHTTPClientOptions() + options.EventAggregator = aggregator + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + collect := Collect(t, httpCollector.Subscribe) + + httpCollector.Collect(context.Background(), collector.HTTPClientRequest{ + Method: http.MethodGet, + URL: "https://example.com", + }) + + received := collect.Wait(1) + require.Len(t, received, 1) + assert.Equal(t, "https://example.com", received[0].URL) +} + +func TestHTTPClientCollector_Collect_AppliesTransformers(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + options := collector.DefaultHTTPClientOptions() + options.EventAggregator = aggregator + options.Transformers = []collector.HTTPClientRequestTransformer{ + func(req collector.HTTPClientRequest) collector.HTTPClientRequest { + if req.Tags == nil { + req.Tags = map[string]string{} + } + req.Tags["transformed"] = "true" + return req + }, + } + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + httpCollector.Collect(context.Background(), collector.HTTPClientRequest{ + Method: http.MethodGet, + URL: "https://example.com", + }) + + events := storage.GetEvents(10) + require.Len(t, events, 1) + + req, ok := events[0].Data.(collector.HTTPClientRequest) + require.True(t, ok) + assert.Equal(t, "true", req.Tags["transformed"]) +} + +func TestHTTPClientCollector_Collect_FillsZeroValues(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + options := collector.DefaultHTTPClientOptions() + options.EventAggregator = aggregator + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + before := time.Now() + + httpCollector.Collect(context.Background(), collector.HTTPClientRequest{ + Method: http.MethodGet, + URL: "https://example.com", + }) + + events := storage.GetEvents(10) + require.Len(t, events, 1) + + req, ok := events[0].Data.(collector.HTTPClientRequest) + require.True(t, ok) + + assert.NotEqual(t, uuid.Nil, req.ID) + assert.False(t, req.RequestTime.Before(before)) + assert.Equal(t, req.RequestTime, req.ResponseTime) +} + +func TestHTTPClientCollector_Collect_PreservesNonZeroValues(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + options := collector.DefaultHTTPClientOptions() + options.EventAggregator = aggregator + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + id := uuid.Must(uuid.NewV7()) + requestTime := time.Now().Add(-time.Minute) + responseTime := time.Now().Add(-30 * time.Second) + + httpCollector.Collect(context.Background(), collector.HTTPClientRequest{ + ID: id, + Method: http.MethodGet, + URL: "https://example.com", + RequestTime: requestTime, + ResponseTime: responseTime, + }) + + events := storage.GetEvents(10) + require.Len(t, events, 1) + + req, ok := events[0].Data.(collector.HTTPClientRequest) + require.True(t, ok) + + assert.Equal(t, id, req.ID) + assert.True(t, requestTime.Equal(req.RequestTime)) + assert.True(t, responseTime.Equal(req.ResponseTime)) +} + +func TestHTTPClientCollector_Add_NotifiesButNoEvent(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + options := collector.DefaultHTTPClientOptions() + options.EventAggregator = aggregator + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + collect := Collect(t, httpCollector.Subscribe) + + httpCollector.Add(collector.HTTPClientRequest{ + Method: http.MethodGet, + URL: "https://example.com", + }) + + received := collect.Wait(1) + require.Len(t, received, 1) + assert.Equal(t, "https://example.com", received[0].URL) + + // Add never reaches the event aggregator + assert.Empty(t, storage.GetEvents(10)) +} + +func TestHTTPClientCollector_MaxBodySize(t *testing.T) { + options := collector.DefaultHTTPClientOptions() + options.MaxBodySize = 4096 + httpCollector := collector.NewHTTPClientCollectorWithOptions(options) + + assert.Equal(t, 4096, httpCollector.MaxBodySize()) +} + // BodyReadTracker tracks if a response body was read type BodyReadTracker struct { data string From 9471095b578284338b03fe434aec93ecf31a5a5b Mon Sep 17 00:00:00 2001 From: Lukas Trombach Date: Fri, 21 Aug 2026 15:42:03 +0200 Subject: [PATCH 4/8] feat: add Instance.CollectHTTPClientRequest --- devlog.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/devlog.go b/devlog.go index bb7ec27..c5554a6 100644 --- a/devlog.go +++ b/devlog.go @@ -118,6 +118,13 @@ func (i *Instance) CollectDBQuery() func(ctx context.Context, dbQuery collector. return i.dbQueryCollector.Collect } +// CollectHTTPClientRequest allows to record outgoing HTTP requests that were not +// performed through the transport returned by CollectHTTPClient, e.g. responses +// served from a cache. +func (i *Instance) CollectHTTPClientRequest() func(ctx context.Context, req collector.HTTPClientRequest) { + return i.httpClientCollector.Collect +} + // DashboardHandler creates a dashboard handler mounted at the given path prefix. // Use functional options from the dashboard package to customize behavior: // From 1333306d39292fbbd53b787d516f9acd4c162524 Mon Sep 17 00:00:00 2001 From: Lukas Trombach Date: Fri, 21 Aug 2026 16:03:55 +0200 Subject: [PATCH 5/8] docs(collector): clarify Add is notify-only, not a smaller Collect --- collector/http_client.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/collector/http_client.go b/collector/http_client.go index 5576c6f..51a5db2 100644 --- a/collector/http_client.go +++ b/collector/http_client.go @@ -84,7 +84,9 @@ func (c *HTTPClientCollector) Subscribe(ctx context.Context) <-chan HTTPClientRe return c.notifier.Subscribe(ctx) } -// Add adds an HTTP request to the collector and notifies subscribers +// Add notifies subscribers of an HTTP request. It does not apply transformers +// or involve the event aggregator. For a synthetic request not performed through +// Transport, use Collect instead. func (c *HTTPClientCollector) Add(req HTTPClientRequest) { c.notifier.Notify(req) } From f57c786bd4df1f0f6355e7cd477331d7fec43f28 Mon Sep 17 00:00:00 2001 From: Lukas Trombach Date: Fri, 21 Aug 2026 16:10:48 +0200 Subject: [PATCH 6/8] feat(dashboard): render Tags in HTTP client request details --- dashboard/views/event-details.templ | 3 + dashboard/views/event-details_templ.go | 234 +++++++++++++------------ 2 files changed, 125 insertions(+), 112 deletions(-) diff --git a/dashboard/views/event-details.templ b/dashboard/views/event-details.templ index d839c5e..02086c6 100644 --- a/dashboard/views/event-details.templ +++ b/dashboard/views/event-details.templ @@ -124,6 +124,9 @@ templ HTTPRequestDetails(event *collector.Event, request collector.HTTPClientReq { formatTime(request.RequestTime) } + if len(request.Tags) > 0 { + @tagList(request.Tags) + } diff --git a/dashboard/views/event-details_templ.go b/dashboard/views/event-details_templ.go index b0569ba..9ab6432 100644 --- a/dashboard/views/event-details_templ.go +++ b/dashboard/views/event-details_templ.go @@ -406,61 +406,71 @@ func HTTPRequestDetails(event *collector.Event, request collector.HTTPClientRequ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

URL

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(request.Tags) > 0 { + templ_7745c5c3_Err = tagList(request.Tags).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

URL

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var19 string templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(request.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 133, Col: 29} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 136, Col: 29} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "

Request

Headers

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

Request

Headers

NameValue
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for key, values := range request.RequestHeaders { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
NameValue
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var20 string templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 155, Col: 77} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 158, Col: 77} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(values, ", ")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 156, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 159, Col: 100} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if request.RequestBody != nil && request.RequestBody.Size() > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "

Body

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" class=\"text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1\" download> Download
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -477,63 +487,63 @@ func HTTPRequestDetails(event *collector.Event, request collector.HTTPClientRequ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

Response

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

Response

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(request.ResponseHeaders) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "

Headers

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

Headers

NameValue
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for key, values := range request.ResponseHeaders { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
NameValue
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var23 string templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 202, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 205, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var24 string templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(values, ", ")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 203, Col: 104} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 206, Col: 104} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if request.ResponseBody != nil && request.ResponseBody.Size() > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

Body

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" class=\"text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1\" download> Download
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -550,12 +560,12 @@ func HTTPRequestDetails(event *collector.Event, request collector.HTTPClientRequ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -586,7 +596,7 @@ func HTTPServerRequestDetails(event *collector.Event, request collector.HTTPServ } ctx = templ.ClearChildren(ctx) duration := request.Duration() - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -597,7 +607,7 @@ func HTTPServerRequestDetails(event *collector.Event, request collector.HTTPServ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(request.Method) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 244, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 247, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(request.Path) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 246, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 249, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "

Status: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
Status: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -645,7 +655,7 @@ func HTTPServerRequestDetails(event *collector.Event, request collector.HTTPServ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var33 string templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(request.StatusCode)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 254, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 257, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var34 string templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(formatDuration(duration)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 260, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 263, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
Incoming
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
Incoming
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var35 string templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(formatTime(request.RequestTime)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 267, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 270, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
From: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
From: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var36 string templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(request.RemoteAddr) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 271, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 274, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "

URL

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

URL

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var37 string templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(request.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 280, Col: 29} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 283, Col: 29} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "

Request

Headers

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "

Request

Headers

NameValue
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for key, values := range request.RequestHeaders { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
NameValue
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var38 string templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 302, Col: 77} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 305, Col: 77} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var39 string templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(values, ", ")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 303, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 306, Col: 100} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if request.RequestBody != nil && request.RequestBody.Size() > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "

Body

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" class=\"text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1\" download> Download
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -781,63 +791,63 @@ func HTTPServerRequestDetails(event *collector.Event, request collector.HTTPServ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "

Response

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "

Response

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(request.ResponseHeaders) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "

Headers

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "

Headers

NameValue
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for key, values := range request.ResponseHeaders { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
NameValue
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var41 string templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 349, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 352, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var42 string templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(values, ", ")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 350, Col: 104} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 353, Col: 104} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if request.ResponseBody != nil && request.ResponseBody.Size() > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "

Body

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" class=\"text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1\" download> Download
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -854,12 +864,12 @@ func HTTPServerRequestDetails(event *collector.Event, request collector.HTTPServ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -889,7 +899,7 @@ func LogRecordDetails(event *collector.Event, record slog.Record) templ.Componen templ_7745c5c3_Var44 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -900,7 +910,7 @@ func LogRecordDetails(event *collector.Event, record slog.Record) templ.Componen if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var47 string templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(record.Level) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 390, Col: 34} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 393, Col: 34} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var48 string templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(record.Message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 392, Col: 75} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 395, Col: 75} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var49 string templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(formatTime(record.Time)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 397, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 400, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "

Attributes

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "

Attributes

KeyValue
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for attr := range iterSlogAttrs(record) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "
KeyValue
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var50 string templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(attr.Key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 416, Col: 78} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 419, Col: 78} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var51 string templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(attr.Value.String()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 417, Col: 89} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 420, Col: 89} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "

Context

Log recorded at ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "

Context

Log recorded at ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var52 string templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(formatTime(record.Time)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 429, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 432, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, ".

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, ".

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if event.Start != event.End { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "

Duration: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "

Duration: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var53 string templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(formatDuration(event.End.Sub(event.Start))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 431, Col: 90} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 434, Col: 90} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1054,150 +1064,150 @@ func DBQueryDetails(event *collector.Event, query collector.DBQuery) templ.Compo templ_7745c5c3_Var54 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "

Database Query

")
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "

Database Query

")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
 		var templ_7745c5c3_Var55 string
 		templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(query.Query)
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 444, Col: 86}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 447, Col: 86}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(query.Args) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "

Arguments

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "

Arguments

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, arg := range query.Args { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var56 string templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(arg.Ordinal) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 453, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 456, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var57 string templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(arg.Value)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 454, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 457, Col: 65} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "

Details

Duration
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "

Details

Duration
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var58 string templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2fms", float64(query.Duration.Microseconds())/1000)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 465, Col: 88} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 468, Col: 88} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "
Timestamp
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "
Timestamp
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var59 string templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(query.Timestamp.Format("2006-01-02 15:04:05.000")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 468, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 471, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if query.Language != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "
Language
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "
Language
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var60 string templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinStringErrs(query.Language) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 472, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 475, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if query.Error != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "

Error

")
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "

Error

")
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 			var templ_7745c5c3_Var61 string
 			templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(query.Error.Error())
 			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 481, Col: 84}
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-details.templ`, Line: 484, Col: 84}
 			}
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
-			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, " || \"sql\",\n });\n queryContent.textContent = output;\n })();\n ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From a5d270745f993bd79bc2475a1a6d5dafcfdd7bcf Mon Sep 17 00:00:00 2001 From: Lukas Trombach Date: Fri, 21 Aug 2026 16:14:19 +0200 Subject: [PATCH 7/8] test(dashboard): verify body download works for synthetic bodies --- dashboard/handler_download_test.go | 109 +++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 dashboard/handler_download_test.go diff --git a/dashboard/handler_download_test.go b/dashboard/handler_download_test.go new file mode 100644 index 0000000..dd6c83f --- /dev/null +++ b/dashboard/handler_download_test.go @@ -0,0 +1,109 @@ +package dashboard + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofrs/uuid" + + "github.com/networkteam/devlog/collector" +) + +func startGlobalCapture(t *testing.T, handler *Handler, sessionID uuid.UUID) { + t.Helper() + + req := httptest.NewRequest(http.MethodPost, "/s/"+sessionID.String()+"/capture/start?mode=global", nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("capture/start: unexpected status %d: %s", rec.Code, rec.Body.String()) + } +} + +// TestHandler_DownloadBody_SyntheticBody verifies that the download endpoints +// work for HTTPClientRequest bodies built via collector.NewBodyFromBytes, not +// just bodies captured lazily from a live http.Response - both request and +// response body downloads resolve the body from the stored event by ID, so +// they should be agnostic to how the *Body was constructed. +func TestHandler_DownloadBody_SyntheticBody(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + httpClientCollector := collector.NewHTTPClientCollectorWithOptions(collector.HTTPClientOptions{ + EventAggregator: aggregator, + }) + defer httpClientCollector.Close() + + handler := NewHandler(aggregator) + defer handler.Close() + + sessionID := uuid.Must(uuid.NewV4()) + startGlobalCapture(t, handler, sessionID) + + requestBody := []byte(`{"request":"payload"}`) + responseBody := []byte(`{"response":"payload"}`) + + httpClientCollector.Collect(context.Background(), collector.HTTPClientRequest{ + Method: http.MethodPost, + URL: "https://example.com/v2/items", + StatusCode: http.StatusOK, + RequestBody: collector.NewBodyFromBytes(requestBody, collector.DefaultMaxBodySize), + RequestHeaders: http.Header{"Content-Type": {"application/json"}}, + ResponseBody: collector.NewBodyFromBytes(responseBody, collector.DefaultMaxBodySize), + ResponseHeaders: http.Header{"Content-Type": {"application/json"}}, + }) + + storage := handler.sessions.Get(sessionID) + if storage == nil { + t.Fatal("expected storage to exist after capture start") + } + events := storage.GetEvents(10) + if len(events) != 1 { + t.Fatalf("expected 1 stored event, got %d", len(events)) + } + eventID := events[0].ID + + t.Run("request body", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/s/"+sessionID.String()+"/download/request-body/"+eventID.String(), nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("unexpected status %d: %s", rec.Code, rec.Body.String()) + } + got, err := io.ReadAll(rec.Body) + if err != nil { + t.Fatalf("read body: %v", err) + } + if string(got) != string(requestBody) { + t.Errorf("expected body %q, got %q", requestBody, got) + } + if ct := rec.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("expected Content-Type application/json, got %q", ct) + } + }) + + t.Run("response body", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/s/"+sessionID.String()+"/download/response-body/"+eventID.String(), nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("unexpected status %d: %s", rec.Code, rec.Body.String()) + } + got, err := io.ReadAll(rec.Body) + if err != nil { + t.Fatalf("read body: %v", err) + } + if string(got) != string(responseBody) { + t.Errorf("expected body %q, got %q", responseBody, got) + } + if ct := rec.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("expected Content-Type application/json, got %q", ct) + } + }) +} From fd893929d9be3fdc6bb63097201cd7d0dae287ab Mon Sep 17 00:00:00 2001 From: Lukas Trombach Date: Fri, 21 Aug 2026 16:17:21 +0200 Subject: [PATCH 8/8] test(views): verify formatDuration handles zero and sub-ms values --- dashboard/views/format_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 dashboard/views/format_test.go diff --git a/dashboard/views/format_test.go b/dashboard/views/format_test.go new file mode 100644 index 0000000..c4e84b6 --- /dev/null +++ b/dashboard/views/format_test.go @@ -0,0 +1,30 @@ +package views + +import ( + "testing" + "time" +) + +func TestFormatDuration(t *testing.T) { + tests := []struct { + name string + d time.Duration + want string + }{ + {"zero", 0, "0μs"}, + {"sub-millisecond", 500 * time.Microsecond, "500μs"}, + {"just under a millisecond", 999 * time.Microsecond, "999μs"}, + {"exactly one millisecond", time.Millisecond, "1ms"}, + {"just under a second", 999 * time.Millisecond, "999ms"}, + {"exactly one second", time.Second, "1.00s"}, + {"multiple seconds", 2500 * time.Millisecond, "2.50s"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := formatDuration(tt.d); got != tt.want { + t.Errorf("formatDuration(%v) = %q, want %q", tt.d, got, tt.want) + } + }) + } +}