TestHandler_SDKv4_Envelope tests Sentry PHP SDK v4. v4 ALWAYS sends envelope to /api/{project}/envelope. CRITICAL DIFFERENCE: v4 puts event_id ONLY in envelope header, NOT in item payload. The handler must inject event_id into the payload so the frontend can render the event.
(t *testing.T)
| 383 | // CRITICAL DIFFERENCE: v4 puts event_id ONLY in envelope header, NOT in item payload. |
| 384 | // The handler must inject event_id into the payload so the frontend can render the event. |
| 385 | func TestHandler_SDKv4_Envelope(t *testing.T) { |
| 386 | h := &handler{} |
| 387 | |
| 388 | // Real payload structure from sentry/sentry 4.23.1. |
| 389 | // Note: item payload does NOT contain event_id — only the envelope header does. |
| 390 | envelope := `{"sent_at":"2026-03-31T12:31:55Z","dsn":"http://test@localhost:8000/1","sdk":{"name":"sentry.php","version":"4.23.1","packages":[{"name":"composer:sentry/sentry","version":"4.23.1"}]},"event_id":"cf37ef540db04de7ab27dcd8df08034d"} |
| 391 | {"type":"event","content_type":"application/json"} |
| 392 | {"timestamp":1774960315.279867,"platform":"php","sdk":{"name":"sentry.php","version":"4.23.1","packages":[{"name":"composer:sentry/sentry","version":"4.23.1"}]},"level":"error","server_name":"web-01","release":"1.0.0","environment":"production","exception":{"values":[{"type":"RuntimeException","value":"Something went wrong"}]}}` |
| 393 | |
| 394 | r := httptest.NewRequest("POST", "/api/1/envelope/", strings.NewReader(envelope)) |
| 395 | r.Header.Set("Content-Type", "application/x-sentry-envelope") |
| 396 | r.Header.Set("X-Sentry-Auth", "Sentry sentry_version=7, sentry_client=sentry.php/4.23.1, sentry_key=test") |
| 397 | |
| 398 | inc, err := h.Handle(r) |
| 399 | if err != nil { |
| 400 | t.Fatalf("Handle() error: %v", err) |
| 401 | } |
| 402 | if inc == nil { |
| 403 | t.Fatal("Handle() returned nil, want non-nil incoming event") |
| 404 | } |
| 405 | if inc.Type != "sentry" { |
| 406 | t.Errorf("Type = %q, want %q", inc.Type, "sentry") |
| 407 | } |
| 408 | if inc.UUID != "cf37ef540db04de7ab27dcd8df08034d" { |
| 409 | t.Errorf("UUID = %q, want %q", inc.UUID, "cf37ef540db04de7ab27dcd8df08034d") |
| 410 | } |
| 411 | |
| 412 | // CRITICAL: Payload MUST contain event_id even though v4 SDK doesn't include it. |
| 413 | // The handler should inject event_id from the envelope header. |
| 414 | var parsed map[string]any |
| 415 | if err := json.Unmarshal(inc.Payload, &parsed); err != nil { |
| 416 | t.Fatalf("failed to parse payload: %v", err) |
| 417 | } |
| 418 | if parsed["event_id"] != "cf37ef540db04de7ab27dcd8df08034d" { |
| 419 | t.Errorf("payload event_id = %v, want %q (should be injected from envelope header)", parsed["event_id"], "cf37ef540db04de7ab27dcd8df08034d") |
| 420 | } |
| 421 | if parsed["level"] != "error" { |
| 422 | t.Errorf("payload level = %v, want %q", parsed["level"], "error") |
| 423 | } |
| 424 | if parsed["environment"] != "production" { |
| 425 | t.Errorf("payload environment = %v, want %q", parsed["environment"], "production") |
| 426 | } |
| 427 | |
| 428 | // Exception data must be preserved. |
| 429 | exc, ok := parsed["exception"].(map[string]any) |
| 430 | if !ok { |
| 431 | t.Fatal("payload missing exception") |
| 432 | } |
| 433 | vals, ok := exc["values"].([]any) |
| 434 | if !ok || len(vals) == 0 { |
| 435 | t.Fatal("payload exception.values is empty") |
| 436 | } |
| 437 | first := vals[0].(map[string]any) |
| 438 | if first["type"] != "RuntimeException" { |
| 439 | t.Errorf("exception type = %v, want %q", first["type"], "RuntimeException") |
| 440 | } |
| 441 | } |
| 442 |