← back to Cli Printing Press
feat(websniff): add HAR and enriched capture parser
6c6c9cf59a55b6975ddd30bc39b7b8344ca0c9b6 · 2026-03-28 21:26:15 -0700 · Matt Van Horn
New internal/websniff/ package with types for HAR 1.2 and enriched
capture formats. ParseCapture() auto-detects format and normalizes
both into EnrichedEntry slice for downstream processing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
A internal/websniff/parser.goA internal/websniff/parser_test.goA internal/websniff/types.goA testdata/sniff/sample-enriched.jsonA testdata/sniff/sample.har
Diff
commit 6c6c9cf59a55b6975ddd30bc39b7b8344ca0c9b6
Author: Matt Van Horn <mvanhorn@MacBook-Pro.local>
Date: Sat Mar 28 21:26:15 2026 -0700
feat(websniff): add HAR and enriched capture parser
New internal/websniff/ package with types for HAR 1.2 and enriched
capture formats. ParseCapture() auto-detects format and normalizes
both into EnrichedEntry slice for downstream processing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
internal/websniff/parser.go | 95 +++++++++
internal/websniff/parser_test.go | 154 ++++++++++++++
internal/websniff/types.go | 61 ++++++
testdata/sniff/sample-enriched.json | 42 ++++
testdata/sniff/sample.har | 408 ++++++++++++++++++++++++++++++++++++
5 files changed, 760 insertions(+)
diff --git a/internal/websniff/parser.go b/internal/websniff/parser.go
new file mode 100644
index 00000000..7d1ba2f2
--- /dev/null
+++ b/internal/websniff/parser.go
@@ -0,0 +1,95 @@
+package websniff
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+)
+
+func ParseHAR(path string) (*HAR, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("reading file: %w", err)
+ }
+
+ var har HAR
+ if err := json.Unmarshal(data, &har); err != nil {
+ return nil, fmt.Errorf("parsing har json: %w", err)
+ }
+
+ return &har, nil
+}
+
+func ParseEnriched(path string) (*EnrichedCapture, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("reading file: %w", err)
+ }
+
+ var capture EnrichedCapture
+ if err := json.Unmarshal(data, &capture); err != nil {
+ return nil, fmt.Errorf("parsing enriched json: %w", err)
+ }
+
+ return &capture, nil
+}
+
+func ParseCapture(path string) ([]EnrichedEntry, string, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, "", fmt.Errorf("reading file: %w", err)
+ }
+
+ if bytes.Contains(data, []byte(`"log"`)) {
+ var har HAR
+ if err := json.Unmarshal(data, &har); err != nil {
+ return nil, "", fmt.Errorf("parsing har json: %w", err)
+ }
+
+ entries := make([]EnrichedEntry, 0, len(har.Log.Entries))
+ for _, entry := range har.Log.Entries {
+ entries = append(entries, convertHAREntry(entry))
+ }
+
+ targetURL := ""
+ if len(har.Log.Entries) > 0 {
+ targetURL = har.Log.Entries[0].Request.URL
+ }
+
+ return entries, targetURL, nil
+ }
+
+ if bytes.Contains(data, []byte(`"target_url"`)) {
+ var capture EnrichedCapture
+ if err := json.Unmarshal(data, &capture); err != nil {
+ return nil, "", fmt.Errorf("parsing enriched json: %w", err)
+ }
+
+ return capture.Entries, capture.TargetURL, nil
+ }
+
+ return nil, "", fmt.Errorf("unknown capture format")
+}
+
+func convertHAREntry(entry HAREntry) EnrichedEntry {
+ headers := make(map[string]string, len(entry.Request.Headers))
+ for _, header := range entry.Request.Headers {
+ headers[header.Name] = header.Value
+ }
+
+ requestBody := ""
+ if entry.Request.PostData != nil {
+ requestBody = entry.Request.PostData.Text
+ }
+
+ return EnrichedEntry{
+ Method: entry.Request.Method,
+ URL: entry.Request.URL,
+ RequestBody: requestBody,
+ ResponseBody: entry.Response.Content.Text,
+ ResponseStatus: entry.Response.Status,
+ ResponseContentType: entry.Response.Content.MimeType,
+ RequestHeaders: headers,
+ }
+}
diff --git a/internal/websniff/parser_test.go b/internal/websniff/parser_test.go
new file mode 100644
index 00000000..1f85acb6
--- /dev/null
+++ b/internal/websniff/parser_test.go
@@ -0,0 +1,154 @@
+package websniff
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseHAR(t *testing.T) {
+ t.Parallel()
+
+ har, err := ParseHAR(filepath.Join("..", "..", "testdata", "sniff", "sample.har"))
+ require.NoError(t, err)
+
+ assert.Len(t, har.Log.Entries, 5)
+ assert.Equal(t, "GET", har.Log.Entries[1].Request.Method)
+ assert.Equal(t, "https://httpbin.org/get", har.Log.Entries[1].Request.URL)
+}
+
+func TestParseEnriched(t *testing.T) {
+ t.Parallel()
+
+ capture, err := ParseEnriched(filepath.Join("..", "..", "testdata", "sniff", "sample-enriched.json"))
+ require.NoError(t, err)
+
+ assert.Len(t, capture.Entries, 3)
+ assert.NotEmpty(t, capture.Entries[0].ResponseBody)
+ assert.Equal(t, "https://hn.algolia.com", capture.TargetURL)
+}
+
+func TestParseCapture(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ path string
+ wantEntries int
+ wantTargetURL string
+ wantFirstURL string
+ wantFirstBody string
+ wantStatusCode int
+ }{
+ {
+ name: "auto-detects har format",
+ path: filepath.Join("..", "..", "testdata", "sniff", "sample.har"),
+ wantEntries: 5,
+ wantTargetURL: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwcHgiICBoZWlnaHQ9IjIwMHB4IiAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiIGNsYXNzPSJsZHMtcm9sbGluZyIgc3R5bGU9ImJhY2tncm91bmQtaW1hZ2U6IG5vbmU7IGJhY2tncm91bmQtcG9zaXRpb246IGluaXRpYWwgaW5pdGlhbDsgYmFja2dyb3VuZC1yZXBlYXQ6IGluaXRpYWwgaW5pdGlhbDsiPjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIGZpbGw9Im5vbmUiIG5nLWF0dHItc3Ryb2tlPSJ7e2NvbmZpZy5jb2xvcn19IiBuZy1hdHRyLXN0cm9rZS13aWR0aD0ie3tjb25maWcud2lkdGh9fSIgbmctYXR0ci1yPSJ7e2NvbmZpZy5yYWRpdXN9fSIgbmctYXR0ci1zdHJva2UtZGFzaGFycmF5PSJ7e2NvbmZpZy5kYXNoYXJyYXl9fSIgc3Ryb2tlPSIjNTU1NTU1IiBzdHJva2Utd2lkdGg9IjEwIiByPSIzNSIgc3Ryb2tlLWRhc2hhcnJheT0iMTY0LjkzMzYxNDMxMzQ2NDE1IDU2Ljk3Nzg3MTQzNzgyMTM4Ij48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgY2FsY01vZGU9ImxpbmVhciIgdmFsdWVzPSIwIDUwIDUwOzM2MCA1MCA1MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49IjBzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlVHJhbnNmb3JtPjwvY2lyY2xlPjwvc3ZnPgo=",
+ wantFirstURL: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwcHgiICBoZWlnaHQ9IjIwMHB4IiAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiIGNsYXNzPSJsZHMtcm9sbGluZyIgc3R5bGU9ImJhY2tncm91bmQtaW1hZ2U6IG5vbmU7IGJhY2tncm91bmQtcG9zaXRpb246IGluaXRpYWwgaW5pdGlhbDsgYmFja2dyb3VuZC1yZXBlYXQ6IGluaXRpYWwgaW5pdGlhbDsiPjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIGZpbGw9Im5vbmUiIG5nLWF0dHItc3Ryb2tlPSJ7e2NvbmZpZy5jb2xvcn19IiBuZy1hdHRyLXN0cm9rZS13aWR0aD0ie3tjb25maWcud2lkdGh9fSIgbmctYXR0ci1yPSJ7e2NvbmZpZy5yYWRpdXN9fSIgbmctYXR0ci1zdHJva2UtZGFzaGFycmF5PSJ7e2NvbmZpZy5kYXNoYXJyYXl9fSIgc3Ryb2tlPSIjNTU1NTU1IiBzdHJva2Utd2lkdGg9IjEwIiByPSIzNSIgc3Ryb2tlLWRhc2hhcnJheT0iMTY0LjkzMzYxNDMxMzQ2NDE1IDU2Ljk3Nzg3MTQzNzgyMTM4Ij48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgY2FsY01vZGU9ImxpbmVhciIgdmFsdWVzPSIwIDUwIDUwOzM2MCA1MCA1MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49IjBzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlVHJhbnNmb3JtPjwvY2lyY2xlPjwvc3ZnPgo=",
+ wantStatusCode: 200,
+ },
+ {
+ name: "auto-detects enriched format",
+ path: filepath.Join("..", "..", "testdata", "sniff", "sample-enriched.json"),
+ wantEntries: 3,
+ wantTargetURL: "https://hn.algolia.com",
+ wantFirstURL: "https://uj5wyc0l7x-dsn.algolia.net/1/indexes/Item_dev/query?x-algolia-api-key=28f0e1ec37a5e792e6845e67da5f20dd&x-algolia-application-id=UJ5WYC0L7X",
+ wantFirstBody: "{\"hits\": [{\"objectID\": \"21711748\", \"title\": \"Troubleshooting K8s\", \"url\": \"https://example.com\", \"author\": \"test\", \"points\": 240, \"num_comments\": 53, \"created_at\": \"2019-12-05T12:22:28Z\", \"created_at_i\": 1575548548}, {\"objectID\": \"21711749\", \"title\": \"K8s Best Practices\", \"url\": \"https://example2.com\", \"author\": \"test2\", \"points\": 180, \"num_comments\": 30, \"created_at\": \"2020-01-10T08:00:00Z\", \"created_at_i\": 1578643200}], \"nbHits\": 4804, \"page\": 0, \"nbPages\": 334, \"hitsPerPage\": 3, \"processingTimeMS\": 16, \"query\": \"kubernetes\"}",
+ wantStatusCode: 200,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ entries, targetURL, err := ParseCapture(tt.path)
+ require.NoError(t, err)
+
+ assert.Len(t, entries, tt.wantEntries)
+ assert.Equal(t, tt.wantTargetURL, targetURL)
+ assert.Equal(t, tt.wantFirstURL, entries[0].URL)
+ assert.Equal(t, tt.wantFirstBody, entries[0].ResponseBody)
+ assert.Equal(t, tt.wantStatusCode, entries[0].ResponseStatus)
+ })
+ }
+}
+
+func TestParseHAR_EdgeCases(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ content string
+ path string
+ parse func(string) (*HAR, error)
+ assertError func(*testing.T, *HAR, error)
+ }{
+ {
+ name: "empty har",
+ content: `{"log":{"entries":[]}}`,
+ parse: ParseHAR,
+ assertError: func(t *testing.T, har *HAR, err error) {
+ require.NoError(t, err)
+ assert.NotNil(t, har)
+ assert.Len(t, har.Log.Entries, 0)
+ },
+ },
+ {
+ name: "invalid json",
+ content: `{`,
+ parse: ParseHAR,
+ assertError: func(t *testing.T, har *HAR, err error) {
+ require.Error(t, err)
+ assert.Nil(t, har)
+ },
+ },
+ {
+ name: "missing file",
+ path: filepath.Join(t.TempDir(), "missing.har"),
+ parse: ParseHAR,
+ assertError: func(t *testing.T, har *HAR, err error) {
+ require.Error(t, err)
+ assert.Nil(t, har)
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ path := tt.path
+ if tt.content != "" {
+ path = filepath.Join(t.TempDir(), "capture.har")
+ err := os.WriteFile(path, []byte(tt.content), 0o600)
+ require.NoError(t, err)
+ }
+
+ har, err := tt.parse(path)
+ tt.assertError(t, har, err)
+ })
+ }
+}
+
+func TestParseCapture_InvalidJSON(t *testing.T) {
+ t.Parallel()
+
+ path := filepath.Join(t.TempDir(), "capture.json")
+ err := os.WriteFile(path, []byte(`{`), 0o600)
+ require.NoError(t, err)
+
+ entries, targetURL, err := ParseCapture(path)
+ require.Error(t, err)
+ assert.Nil(t, entries)
+ assert.Empty(t, targetURL)
+}
+
+func TestParseCapture_MissingFile(t *testing.T) {
+ t.Parallel()
+
+ entries, targetURL, err := ParseCapture(filepath.Join(t.TempDir(), "missing.json"))
+ require.Error(t, err)
+ assert.Nil(t, entries)
+ assert.Empty(t, targetURL)
+}
diff --git a/internal/websniff/types.go b/internal/websniff/types.go
new file mode 100644
index 00000000..48597dc2
--- /dev/null
+++ b/internal/websniff/types.go
@@ -0,0 +1,61 @@
+package websniff
+
+type HAR struct {
+ Log HARLog `json:"log"`
+}
+
+type HARLog struct {
+ Entries []HAREntry `json:"entries"`
+}
+
+type HAREntry struct {
+ Request HARRequest `json:"request"`
+ Response HARResponse `json:"response"`
+}
+
+type HARRequest struct {
+ Method string `json:"method"`
+ URL string `json:"url"`
+ Headers []HARHeader `json:"headers"`
+ PostData *HARPostData `json:"postData,omitempty"`
+}
+
+type HARPostData struct {
+ MimeType string `json:"mimeType"`
+ Text string `json:"text"`
+}
+
+type HARResponse struct {
+ Status int `json:"status"`
+ Content HARResponseContent `json:"content"`
+}
+
+type HARResponseContent struct {
+ MimeType string `json:"mimeType"`
+ Size int `json:"size"`
+ Text string `json:"text"`
+}
+
+type HARHeader struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+}
+
+type EnrichedCapture struct {
+ TargetURL string `json:"target_url"`
+ CapturedAt string `json:"captured_at"`
+ InteractionRounds int `json:"interaction_rounds"`
+ Entries []EnrichedEntry `json:"entries"`
+}
+
+type EnrichedEntry struct {
+ Method string `json:"method"`
+ URL string `json:"url"`
+ RequestBody string `json:"request_body"`
+ ResponseBody string `json:"response_body"`
+ ResponseStatus int `json:"response_status"`
+ ResponseContentType string `json:"response_content_type"`
+ RequestHeaders map[string]string `json:"request_headers"`
+ Classification string `json:"classification"`
+ IsNoise bool `json:"is_noise"`
+}
diff --git a/testdata/sniff/sample-enriched.json b/testdata/sniff/sample-enriched.json
new file mode 100644
index 00000000..0b0918ca
--- /dev/null
+++ b/testdata/sniff/sample-enriched.json
@@ -0,0 +1,42 @@
+{
+ "target_url": "https://hn.algolia.com",
+ "captured_at": "2026-03-28T16:00:00Z",
+ "interaction_rounds": 2,
+ "entries": [
+ {
+ "method": "POST",
+ "url": "https://uj5wyc0l7x-dsn.algolia.net/1/indexes/Item_dev/query?x-algolia-api-key=28f0e1ec37a5e792e6845e67da5f20dd&x-algolia-application-id=UJ5WYC0L7X",
+ "request_headers": {
+ "Content-Type": "application/json"
+ },
+ "request_body": "{\"query\": \"kubernetes\", \"page\": 0, \"hitsPerPage\": 3}",
+ "response_status": 200,
+ "response_content_type": "application/json",
+ "response_body": "{\"hits\": [{\"objectID\": \"21711748\", \"title\": \"Troubleshooting K8s\", \"url\": \"https://example.com\", \"author\": \"test\", \"points\": 240, \"num_comments\": 53, \"created_at\": \"2019-12-05T12:22:28Z\", \"created_at_i\": 1575548548}, {\"objectID\": \"21711749\", \"title\": \"K8s Best Practices\", \"url\": \"https://example2.com\", \"author\": \"test2\", \"points\": 180, \"num_comments\": 30, \"created_at\": \"2020-01-10T08:00:00Z\", \"created_at_i\": 1578643200}], \"nbHits\": 4804, \"page\": 0, \"nbPages\": 334, \"hitsPerPage\": 3, \"processingTimeMS\": 16, \"query\": \"kubernetes\"}",
+ "classification": "api",
+ "is_noise": false
+ },
+ {
+ "method": "GET",
+ "url": "https://www.google-analytics.com/collect?v=1&tid=UA-12345",
+ "request_headers": {},
+ "request_body": "",
+ "response_status": 200,
+ "response_content_type": "image/gif",
+ "response_body": "",
+ "classification": "noise",
+ "is_noise": true
+ },
+ {
+ "method": "GET",
+ "url": "https://hn.algolia.com/assets/app-abc123.js",
+ "request_headers": {},
+ "request_body": "",
+ "response_status": 200,
+ "response_content_type": "application/javascript",
+ "response_body": "",
+ "classification": "noise",
+ "is_noise": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/testdata/sniff/sample.har b/testdata/sniff/sample.har
new file mode 100644
index 00000000..7deca133
--- /dev/null
+++ b/testdata/sniff/sample.har
@@ -0,0 +1,408 @@
+{
+ "log": {
+ "browser": {
+ "name": "Chrome",
+ "version": "147.0.7727.24"
+ },
+ "creator": {
+ "name": "agent-browser",
+ "version": "0.23.0"
+ },
+ "entries": [
+ {
+ "_resourceType": "Image",
+ "cache": {},
+ "request": {
+ "bodySize": 0,
+ "cookies": [],
+ "headers": [
+ {
+ "name": "Referer",
+ "value": ""
+ },
+ {
+ "name": "User-Agent",
+ "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/147.0.0.0 Safari/537.36"
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/1.1",
+ "method": "GET",
+ "queryString": [],
+ "url": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwcHgiICBoZWlnaHQ9IjIwMHB4IiAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiIGNsYXNzPSJsZHMtcm9sbGluZyIgc3R5bGU9ImJhY2tncm91bmQtaW1hZ2U6IG5vbmU7IGJhY2tncm91bmQtcG9zaXRpb246IGluaXRpYWwgaW5pdGlhbDsgYmFja2dyb3VuZC1yZXBlYXQ6IGluaXRpYWwgaW5pdGlhbDsiPjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIGZpbGw9Im5vbmUiIG5nLWF0dHItc3Ryb2tlPSJ7e2NvbmZpZy5jb2xvcn19IiBuZy1hdHRyLXN0cm9rZS13aWR0aD0ie3tjb25maWcud2lkdGh9fSIgbmctYXR0ci1yPSJ7e2NvbmZpZy5yYWRpdXN9fSIgbmctYXR0ci1zdHJva2UtZGFzaGFycmF5PSJ7e2NvbmZpZy5kYXNoYXJyYXl9fSIgc3Ryb2tlPSIjNTU1NTU1IiBzdHJva2Utd2lkdGg9IjEwIiByPSIzNSIgc3Ryb2tlLWRhc2hhcnJheT0iMTY0LjkzMzYxNDMxMzQ2NDE1IDU2Ljk3Nzg3MTQzNzgyMTM4Ij48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgY2FsY01vZGU9ImxpbmVhciIgdmFsdWVzPSIwIDUwIDUwOzM2MCA1MCA1MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49IjBzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlVHJhbnNmb3JtPjwvY2lyY2xlPjwvc3ZnPgo="
+ },
+ "response": {
+ "bodySize": 0,
+ "content": {
+ "mimeType": "image/svg+xml",
+ "size": 0
+ },
+ "cookies": [],
+ "headers": [
+ {
+ "name": "Content-Type",
+ "value": "image/svg+xml"
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/1.1",
+ "redirectURL": "",
+ "status": 200,
+ "statusText": "OK"
+ },
+ "startedDateTime": "2026-03-28T22:46:42.118844928Z",
+ "time": 0.0,
+ "timings": {
+ "receive": 0,
+ "send": 0,
+ "wait": 0
+ }
+ },
+ {
+ "_resourceType": "Document",
+ "cache": {},
+ "request": {
+ "bodySize": 0,
+ "cookies": [],
+ "headers": [
+ {
+ "name": "Accept-Language",
+ "value": "en-US,en;q=0.9"
+ },
+ {
+ "name": "Upgrade-Insecure-Requests",
+ "value": "1"
+ },
+ {
+ "name": "User-Agent",
+ "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/147.0.0.0 Safari/537.36"
+ },
+ {
+ "name": "sec-ch-ua",
+ "value": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\""
+ },
+ {
+ "name": "sec-ch-ua-mobile",
+ "value": "?0"
+ },
+ {
+ "name": "sec-ch-ua-platform",
+ "value": "\"macOS\""
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/2.0",
+ "method": "GET",
+ "queryString": [],
+ "url": "https://httpbin.org/get"
+ },
+ "response": {
+ "bodySize": 1071,
+ "content": {
+ "mimeType": "application/json",
+ "size": 1071
+ },
+ "cookies": [],
+ "headers": [
+ {
+ "name": "access-control-allow-credentials",
+ "value": "true"
+ },
+ {
+ "name": "access-control-allow-origin",
+ "value": "*"
+ },
+ {
+ "name": "content-length",
+ "value": "931"
+ },
+ {
+ "name": "content-type",
+ "value": "application/json"
+ },
+ {
+ "name": "date",
+ "value": "Sat, 28 Mar 2026 22:47:08 GMT"
+ },
+ {
+ "name": "server",
+ "value": "gunicorn/19.9.0"
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/2.0",
+ "redirectURL": "",
+ "status": 200,
+ "statusText": ""
+ },
+ "startedDateTime": "2026-03-28T22:47:08.268548096Z",
+ "time": 304.0609999927804,
+ "timings": {
+ "blocked": 0.115,
+ "receive": 0.4729999927803874,
+ "send": 0.09899999999999999,
+ "wait": 303.374
+ }
+ },
+ {
+ "_resourceType": "Other",
+ "cache": {},
+ "request": {
+ "bodySize": 0,
+ "cookies": [],
+ "headers": [
+ {
+ "name": "Accept-Language",
+ "value": "en-US,en;q=0.9"
+ },
+ {
+ "name": "Referer",
+ "value": "https://httpbin.org/get"
+ },
+ {
+ "name": "User-Agent",
+ "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/147.0.0.0 Safari/537.36"
+ },
+ {
+ "name": "sec-ch-ua",
+ "value": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\""
+ },
+ {
+ "name": "sec-ch-ua-mobile",
+ "value": "?0"
+ },
+ {
+ "name": "sec-ch-ua-platform",
+ "value": "\"macOS\""
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/2.0",
+ "method": "GET",
+ "queryString": [],
+ "url": "https://httpbin.org/favicon.ico"
+ },
+ "response": {
+ "bodySize": 369,
+ "content": {
+ "mimeType": "text/html",
+ "size": 369
+ },
+ "cookies": [],
+ "headers": [
+ {
+ "name": "access-control-allow-credentials",
+ "value": "true"
+ },
+ {
+ "name": "access-control-allow-origin",
+ "value": "*"
+ },
+ {
+ "name": "content-length",
+ "value": "233"
+ },
+ {
+ "name": "content-type",
+ "value": "text/html"
+ },
+ {
+ "name": "date",
+ "value": "Sat, 28 Mar 2026 22:47:08 GMT"
+ },
+ {
+ "name": "server",
+ "value": "gunicorn/19.9.0"
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/2.0",
+ "redirectURL": "",
+ "status": 404,
+ "statusText": ""
+ },
+ "startedDateTime": "2026-03-28T22:47:08.59478912Z",
+ "time": 111.62999998887442,
+ "timings": {
+ "blocked": 0.222,
+ "receive": 0.3539999888744205,
+ "send": 0.13699999999999998,
+ "wait": 110.917
+ }
+ },
+ {
+ "_resourceType": "Document",
+ "cache": {},
+ "request": {
+ "bodySize": 0,
+ "cookies": [],
+ "headers": [
+ {
+ "name": "Accept-Language",
+ "value": "en-US,en;q=0.9"
+ },
+ {
+ "name": "Upgrade-Insecure-Requests",
+ "value": "1"
+ },
+ {
+ "name": "User-Agent",
+ "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/147.0.0.0 Safari/537.36"
+ },
+ {
+ "name": "sec-ch-ua",
+ "value": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\""
+ },
+ {
+ "name": "sec-ch-ua-mobile",
+ "value": "?0"
+ },
+ {
+ "name": "sec-ch-ua-platform",
+ "value": "\"macOS\""
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/2.0",
+ "method": "GET",
+ "queryString": [],
+ "url": "https://httpbin.org/headers"
+ },
+ "response": {
+ "bodySize": 988,
+ "content": {
+ "mimeType": "application/json",
+ "size": 988
+ },
+ "cookies": [],
+ "headers": [
+ {
+ "name": "access-control-allow-credentials",
+ "value": "true"
+ },
+ {
+ "name": "access-control-allow-origin",
+ "value": "*"
+ },
+ {
+ "name": "content-length",
+ "value": "848"
+ },
+ {
+ "name": "content-type",
+ "value": "application/json"
+ },
+ {
+ "name": "date",
+ "value": "Sat, 28 Mar 2026 22:47:11 GMT"
+ },
+ {
+ "name": "server",
+ "value": "gunicorn/19.9.0"
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/2.0",
+ "redirectURL": "",
+ "status": 200,
+ "statusText": ""
+ },
+ "startedDateTime": "2026-03-28T22:47:10.793214976Z",
+ "time": 326.80599998972565,
+ "timings": {
+ "blocked": 0.19,
+ "receive": 1.8179999897256494,
+ "send": 0.17099999999999999,
+ "wait": 324.627
+ }
+ },
+ {
+ "_resourceType": "Document",
+ "cache": {},
+ "request": {
+ "bodySize": 0,
+ "cookies": [],
+ "headers": [
+ {
+ "name": "Accept-Language",
+ "value": "en-US,en;q=0.9"
+ },
+ {
+ "name": "Upgrade-Insecure-Requests",
+ "value": "1"
+ },
+ {
+ "name": "User-Agent",
+ "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/147.0.0.0 Safari/537.36"
+ },
+ {
+ "name": "sec-ch-ua",
+ "value": "\"Chromium\";v=\"147\", \"Not.A/Brand\";v=\"8\""
+ },
+ {
+ "name": "sec-ch-ua-mobile",
+ "value": "?0"
+ },
+ {
+ "name": "sec-ch-ua-platform",
+ "value": "\"macOS\""
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/2.0",
+ "method": "GET",
+ "queryString": [],
+ "url": "https://httpbin.org/ip"
+ },
+ "response": {
+ "bodySize": 172,
+ "content": {
+ "mimeType": "application/json",
+ "size": 172
+ },
+ "cookies": [],
+ "headers": [
+ {
+ "name": "access-control-allow-credentials",
+ "value": "true"
+ },
+ {
+ "name": "access-control-allow-origin",
+ "value": "*"
+ },
+ {
+ "name": "content-length",
+ "value": "33"
+ },
+ {
+ "name": "content-type",
+ "value": "application/json"
+ },
+ {
+ "name": "date",
+ "value": "Sat, 28 Mar 2026 22:47:12 GMT"
+ },
+ {
+ "name": "server",
+ "value": "gunicorn/19.9.0"
+ }
+ ],
+ "headersSize": -1,
+ "httpVersion": "HTTP/2.0",
+ "redirectURL": "",
+ "status": 200,
+ "statusText": ""
+ },
+ "startedDateTime": "2026-03-28T22:47:12.3217152Z",
+ "time": 145.02099997888504,
+ "timings": {
+ "blocked": 0.201,
+ "receive": 0.3029999788850546,
+ "send": 0.16099999999999998,
+ "wait": 144.356
+ }
+ }
+ ],
+ "version": "1.2"
+ }
+}
\ No newline at end of file
← c779648a feat(ci): automated releases, linting, and commit convention
·
back to Cli Printing Press
·
feat(websniff): add API endpoint classifier with analytics b 07f158f6 →