mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
Twenty-nine handler bodies across six packages named gorilla/mux to read a path
segment, so the router was braided into the rule, alert, trace and sentry logic
that addresses a resource by id. The value each one needs is the SEGMENT; which
router matched the route is not a fact these handlers should hold. All twenty-
nine now read through coretypes.Param — the one route-value reader — and the
tracefunnel test's two injectors through coretypes.SetParams, its writer half.
A PATH segment and a QUERY value stay apart, because that is the failure this
seam exists to prevent: r.URL.Query().Get is untouched everywhere it appears
(the history window, sentry's ?project=, the funnel limits), and three of the
new tests pin it by sending a query value of the SAME name as the path segment
and asserting the path still answers.
Two helpers, both deleting real duplication rather than adding a second reader:
o11yruler.idFromPath replaces the same five-line uuid-v7 block written out
seven times, and o11yalertmanager.channelIDFromPath replaces a seventeen-line
nil-map/ok/parse dance written out three times. Absent and malformed remain
DIFFERENT refusals in the alertmanager one — "id is required in path" vs "id is
not a valid uuid-v7" — which is what the code it replaces answered.
Eleven new tests, and they are not vacuous. Each drives a REAL router at the
REAL template — /v1/o11y/rules/{id}/history/stats, /v1/o11y/channels/{id},
/v1/o11y/traces/{traceID}/waterfall, /v1/sentry/events/{id}, the uuid-
constrained /v1/sentry/{project}/envelope/ — and asserts the MODULE was asked
about what the router matched, so a passing test needs the whole path from
route table to business call. Stubbing Param to "" (go test -overlay, so the
shared tree is never mutated) turns all six packages RED; before these tests
five of them had no test that touched a param at all.
Registration is untouched: no {id} literal, no *mux.Router, no .Methods() moved.
Route table unchanged — TestEveryRouteIsNamedAndCounted still passes at 367.
Co-authored-by: Hanzo Dev <[email protected]>
233 lines
6.0 KiB
Go
233 lines
6.0 KiB
Go
package impltracefunnel
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/hanzoai/o11y/pkg/errors"
|
|
"github.com/hanzoai/o11y/pkg/http/render"
|
|
"github.com/hanzoai/o11y/pkg/modules/tracefunnel"
|
|
"github.com/hanzoai/o11y/pkg/types/authtypes"
|
|
"github.com/hanzoai/o11y/pkg/types/coretypes"
|
|
tf "github.com/hanzoai/o11y/pkg/types/tracefunneltypes"
|
|
"github.com/hanzoai/o11y/pkg/valuer"
|
|
)
|
|
|
|
type handler struct {
|
|
module tracefunnel.Module
|
|
}
|
|
|
|
func NewHandler(module tracefunnel.Module) tracefunnel.Handler {
|
|
return &handler{module: module}
|
|
}
|
|
|
|
func (handler *handler) New(rw http.ResponseWriter, r *http.Request) {
|
|
var req tf.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := handler.module.Create(r.Context(), req.Timestamp, req.Name, valuer.MustNewUUID(claims.UserID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to create funnel: %v", err))
|
|
return
|
|
}
|
|
|
|
response := tf.ConstructFunnelResponse(funnel, &claims)
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
|
|
var req tf.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := handler.module.Get(r.Context(), req.FunnelID, valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"funnel not found: %v", err))
|
|
return
|
|
}
|
|
|
|
steps, err := tf.ProcessFunnelSteps(req.Steps)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
funnel.Steps = steps
|
|
funnel.UpdatedAt = updatedAt
|
|
funnel.UpdatedBy = claims.UserID
|
|
|
|
if req.Name != "" {
|
|
funnel.Name = req.Name
|
|
}
|
|
if req.Description != "" {
|
|
funnel.Description = req.Description
|
|
}
|
|
|
|
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to update funnel in database: %v", err))
|
|
return
|
|
}
|
|
|
|
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to get updated funnel: %v", err))
|
|
return
|
|
}
|
|
|
|
response := tf.ConstructFunnelResponse(updatedFunnel, &claims)
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
|
|
var req tf.PostableFunnel
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
funnel, err := handler.module.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"funnel not found: %v", err))
|
|
return
|
|
}
|
|
|
|
funnel.UpdatedAt = updatedAt
|
|
funnel.UpdatedBy = claims.UserID
|
|
|
|
if req.Name != "" {
|
|
funnel.Name = req.Name
|
|
}
|
|
if req.Description != "" {
|
|
funnel.Description = req.Description
|
|
}
|
|
|
|
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to update funnel in database: %v", err))
|
|
return
|
|
}
|
|
|
|
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to get updated funnel: %v", err))
|
|
return
|
|
}
|
|
|
|
response := tf.ConstructFunnelResponse(updatedFunnel, &claims)
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
funnels, err := handler.module.List(r.Context(), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to list funnels: %v", err))
|
|
return
|
|
}
|
|
|
|
var response []tf.GettableFunnel
|
|
for _, f := range funnels {
|
|
response = append(response, tf.ConstructFunnelResponse(f, &claims))
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
funnel, err := handler.module.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"funnel not found: %v", err))
|
|
return
|
|
}
|
|
response := tf.ConstructFunnelResponse(funnel, &claims)
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
|
funnelID := coretypes.Param(r, "funnel_id")
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
if err := handler.module.Delete(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID)); err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to delete funnel: %v", err))
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, nil)
|
|
}
|