Skip to content

Commit 9f62c20

Browse files
authored
Merge pull request #320 from buggregator/feat/sentry-v2-structured-storage
feat: add Sentry v2 structured storage, parser, and REST API
2 parents 4ecbf65 + beb786d commit 9f62c20

43 files changed

Lines changed: 5011 additions & 210 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff 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+
```

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ php/vardumper/box.phar
66
php/vardumper/micro-*.sfx
77
php/vardumper/vardumper-parser.phar
88
modules/vardumper/bin/vardumper-parser-*
9+
internal/frontend/dist

buggregator.yaml.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
# HTTP server
99
server:
1010
addr: ${HTTP_ADDR::8000}
11+
cors_origins: # Allowed CORS origins. Default: ["*"] (all origins).
12+
- "*" # Use specific origins to restrict: ["http://localhost:3000"]
1113

1214
# Database
1315
database:

docker-compose.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ services:
88
- "${SMTP_PORT:-1025}:1025" # SMTP
99
- "${VAR_DUMPER_PORT:-9912}:9912" # VarDumper
1010
- "${MONOLOG_PORT:-9913}:9913" # Monolog
11+
volumes:
12+
- ../frontend/dist:/app/frontend/dist:ro # Mount local frontend build for development
13+
environment:
14+
FRONTEND_DIR: /app/frontend/dist # Serve from mounted volume instead of embedded
1115
networks:
1216
- buggregator
1317

@@ -40,12 +44,17 @@ services:
4044
RAY_PORT: 8000
4145
VAR_DUMPER_SERVER: buggregator:9912
4246
SENTRY_LARAVEL_DSN: http://sentry@buggregator:8000/default
47+
SENTRY_TRACES_SAMPLE_RATE: "1.0"
4348
LOG_SOCKET_URL: buggregator:9913
4449
INSPECTOR_URL: http://inspector@buggregator:8000
4550
INSPECTOR_API_KEY: test
4651
PROFILER_ENDPOINT: http://profiler@buggregator:8000
4752
HTTP_DUMP_ENDPOINT: http://http-dump@buggregator:8000
4853
SMS_ENDPOINT: http://buggregator:8000/sms
54+
volumes:
55+
- ../examples/app:/app/app:ro
56+
- ../examples/routes:/app/routes:ro
57+
- ../examples/config/sentry.php:/app/config/sentry.php:ro
4958
networks:
5059
- buggregator
5160

e2e/tests/helpers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ export async function triggerExample(action: string, route = '/example/call'): P
1515
// We don't care about the response, just that it was sent
1616
}
1717

18-
/** Clear all events via the API */
18+
/** Clear all events via the API (including sentry structured tables) */
1919
export async function clearEvents(): Promise<void> {
2020
await fetch(`${BUGGREGATOR_URL}/api/events`, {
2121
method: 'DELETE',
2222
headers: { 'Content-Type': 'application/json' },
2323
body: JSON.stringify({}),
2424
});
25+
// Also clear sentry structured tables.
26+
await fetch(`${BUGGREGATOR_URL}/api/sentry/all`, {
27+
method: 'DELETE',
28+
}).catch(() => {});
2529
}
2630

2731
/** Get event count from the API */

0 commit comments

Comments
 (0)