agent-orchestrator/backend/internal/adapters/telemetry/posthog_test.go

142 lines
4.3 KiB
Go

package telemetry
import (
"context"
"encoding/json"
"net/http"
"testing"
"time"
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)
func TestPostHogSinkCapturesEvent(t *testing.T) {
requests := make(chan map[string]any, 1)
sink, err := NewPostHogSink(t.TempDir(), "phc_test", "https://us.i.posthog.com", roundTripClient(func(req *http.Request) (*http.Response, error) {
defer req.Body.Close()
var body map[string]any
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, err
}
requests <- body
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: http.NoBody,
}, nil
}), nil)
if err != nil {
t.Fatalf("NewPostHogSink: %v", err)
}
projectID := domain.ProjectID("proj-1")
sessionID := domain.SessionID("sess-1")
sink.Emit(context.Background(), ports.TelemetryEvent{
Name: "ao.session.spawned",
Source: "session_service",
OccurredAt: time.Unix(1700000000, 0).UTC(),
Level: ports.TelemetryLevelInfo,
ProjectID: &projectID,
SessionID: &sessionID,
RequestID: "req-1",
Payload: map[string]any{
"kind": "worker",
},
})
if err := sink.Close(context.Background()); err != nil {
t.Fatalf("Close: %v", err)
}
select {
case req := <-requests:
if got := req["event"]; got != "ao.session.spawned" {
t.Fatalf("event = %#v, want ao.session.spawned", got)
}
props, ok := req["properties"].(map[string]any)
if !ok {
t.Fatalf("properties type = %T, want map[string]any", req["properties"])
}
if props["kind"] != "worker" {
t.Fatalf("properties.kind = %#v, want worker", props["kind"])
}
if props["project_id_hash"] == "" || props["session_id_hash"] == "" {
t.Fatalf("hashed ids missing from properties: %#v", props)
}
case <-time.After(2 * time.Second):
t.Fatal("PostHog sink did not send request")
}
}
func TestPostHogSinkSanitizesPayloads(t *testing.T) {
requests := make(chan map[string]any, 1)
sink, err := NewPostHogSink(t.TempDir(), "phc_test", "https://us.i.posthog.com", roundTripClient(func(req *http.Request) (*http.Response, error) {
defer req.Body.Close()
var body map[string]any
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return nil, err
}
requests <- body
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: http.NoBody,
}, nil
}), nil)
if err != nil {
t.Fatalf("NewPostHogSink: %v", err)
}
sink.Emit(context.Background(), ports.TelemetryEvent{
Name: "ao.daemon.panic",
Source: "http",
OccurredAt: time.Unix(1700000000, 0).UTC(),
Level: ports.TelemetryLevelError,
Payload: map[string]any{
"component": "httpd",
"operation": "http_request_panic",
"method": http.MethodGet,
"path": "/api/v1/sessions/demo",
"panic_kind": "error",
"fingerprint": "abc123",
"stack_fingerprint": "def456",
"panic": "open /Users/name/private: no such file",
"stack": "stack trace with local path",
},
})
if err := sink.Close(context.Background()); err != nil {
t.Fatalf("Close: %v", err)
}
select {
case req := <-requests:
props, ok := req["properties"].(map[string]any)
if !ok {
t.Fatalf("properties type = %T, want map[string]any", req["properties"])
}
if props["component"] != "httpd" || props["operation"] != "http_request_panic" {
t.Fatalf("sanitized properties = %#v, want allowlisted metadata", props)
}
if props["method"] != http.MethodGet || props["path"] != "/api/v1/sessions/demo" || props["panic_kind"] != "error" {
t.Fatalf("sanitized properties = %#v, want allowlisted fields", props)
}
if props["fingerprint"] != "abc123" || props["stack_fingerprint"] != "def456" {
t.Fatalf("sanitized properties = %#v, want exported fingerprints", props)
}
if _, ok := props["panic"]; ok {
t.Fatalf("panic property should be dropped: %#v", props)
}
if _, ok := props["stack"]; ok {
t.Fatalf("stack property should be dropped: %#v", props)
}
case <-time.After(2 * time.Second):
t.Fatal("PostHog sink did not send request")
}
}
type roundTripClient func(*http.Request) (*http.Response, error)
func (f roundTripClient) Do(req *http.Request) (*http.Response, error) { return f(req) }
var _ postHogClient = roundTripClient(nil)