update
Build and Publish / Metadata (push) Successful in 8s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 2m7s

This commit is contained in:
mats committed 2026-09-21 19:38:09 +02:00
1 parent 0ed2935f04
commit c8cc0fb28a
11 files changed
+602 -21

No files matched your search

+2
View File
@@ -18,6 +18,7 @@ import (
"git.tornberg.me/go-otel/internal/prometheus"
"git.tornberg.me/go-otel/internal/sourcemaps"
"git.tornberg.me/go-otel/internal/storage"
"git.tornberg.me/go-otel/internal/todos"
"git.tornberg.me/go-otel/internal/uaclass"
"git.tornberg.me/go-otel/internal/websocket"
@@ -43,6 +44,7 @@ type Collector struct {
ingestLimiter *ipRateLimiter
ingestKey string
todoStore todos.Store
sourceMapKey string
notifyIssuesFn func()
+118
View File
@@ -0,0 +1,118 @@
package collector
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"time"
"git.tornberg.me/go-otel/internal/todos"
)
const (
maxTodoBody = 16 << 10
maxTodoText = 4000
maxTodoMeta = 500
)
// SetTodoStore enables the todo endpoints.
func (c *Collector) SetTodoStore(s todos.Store) { c.todoStore = s }
func clipTo(s string, n int) string {
s = strings.TrimSpace(s)
if len(s) > n {
return s[:n]
}
return s
}
type todoPayload struct {
Project string `json:"project"`
Text string `json:"text"`
Component string `json:"component"`
Path string `json:"path"`
Hash string `json:"hash"`
URL string `json:"url"`
UserAgent string `json:"ua"`
Session string `json:"session"`
}
// HandleTodoCreate ingests a todo posted by otel-todo.js. It is guarded by
// the same ingest key and rate limit as the other RUM endpoints.
func (c *Collector) HandleTodoCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if c.todoStore == nil {
http.Error(w, "todos not enabled", http.StatusServiceUnavailable)
return
}
if !c.checkIngest(w, r) {
return
}
var p todoPayload
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxTodoBody)).Decode(&p); err != nil {
http.Error(w, "invalid todo payload", http.StatusBadRequest)
return
}
text := clipTo(p.Text, maxTodoText)
if text == "" {
http.Error(w, "text is required", http.StatusBadRequest)
return
}
project := clipTo(p.Project, maxTodoMeta)
if project == "" {
project = "frontend"
}
id := make([]byte, 8)
_, _ = rand.Read(id)
t := todos.Todo{
ID: hex.EncodeToString(id),
Project: project,
Text: text,
Component: clipTo(p.Component, maxTodoMeta),
Path: clipTo(p.Path, maxTodoMeta),
Hash: clipTo(p.Hash, maxTodoMeta),
URL: clipTo(p.URL, maxTodoMeta),
UserAgent: clipTo(p.UserAgent, maxTodoMeta),
Session: clipTo(p.Session, maxTodoMeta),
CreatedAt: time.Now().UTC(),
}
if err := c.todoStore.Add(r.Context(), t); err != nil {
log.Printf("failed to store todo: %v", err)
http.Error(w, "failed to store todo", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(t)
}
// HandleTodoList returns todos, newest first. ?project= filters by project
// (omit for all); ?limit= defaults to 100, max 1000. Registered behind auth.
func (c *Collector) HandleTodoList(w http.ResponseWriter, r *http.Request) {
if c.todoStore == nil {
http.Error(w, "todos not enabled", http.StatusServiceUnavailable)
return
}
limit := 100
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 {
limit = min(v, 1000)
}
items, err := c.todoStore.List(r.Context(), r.URL.Query().Get("project"), limit)
if err != nil {
log.Printf("failed to list todos: %v", err)
http.Error(w, "failed to list todos", http.StatusInternalServerError)
return
}
if items == nil {
items = []todos.Todo{}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(items)
}
+45
View File
@@ -0,0 +1,45 @@
package collector
import (
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"git.tornberg.me/go-otel/internal/todos"
)
func TestTodoCreateAndList(t *testing.T) {
coll := New(nil, 10, nil)
st, err := todos.NewFileStore(filepath.Join(t.TempDir(), "todos.jsonl"))
if err != nil {
t.Fatal(err)
}
coll.SetTodoStore(st)
post := func(body string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
coll.HandleTodoCreate(w, httptest.NewRequest(http.MethodPost, "/v1/todos", strings.NewReader(body)))
return w
}
if w := post(`{"project":"a","text":"fix cart","component":"Cart","path":"/cart","hash":"#x"}`); w.Code != http.StatusCreated {
t.Fatalf("create: %d %s", w.Code, w.Body)
}
post(`{"project":"b","text":"other"}`)
if w := post(`{"project":"a","text":" "}`); w.Code != http.StatusBadRequest {
t.Fatalf("empty text: %d", w.Code)
}
list := func(q string) string {
w := httptest.NewRecorder()
coll.HandleTodoList(w, httptest.NewRequest(http.MethodGet, "/api/todos"+q, nil))
return w.Body.String()
}
if got := list("?project=a"); !strings.Contains(got, "fix cart") || strings.Contains(got, "other") || !strings.Contains(got, `"hash":"#x"`) {
t.Fatalf("project filter: %s", got)
}
if got := list(""); !strings.Contains(got, "fix cart") || !strings.Contains(got, "other") {
t.Fatalf("all: %s", got)
}
}