| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 522d554 commit ef7ec3b
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,185 @@ | |||
| 1 | + --- | ||
| 2 | + name: sentry-sdk-testing | ||
| 3 | + description: Test Buggregator's Sentry module compatibility with different Sentry PHP SDK versions. Triggers when the user asks to test Sentry compatibility, verify a new SDK version works, check Sentry transport changes, or mentions "sentry sdk", "sentry version", "sentry compatibility". Also triggers on requests to prepare for a new Sentry SDK release. | ||
| 4 | + --- | ||
| 5 | + | ||
| 6 | + # Sentry SDK Compatibility Testing | ||
| 7 | + | ||
| 8 | + ## Role | ||
| 9 | + | ||
| 10 | + You are a protocol compatibility engineer. Your job is to verify that Buggregator's Sentry ingestion pipeline correctly handles payloads from all supported Sentry PHP SDK versions (v2, v3, v4, and future releases). | ||
| 11 | + | ||
| 12 | + ## When to Use | ||
| 13 | + | ||
| 14 | + - A new major/minor Sentry PHP SDK version is released | ||
| 15 | + - A user reports that events from a specific SDK version are not appearing in the UI | ||
| 16 | + - Before a Buggregator release, to verify no regressions in Sentry support | ||
| 17 | + | ||
| 18 | + ## Workflow | ||
| 19 | + | ||
| 20 | + ### Phase 1 — Install SDK Versions | ||
| 21 | + | ||
| 22 | + Create isolated temporary directories for each SDK version and install them via Composer: | ||
| 23 | + | ||
| 24 | + ```bash | ||
| 25 | + # For each version you need to test: | ||
| 26 | + mkdir -p /tmp/sentry-vN | ||
| 27 | + cd /tmp/sentry-vN | ||
| 28 | + composer require sentry/sentry:"^N.0" --no-interaction | ||
| 29 | + | ||
| 30 | + # If PHP version constraint fails: | ||
| 31 | + composer require sentry/sentry:"^N.0" --no-interaction --ignore-platform-reqs | ||
| 32 | + | ||
| 33 | + # Verify installed version: | ||
| 34 | + cd /tmp/sentry-vN && composer show sentry/sentry | head -5 | ||
| 35 | + ``` | ||
| 36 | + | ||
| 37 | + ### Phase 2 — Analyze Transport Layer | ||
| 38 | + | ||
| 39 | + For each SDK version, examine these key files: | ||
| 40 | + | ||
| 41 | + | What to Find | Where to Look | | ||
| 42 | + |---|---| | ||
| 43 | + | Endpoint paths | `src/Dsn.php` — `getStoreApiEndpointUrl()`, `getEnvelopeApiEndpointUrl()` | | ||
| 44 | + | Transport logic | `src/Transport/HttpTransport.php` — `send()` method, which endpoint is chosen | | ||
| 45 | + | Headers | `src/HttpClient/Authentication/`, `src/Util/Http.php` | | ||
| 46 | + | Serialization | `src/Serializer/PayloadSerializer.php` (v2/v3) or `src/Serializer/EnvelopItems/*.php` (v4+) | | ||
| 47 | + | Compression | `src/HttpClient/Plugin/GzipEncoderPlugin.php` or `src/HttpClient/HttpClient.php` | | ||
| 48 | + | Event structure | `src/Event.php` — `toArray()` method (v2/v3) or `EnvelopItems/EventItem.php` (v4+) | | ||
| 49 | + | ||
| 50 | + Document findings in a comparison table covering: | ||
| 51 | + 1. **Endpoint** — `/store/` vs `/envelope/` vs both | ||
| 52 | + 2. **Content-Type** — `application/json` vs `application/x-sentry-envelope` | ||
| 53 | + 3. **Body format** — plain JSON vs envelope (header + items) | ||
| 54 | + 4. **`event_id` location** — in payload, in envelope header, or both | ||
| 55 | + 5. **Timestamp format** — ISO 8601 string vs numeric epoch float | ||
| 56 | + 6. **Message format** — plain string vs `{"message","params","formatted"}` object | ||
| 57 | + 7. **New/removed fields** in event payload | ||
| 58 | + 8. **New envelope item types** (e.g., `log`, `trace_metric`, `client_report`) | ||
| 59 | + 9. **Compression** — gzip, deflate, or none by default | ||
| 60 | + | ||
| 61 | + ### Phase 3 — Generate Real Payloads | ||
| 62 | + | ||
| 63 | + Write a PHP script in each SDK directory to produce real serialized output: | ||
| 64 | + | ||
| 65 | + ```php | ||
| 66 | + <?php | ||
| 67 | + require_once 'vendor/autoload.php'; | ||
| 68 | + | ||
| 69 | + use Sentry\Options; | ||
| 70 | + use Sentry\Serializer\PayloadSerializer; | ||
| 71 | + use Sentry\Event; | ||
| 72 | + use Sentry\Severity; | ||
| 73 | + use Sentry\ExceptionDataBag; | ||
| 74 | + | ||
| 75 | + $options = new Options([ | ||
| 76 | + 'dsn' => 'http://test@localhost:8000/1', | ||
| 77 | + 'http_compression' => false, | ||
| 78 | + // For v3 with tracing: 'traces_sample_rate' => 1.0, | ||
| 79 | + ]); | ||
| 80 | + | ||
| 81 | + $serializer = new PayloadSerializer($options); | ||
| 82 | + | ||
| 83 | + $event = Event::createEvent(); // v3/v4 | ||
| 84 | + // $event = new Event(); // v2 | ||
| 85 | + $event->setLevel(Severity::error()); | ||
| 86 | + $event->setEnvironment('production'); | ||
| 87 | + $event->setRelease('1.0.0'); | ||
| 88 | + $event->setServerName('web-01'); | ||
| 89 | + | ||
| 90 | + $exception = new ExceptionDataBag(new \RuntimeException('Test error')); | ||
| 91 | + $event->setExceptions([$exception]); | ||
| 92 | + | ||
| 93 | + echo $serializer->serialize($event); | ||
| 94 | + ``` | ||
| 95 | + | ||
| 96 | + Save generated payloads for use in Go tests. | ||
| 97 | + | ||
| 98 | + ### Phase 4 — Write Go Tests | ||
| 99 | + | ||
| 100 | + Add tests in `modules/sentry/handler_test.go` using real payloads from Phase 3. | ||
| 101 | + | ||
| 102 | + Each test must verify: | ||
| 103 | + | ||
| 104 | + 1. **`Handle()` returns non-nil** — the event is recognized | ||
| 105 | + 2. **`inc.UUID`** — matches the expected event_id | ||
| 106 | + 3. **`inc.Type == "sentry"`** — correct event type | ||
| 107 | + 4. **`inc.Project`** — extracted from URL path | ||
| 108 | + 5. **`event_id` in payload** — present in `inc.Payload` JSON (critical for frontend rendering) | ||
| 109 | + 6. **Exception/message data preserved** — key fields survive parsing | ||
| 110 | + | ||
| 111 | + For structured storage tests (with DB), also verify: | ||
| 112 | + - Row exists in `sentry_error_events` | ||
| 113 | + - Exceptions stored in `sentry_exceptions` | ||
| 114 | + - Breadcrumbs stored in `sentry_breadcrumbs` | ||
| 115 | + | ||
| 116 | + Test naming convention: | ||
| 117 | + ``` | ||
| 118 | + TestHandler_SDKvN_Format (e.g., TestHandler_SDKv4_Envelope) | ||
| 119 | + TestHandler_SDKvN_Format_WithDB (e.g., TestHandler_SDKv2_PlainJSON_WithDB) | ||
| 120 | + ``` | ||
| 121 | + | ||
| 122 | + ### Phase 5 — Fix Compatibility Issues | ||
| 123 | + | ||
| 124 | + Common patterns that break between SDK versions: | ||
| 125 | + | ||
| 126 | + | Problem | Symptom | Fix Pattern | | ||
| 127 | + |---|---|---| | ||
| 128 | + | `event_id` missing from item payload | Events invisible in UI | Inject from envelope header via `injectEventID()` | | ||
| 129 | + | Timestamp format change (string vs float) | `json.Unmarshal` fails silently | Use `FlexibleTS` type that accepts both | | ||
| 130 | + | Field type change (string vs object) | Structured storage skipped | Use flexible unmarshaler (e.g., `FlexibleMessage`) | | ||
| 131 | + | New envelope item type | Unknown items silently dropped | Add case to `handleEnvelope()` switch | | ||
| 132 | + | New fields in payload | Data loss in structured tables | Add fields to Go structs, update store functions | | ||
| 133 | + | Endpoint change | Handler doesn't match request | Update `Match()` path checks | | ||
| 134 | + | ||
| 135 | + ### Phase 6 — Verify All Tests Pass | ||
| 136 | + | ||
| 137 | + ```bash | ||
| 138 | + # Sentry module tests only: | ||
| 139 | + cd /home/butschster/repos/buggregator/server | ||
| 140 | + go test ./modules/sentry/ -v | ||
| 141 | + | ||
| 142 | + # Full project: | ||
| 143 | + go test ./... | ||
| 144 | + ``` | ||
| 145 | + | ||
| 146 | + ## Known Version Differences (Reference) | ||
| 147 | + | ||
| 148 | + ### SDK v2 (2.x) | ||
| 149 | + - Plain JSON only, `/api/{id}/store/` | ||
| 150 | + - Timestamp: ISO 8601 string | ||
| 151 | + - No envelope support | ||
| 152 | + - No tracing/spans | ||
| 153 | + | ||
| 154 | + ### SDK v3 (3.x) | ||
| 155 | + - Plain JSON to `/store/` (no tracing) or Envelope to `/envelope/` (with tracing) | ||
| 156 | + - Timestamp: float epoch | ||
| 157 | + - `event_id` present in both envelope header and item payload | ||
| 158 | + | ||
| 159 | + ### SDK v4 (4.x) | ||
| 160 | + - Always Envelope to `/api/{id}/envelope/` | ||
| 161 | + - Timestamp: float epoch | ||
| 162 | + - `event_id` ONLY in envelope header (not in item payload) | ||
| 163 | + - New item types: `log`, `trace_metric`, `client_report` | ||
| 164 | + - SDK payload includes `packages` array | ||
| 165 | + - Span `origin` field added | ||
| 166 | + | ||
| 167 | + ## Key Files in Buggregator | ||
| 168 | + | ||
| 169 | + | File | Responsibility | | ||
| 170 | + |---|---| | ||
| 171 | + | `modules/sentry/handler.go` | Request matching, decompression, JSON vs envelope routing, event_id injection | | ||
| 172 | + | `modules/sentry/envelope.go` | Envelope parsing (header + item pairs) | | ||
| 173 | + | `modules/sentry/types.go` | `ErrorEvent`, `FlexibleTS`, `FlexibleMessage`, `Transaction`, `RawSpan`, etc. | | ||
| 174 | + | `modules/sentry/store_error.go` | Structured storage for error events, exceptions, breadcrumbs | | ||
| 175 | + | `modules/sentry/store_transaction.go` | Structured storage for transactions and spans | | ||
| 176 | + | `modules/sentry/preview.go` | Preview mapper for WebSocket broadcast | | ||
| 177 | + | `modules/sentry/handler_test.go` | SDK compatibility tests | | ||
| 178 | + | ||
| 179 | + ## Cleanup | ||
| 180 | + | ||
| 181 | + After testing, remove temporary directories: | ||
| 182 | + | ||
| 183 | + ```bash | ||
| 184 | + rm -rf /tmp/sentry-v2 /tmp/sentry-v3 /tmp/sentry-v4 /tmp/sentry-v5 | ||
| 185 | + ``` | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -113,7 +113,7 @@ func (h *handler) handleEnvelope(body []byte, project string) (*event.Incoming, | |||
| 113 | 113 | for _, item := range items { | |
| 114 | 114 | switch item.Type { | |
| 115 | 115 | case "event": | |
| 116 | - uuid, payload := h.handleEventItem(item, project) | ||
| 116 | + uuid, payload := h.handleEventItem(item, envHeader.EventID, project) | ||
| 117 | 117 | if uuid != "" { | |
| 118 | 118 | primaryUUID = uuid | |
| 119 | 119 | } | |
@@ -177,22 +177,33 @@ func (h *handler) handleEnvelope(body []byte, project string) (*event.Incoming, | |||
| 177 | 177 | } | |
| 178 | 178 | ||
| 179 | 179 | // handleEventItem processes an "event" envelope item (error/message event). | |
| 180 | - func (h *handler) handleEventItem(item EnvelopeItem, project string) (string, json.RawMessage) { | ||
| 180 | + // envelopeEventID is the event_id from the envelope header — Sentry SDK v4+ | ||
| 181 | + // omits event_id from the item payload, so we inject it when missing. | ||
| 182 | + func (h *handler) handleEventItem(item EnvelopeItem, envelopeEventID string, project string) (string, json.RawMessage) { | ||
| 181 | 183 | var ev ErrorEvent | |
| 182 | 184 | if err := json.Unmarshal(item.Payload, &ev); err != nil { | |
| 183 | 185 | slog.Warn("sentry: failed to parse error event", "err", err) | |
| 184 | 186 | return "", item.Payload | |
| 185 | 187 | } | |
| 186 | 188 | ||
| 189 | + // Sentry SDK v4+ sends event_id only in the envelope header, not in | ||
| 190 | + // the item payload. Inject it so downstream consumers (frontend, API) | ||
| 191 | + // always see it in the payload. | ||
| 187 | 192 | uuid := ev.EventID | |
| 193 | + payload := item.Payload | ||
| 194 | + if uuid == "" && envelopeEventID != "" { | ||
| 195 | + uuid = envelopeEventID | ||
| 196 | + ev.EventID = envelopeEventID | ||
| 197 | + payload = injectEventID(payload, envelopeEventID) | ||
| 198 | + } | ||
| 188 | 199 | ||
| 189 | 200 | if h.db != nil { | |
| 190 | - if _, err := storeErrorEvent(h.db, &ev, item.Payload, project); err != nil { | ||
| 201 | + if _, err := storeErrorEvent(h.db, &ev, payload, project); err != nil { | ||
| 191 | 202 | slog.Warn("sentry: failed to store structured error event", "err", err) | |
| 192 | 203 | } | |
| 193 | 204 | } | |
| 194 | 205 | ||
| 195 | - return uuid, item.Payload | ||
| 206 | + return uuid, payload | ||
| 196 | 207 | } | |
| 197 | 208 | ||
| 198 | 209 | // handleTransactionItem processes a "transaction" envelope item. | |
@@ -261,6 +272,21 @@ func (h *handler) handleLogItem(item EnvelopeItem) { | |||
| 261 | 272 | } | |
| 262 | 273 | } | |
| 263 | 274 | ||
| 275 | + // injectEventID adds "event_id" to a JSON payload that lacks it. | ||
| 276 | + // Used to normalize Sentry SDK v4+ payloads which omit event_id from item bodies. | ||
| 277 | + func injectEventID(payload json.RawMessage, eventID string) json.RawMessage { | ||
| 278 | + var obj map[string]json.RawMessage | ||
| 279 | + if err := json.Unmarshal(payload, &obj); err != nil { | ||
| 280 | + return payload | ||
| 281 | + } | ||
| 282 | + obj["event_id"] = json.RawMessage(`"` + eventID + `"`) | ||
| 283 | + result, err := json.Marshal(obj) | ||
| 284 | + if err != nil { | ||
| 285 | + return payload | ||
| 286 | + } | ||
| 287 | + return result | ||
| 288 | + } | ||
| 289 | + | ||
| 264 | 290 | // decompress handles gzip and deflate Content-Encoding. | |
| 265 | 291 | // Also auto-detects gzip/zlib by magic bytes if header is missing. | |
| 266 | 292 | func decompress(data []byte, encoding string) []byte { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments