diff --git a/.gitignore b/.gitignore index 0ad2ef1..8d27a16 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ php/vardumper/box.phar php/vardumper/micro-*.sfx php/vardumper/vardumper-parser.phar modules/vardumper/bin/vardumper-parser-* -/internal/frontend/dist/* \ No newline at end of file +/internal/frontend/dist/* +fix.patch +.claude/settings.local.json diff --git a/internal/server/http/detect.go b/internal/server/http/detect.go index a14e59d..714ca50 100644 --- a/internal/server/http/detect.go +++ b/internal/server/http/detect.go @@ -37,7 +37,8 @@ func detectEventType(r *http.Request) *DetectedEvent { } // Method 3: SDK-specific headers that identify the event type. - if r.Header.Get("X-Sentry-Auth") != "" || strings.HasSuffix(r.URL.Path, "/envelope") || strings.HasSuffix(r.URL.Path, "/store") { + isSentryStore := strings.HasSuffix(r.URL.Path, "/store") && !strings.Contains(r.URL.Path, "/profiler/") + if r.Header.Get("X-Sentry-Auth") != "" || strings.HasSuffix(r.URL.Path, "/envelope") || isSentryStore { return &DetectedEvent{Type: "sentry"} } if r.Header.Get("X-Inspector-Key") != "" || r.Header.Get("X-Inspector-Version") != "" { diff --git a/internal/server/http/detect_test.go b/internal/server/http/detect_test.go index 3f31479..4a558a1 100644 --- a/internal/server/http/detect_test.go +++ b/internal/server/http/detect_test.go @@ -116,6 +116,13 @@ func TestDetectEventType(t *testing.T) { }, wantType: "sentry", }, + { + name: "profiler store path does not detect sentry", + makeRequest: func() *nethttp.Request { + return httptest.NewRequest("POST", "http://localhost/api/profiler/store", nil) + }, + wantNil: true, + }, { name: "X-Inspector-Key header detects inspector", makeRequest: func() *nethttp.Request { diff --git a/modules/profiler/api.go b/modules/profiler/api.go index a5c72c9..321c786 100644 --- a/modules/profiler/api.go +++ b/modules/profiler/api.go @@ -265,6 +265,7 @@ func handleFlameChart(db *sql.DB) http.HandlerFunc { } } + visited := make(map[string]bool) var buildNode func(e EdgeRow, start float64) *flameNode buildNode = func(e EdgeRow, start float64) *flameNode { val := float64(GetEdgeMetricValue(&e, metric)) @@ -285,11 +286,15 @@ func handleFlameChart(db *sql.DB) http.HandlerFunc { Color: flameColor(pct), } - childStart := start - for _, child := range childrenMap[e.Callee] { - childNode := buildNode(child, childStart) - node.Children = append(node.Children, childNode) - childStart += childNode.Duration + if !visited[e.Callee] { + visited[e.Callee] = true + childStart := start + for _, child := range childrenMap[e.Callee] { + childNode := buildNode(child, childStart) + node.Children = append(node.Children, childNode) + childStart += childNode.Duration + } + visited[e.Callee] = false } if node.Children == nil { node.Children = []*flameNode{} diff --git a/modules/profiler/handler.go b/modules/profiler/handler.go index 482a771..319c343 100644 --- a/modules/profiler/handler.go +++ b/modules/profiler/handler.go @@ -1,7 +1,6 @@ package profiler import ( - "database/sql" "encoding/json" "io" "net/http" @@ -11,7 +10,6 @@ import ( ) type handler struct { - db *sql.DB } func (h *handler) Priority() int { return 40 } @@ -40,14 +38,11 @@ func (h *handler) Handle(r *http.Request) (*event.Incoming, error) { // Process the profile (compute diffs, percentages, edges). peaks, edges := Process(&incoming) - // Store in profiler-specific tables. uuid := event.GenerateUUID() - if err := storeProfile(h.db, uuid, incoming.AppName, peaks, edges); err != nil { - return nil, err - } // Build event payload matching PHP Buggregator format. - // profile_uuid is used by the frontend to fetch call-graph/top/flame-chart. + // peaks and edges are embedded so OnEventStored can store them + // without re-parsing the original payload. payload := map[string]any{ "profile_uuid": uuid, "app_name": incoming.AppName, @@ -55,6 +50,7 @@ func (h *handler) Handle(r *http.Request) (*event.Incoming, error) { "date": incoming.Date, "tags": incoming.Tags, "peaks": peaks, + "edges": edges, "total_edges": len(edges), } b, _ := json.Marshal(payload) @@ -65,45 +61,3 @@ func (h *handler) Handle(r *http.Request) (*event.Incoming, error) { Payload: json.RawMessage(b), }, nil } - -func storeProfile(db *sql.DB, uuid, name string, peaks Metrics, edges map[string]Edge) error { - tx, err := db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - _, err = tx.Exec( - `INSERT INTO profiles (uuid, name, cpu, wt, ct, mu, pmu) VALUES (?, ?, ?, ?, ?, ?, ?)`, - uuid, name, peaks.CPU, peaks.WallTime, peaks.Calls, peaks.Memory, peaks.PeakMem, - ) - if err != nil { - return err - } - - stmt, err := tx.Prepare(`INSERT INTO profile_edges - (uuid, profile_uuid, "order", cpu, wt, ct, mu, pmu, d_cpu, d_wt, d_ct, d_mu, d_pmu, p_cpu, p_wt, p_ct, p_mu, p_pmu, callee, caller, parent_uuid) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) - if err != nil { - return err - } - defer stmt.Close() - - order := 0 - for _, edge := range edges { - edgeUUID := event.GenerateUUID() - _, err = stmt.Exec( - edgeUUID, uuid, order, - edge.Cost.CPU, edge.Cost.WallTime, edge.Cost.Calls, edge.Cost.Memory, edge.Cost.PeakMem, - edge.Diff.CPU, edge.Diff.WallTime, edge.Diff.Calls, edge.Diff.Memory, edge.Diff.PeakMem, - edge.Percents.CPU, edge.Percents.WallTime, edge.Percents.Calls, edge.Percents.Memory, edge.Percents.PeakMem, - edge.Callee, edge.Caller, edge.Parent, - ) - if err != nil { - return err - } - order++ - } - - return tx.Commit() -} diff --git a/modules/profiler/module.go b/modules/profiler/module.go index 41a086d..5e0a690 100644 --- a/modules/profiler/module.go +++ b/modules/profiler/module.go @@ -2,6 +2,8 @@ package profiler import ( "database/sql" + "encoding/json" + "log/slog" "net/http" "github.com/buggregator/go-buggregator/internal/event" @@ -29,7 +31,7 @@ func (m *Module) OnInit(db *sql.DB) error { } func (m *Module) HTTPHandler() module.HTTPIngestionHandler { - return &handler{db: m.db} + return &handler{} } func (m *Module) RegisterRoutes(mux *http.ServeMux, store event.Store) { @@ -39,3 +41,24 @@ func (m *Module) RegisterRoutes(mux *http.ServeMux, store event.Store) { func (m *Module) PreviewMapper() event.PreviewMapper { return &previewMapper{} } + +func (m *Module) OnEventStored(ev event.Event) { + if ev.Type != "profiler" || m.db == nil { + return + } + + var payload struct { + ProfileUUID string `json:"profile_uuid"` + AppName string `json:"app_name"` + Peaks Metrics `json:"peaks"` + Edges map[string]Edge `json:"edges"` + } + if err := json.Unmarshal(ev.Payload, &payload); err != nil { + slog.Warn("profiler: failed to parse event payload", "err", err) + return + } + + if err := storeProfile(m.db, payload.ProfileUUID, payload.AppName, payload.Peaks, payload.Edges); err != nil { + slog.Warn("profiler: failed to store profile", "err", err) + } +} diff --git a/modules/profiler/store.go b/modules/profiler/store.go new file mode 100644 index 0000000..bbaf4b5 --- /dev/null +++ b/modules/profiler/store.go @@ -0,0 +1,49 @@ +package profiler + +import ( + "database/sql" + + "github.com/buggregator/go-buggregator/internal/event" +) + +func storeProfile(db *sql.DB, uuid, name string, peaks Metrics, edges map[string]Edge) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.Exec( + `INSERT INTO profiles (uuid, name, cpu, wt, ct, mu, pmu) VALUES (?, ?, ?, ?, ?, ?, ?)`, + uuid, name, peaks.CPU, peaks.WallTime, peaks.Calls, peaks.Memory, peaks.PeakMem, + ) + if err != nil { + return err + } + + stmt, err := tx.Prepare(`INSERT INTO profile_edges + (uuid, profile_uuid, "order", cpu, wt, ct, mu, pmu, d_cpu, d_wt, d_ct, d_mu, d_pmu, p_cpu, p_wt, p_ct, p_mu, p_pmu, callee, caller, parent_uuid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + if err != nil { + return err + } + defer stmt.Close() + + order := 0 + for _, edge := range edges { + edgeUUID := event.GenerateUUID() + _, err = stmt.Exec( + edgeUUID, uuid, order, + edge.Cost.CPU, edge.Cost.WallTime, edge.Cost.Calls, edge.Cost.Memory, edge.Cost.PeakMem, + edge.Diff.CPU, edge.Diff.WallTime, edge.Diff.Calls, edge.Diff.Memory, edge.Diff.PeakMem, + edge.Percents.CPU, edge.Percents.WallTime, edge.Percents.Calls, edge.Percents.Memory, edge.Percents.PeakMem, + edge.Callee, edge.Caller, edge.Parent, + ) + if err != nil { + return err + } + order++ + } + + return tx.Commit() +} diff --git a/modules/sentry/handler.go b/modules/sentry/handler.go index 4ab414b..92df842 100644 --- a/modules/sentry/handler.go +++ b/modules/sentry/handler.go @@ -31,7 +31,8 @@ func (h *handler) Match(r *http.Request) bool { return true } path := r.URL.Path - return strings.HasSuffix(path, "/store") || strings.HasSuffix(path, "/envelope") + isSentryStore := strings.HasSuffix(path, "/store") && !strings.Contains(path, "/profiler/") + return isSentryStore || strings.HasSuffix(path, "/envelope") } func (h *handler) Handle(r *http.Request) (*event.Incoming, error) {