mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-11 17:51:33 +08:00
454 lines
12 KiB
Go
454 lines
12 KiB
Go
package impltracefunnel
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
"github.com/SigNoz/signoz/pkg/http/render"
|
|
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
tf "github.com/SigNoz/signoz/pkg/types/tracefunnel"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
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.FunnelRequest
|
|
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
|
|
}
|
|
userID := claims.UserID
|
|
orgID := claims.OrgID
|
|
|
|
funnels, err := handler.module.List(r.Context(), orgID)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
for _, f := range funnels {
|
|
if f.Name == req.Name {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"a funnel with name '%s' already exists in this organization",
|
|
req.Name))
|
|
return
|
|
}
|
|
}
|
|
|
|
funnel, err := handler.module.Create(r.Context(), req.Timestamp, req.Name, userID, orgID)
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to create funnel"))
|
|
return
|
|
}
|
|
|
|
response := tf.FunnelResponse{
|
|
FunnelID: funnel.ID.String(),
|
|
FunnelName: funnel.Name,
|
|
CreatedAt: req.Timestamp,
|
|
UserEmail: claims.Email,
|
|
OrgID: orgID,
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
|
|
var req tf.FunnelRequest
|
|
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
|
|
}
|
|
userID := claims.UserID
|
|
orgID := claims.OrgID
|
|
|
|
if err := tracefunnel.ValidateTimestamp(req.Timestamp, "timestamp"); err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"timestamp is invalid: %v", err))
|
|
return
|
|
}
|
|
|
|
funnel, err := handler.module.Get(r.Context(), req.FunnelID.String())
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"funnel not found: %v", err))
|
|
return
|
|
}
|
|
|
|
// Check if name is being updated and if it already exists
|
|
if req.Name != "" && req.Name != funnel.Name {
|
|
funnels, err := handler.module.List(r.Context(), orgID)
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to list funnels: %v", err))
|
|
return
|
|
}
|
|
|
|
for _, f := range funnels {
|
|
if f.Name == req.Name {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"a funnel with name '%s' already exists in this organization", req.Name))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process each step in the request
|
|
for i := range req.Steps {
|
|
if req.Steps[i].Order < 1 {
|
|
req.Steps[i].Order = int64(i + 1) // Default to sequential ordering if not specified
|
|
}
|
|
// Generate a new UUID for the step if it doesn't have one
|
|
if req.Steps[i].Id.IsZero() {
|
|
newUUID := valuer.GenerateUUID()
|
|
req.Steps[i].Id = newUUID
|
|
}
|
|
}
|
|
|
|
if err := tracefunnel.ValidateFunnelSteps(req.Steps); err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"invalid funnel steps: %v", err))
|
|
return
|
|
}
|
|
|
|
// Normalize step orders
|
|
req.Steps = tracefunnel.NormalizeFunnelSteps(req.Steps)
|
|
|
|
// Update the funnel with new steps
|
|
funnel.Steps = req.Steps
|
|
funnel.UpdatedAt = time.Unix(0, req.Timestamp*1000000) // Convert to nanoseconds
|
|
funnel.UpdatedBy = userID
|
|
|
|
if req.Name != "" {
|
|
funnel.Name = req.Name
|
|
}
|
|
if req.Description != "" {
|
|
funnel.Description = req.Description
|
|
}
|
|
|
|
// Update funnel in database
|
|
err = handler.module.Update(r.Context(), funnel, userID)
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to update funnel in database: %v", err))
|
|
return
|
|
}
|
|
|
|
// Get the updated funnel to return in response
|
|
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID.String())
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to get updated funnel: %v", err))
|
|
return
|
|
}
|
|
|
|
response := tf.FunnelResponse{
|
|
FunnelName: updatedFunnel.Name,
|
|
FunnelID: updatedFunnel.ID.String(),
|
|
Steps: updatedFunnel.Steps,
|
|
CreatedAt: updatedFunnel.CreatedAt.UnixNano() / 1000000,
|
|
CreatedBy: updatedFunnel.CreatedBy,
|
|
OrgID: updatedFunnel.OrgID.String(),
|
|
UpdatedBy: userID,
|
|
UpdatedAt: updatedFunnel.UpdatedAt.UnixNano() / 1000000,
|
|
Description: updatedFunnel.Description,
|
|
UserEmail: claims.Email,
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
|
|
var req tf.FunnelRequest
|
|
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
|
|
}
|
|
userID := claims.UserID
|
|
orgID := claims.OrgID
|
|
|
|
if err := tracefunnel.ValidateTimestamp(req.Timestamp, "timestamp"); err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "timestamp is invalid: %v", err))
|
|
return
|
|
}
|
|
vars := mux.Vars(r)
|
|
funnelID := vars["funnel_id"]
|
|
|
|
funnel, err := handler.module.Get(r.Context(), funnelID)
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "funnel not found: %v", err))
|
|
return
|
|
}
|
|
|
|
// Check if name is being updated and if it already exists
|
|
if req.Name != "" && req.Name != funnel.Name {
|
|
funnels, err := handler.module.List(r.Context(), orgID)
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to list funnels: %v", err))
|
|
return
|
|
}
|
|
|
|
for _, f := range funnels {
|
|
if f.Name == req.Name {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "a funnel with name '%s' already exists in this organization", req.Name))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
funnel.UpdatedAt = time.Unix(0, req.Timestamp*1000000) // Convert to nanoseconds
|
|
funnel.UpdatedBy = userID
|
|
|
|
if req.Name != "" {
|
|
funnel.Name = req.Name
|
|
}
|
|
if req.Description != "" {
|
|
funnel.Description = req.Description
|
|
}
|
|
|
|
err = handler.module.Update(r.Context(), funnel, userID)
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to update funnel in database: %v", err))
|
|
return
|
|
}
|
|
|
|
// Get the updated funnel to return in response
|
|
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID.String())
|
|
if err != nil {
|
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to get updated funnel: %v", err))
|
|
return
|
|
}
|
|
|
|
response := tf.FunnelResponse{
|
|
FunnelName: updatedFunnel.Name,
|
|
FunnelID: updatedFunnel.ID.String(),
|
|
Steps: updatedFunnel.Steps,
|
|
CreatedAt: updatedFunnel.CreatedAt.UnixNano() / 1000000,
|
|
CreatedBy: updatedFunnel.CreatedBy,
|
|
OrgID: updatedFunnel.OrgID.String(),
|
|
UpdatedBy: userID,
|
|
UpdatedAt: updatedFunnel.UpdatedAt.UnixNano() / 1000000,
|
|
Description: updatedFunnel.Description,
|
|
UserEmail: claims.Email,
|
|
}
|
|
|
|
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,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"unauthenticated"))
|
|
return
|
|
}
|
|
|
|
orgID := claims.OrgID
|
|
funnels, err := handler.module.List(r.Context(), orgID)
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to list funnels: %v", err))
|
|
return
|
|
}
|
|
|
|
var response []tf.FunnelResponse
|
|
for _, f := range funnels {
|
|
funnelResp := tf.FunnelResponse{
|
|
FunnelName: f.Name,
|
|
FunnelID: f.ID.String(),
|
|
CreatedAt: f.CreatedAt.UnixNano() / 1000000,
|
|
CreatedBy: f.CreatedBy,
|
|
OrgID: f.OrgID.String(),
|
|
UpdatedAt: f.UpdatedAt.UnixNano() / 1000000,
|
|
UpdatedBy: f.UpdatedBy,
|
|
Description: f.Description,
|
|
}
|
|
|
|
// Get user email if available
|
|
if f.CreatedByUser != nil {
|
|
funnelResp.UserEmail = f.CreatedByUser.Email
|
|
}
|
|
|
|
response = append(response, funnelResp)
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
funnelID := vars["funnel_id"]
|
|
|
|
funnel, err := handler.module.Get(r.Context(), funnelID)
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"funnel not found: %v", err))
|
|
return
|
|
}
|
|
|
|
response := tf.FunnelResponse{
|
|
FunnelID: funnel.ID.String(),
|
|
FunnelName: funnel.Name,
|
|
Description: funnel.Description,
|
|
CreatedAt: funnel.CreatedAt.UnixNano() / 1000000,
|
|
UpdatedAt: funnel.UpdatedAt.UnixNano() / 1000000,
|
|
CreatedBy: funnel.CreatedBy,
|
|
UpdatedBy: funnel.UpdatedBy,
|
|
OrgID: funnel.OrgID.String(),
|
|
Steps: funnel.Steps,
|
|
}
|
|
|
|
if funnel.CreatedByUser != nil {
|
|
response.UserEmail = funnel.CreatedByUser.Email
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, response)
|
|
}
|
|
|
|
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
funnelID := vars["funnel_id"]
|
|
|
|
err := handler.module.Delete(r.Context(), funnelID)
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to delete funnel: %v", err))
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, nil)
|
|
}
|
|
|
|
func (handler *handler) Save(rw http.ResponseWriter, r *http.Request) {
|
|
var req tf.FunnelRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"invalid request: %v", err))
|
|
return
|
|
}
|
|
|
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"unauthenticated"))
|
|
return
|
|
}
|
|
orgID := claims.OrgID
|
|
usrID := claims.UserID
|
|
|
|
funnel, err := handler.module.Get(r.Context(), req.FunnelID.String())
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"funnel not found: %v", err))
|
|
return
|
|
}
|
|
|
|
updateTimestamp := req.Timestamp
|
|
if updateTimestamp == 0 {
|
|
updateTimestamp = time.Now().UnixMilli()
|
|
} else if !tracefunnel.ValidateTimestampIsMilliseconds(updateTimestamp) {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"timestamp must be in milliseconds format (13 digits)"))
|
|
return
|
|
}
|
|
funnel.UpdatedAt = time.Unix(0, updateTimestamp*1000000) // Convert to nanoseconds
|
|
|
|
if req.UserID != "" {
|
|
funnel.UpdatedBy = usrID
|
|
}
|
|
|
|
funnel.Description = req.Description
|
|
|
|
if err := handler.module.Save(r.Context(), funnel, funnel.UpdatedBy, orgID); err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to save funnel: %v", err))
|
|
return
|
|
}
|
|
|
|
createdAt, updatedAt, extraDataFromDB, err := handler.module.GetFunnelMetadata(r.Context(), funnel.ID.String())
|
|
if err != nil {
|
|
render.Error(rw,
|
|
errors.Newf(errors.TypeInvalidInput,
|
|
errors.CodeInvalidInput,
|
|
"failed to get funnel metadata: %v", err))
|
|
return
|
|
}
|
|
|
|
resp := tf.FunnelResponse{
|
|
FunnelName: funnel.Name,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
CreatedBy: funnel.CreatedBy,
|
|
UpdatedBy: funnel.UpdatedBy,
|
|
OrgID: funnel.OrgID.String(),
|
|
Description: extraDataFromDB,
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, resp)
|
|
}
|