diff --git a/internal/api/event.go b/internal/api/event.go index e4dfe88..7cb828f 100644 --- a/internal/api/event.go +++ b/internal/api/event.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "slices" "notashelf.dev/watchdog/internal/limits" ) @@ -46,13 +47,7 @@ func (e *Event) Validate(allowedDomains []string) error { } // Check if domain is in allowed list - allowed := false - for _, domain := range allowedDomains { - if e.Domain == domain { - allowed = true - break - } - } + allowed := slices.Contains(allowedDomains, e.Domain) if !allowed { return fmt.Errorf("domain not allowed") } @@ -76,3 +71,34 @@ func (e *Event) Validate(allowedDomains []string) error { return nil } + +// ValidateWithMap checks if the event is valid using a domain map (O(1) lookup) +func (e *Event) ValidateWithMap(allowedDomains map[string]bool) error { + if e.Domain == "" { + return fmt.Errorf("domain required") + } + + // Check if domain is in allowed map (O(1) instead of O(n)) + if !allowedDomains[e.Domain] { + return fmt.Errorf("domain not allowed") + } + + if e.Path == "" { + return fmt.Errorf("path required") + } + + if len(e.Path) > limits.MaxPathLen { + return fmt.Errorf("path too long") + } + + if len(e.Referrer) > limits.MaxRefLen { + return fmt.Errorf("referrer too long") + } + + // Validate screen width is in reasonable range + if e.Width < 0 || e.Width > limits.MaxWidth { + return fmt.Errorf("invalid width") + } + + return nil +} diff --git a/internal/api/event_test.go b/internal/api/event_test.go index f9d0dcf..0a075fe 100644 --- a/internal/api/event_test.go +++ b/internal/api/event_test.go @@ -230,3 +230,37 @@ func TestValidateEvent(t *testing.T) { }) } } + +func BenchmarkValidate_SliceLookup(b *testing.B) { + // Simulate multi-site with 50 domains + domains := make([]string, 50) + for i := range 50 { + domains[i] = strings.Repeat("site", i) + ".com" + } + + event := Event{ + Domain: domains[49], // Worst case - last in list + Path: "/test", + } + + for b.Loop() { + _ = event.Validate(domains) + } +} + +func BenchmarkValidate_MapLookup(b *testing.B) { + // Simulate multi-site with 50 domains + domainMap := make(map[string]bool, 50) + for i := range 50 { + domainMap[strings.Repeat("site", i)+".com"] = true + } + + event := Event{ + Domain: strings.Repeat("site", 49) + ".com", // any position + Path: "/test", + } + + for b.Loop() { + _ = event.ValidateWithMap(domainMap) + } +} diff --git a/internal/api/handler.go b/internal/api/handler.go index 049b2e7..bed0abf 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -15,6 +15,7 @@ import ( // Handles incoming analytics events type IngestionHandler struct { cfg *config.Config + domainMap map[string]bool // O(1) domain validation pathNorm *normalize.PathNormalizer pathRegistry *aggregate.PathRegistry refRegistry *normalize.ReferrerRegistry @@ -40,8 +41,15 @@ func NewIngestionHandler( ) } + // Build domain map for O(1) validation + domainMap := make(map[string]bool, len(cfg.Site.Domains)) + for _, domain := range cfg.Site.Domains { + domainMap[domain] = true + } + return &IngestionHandler{ cfg: cfg, + domainMap: domainMap, pathNorm: pathNorm, pathRegistry: pathRegistry, refRegistry: refRegistry, @@ -95,8 +103,8 @@ func (h *IngestionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // Validate event - if err := event.Validate(h.cfg.Site.Domains); err != nil { + // Validate event via map lookup (also O(1)) + if err := event.ValidateWithMap(h.domainMap); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return }