mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-15 14:35:53 +08:00
fix: review comments and some changes
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
This commit is contained in:
parent
22fdeb1381
commit
a22d061ec1
@ -2,13 +2,14 @@ package impltracefunnel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||||
tf "github.com/SigNoz/signoz/pkg/types/tracefunnel"
|
tf "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ func (handler *handler) New(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
funnel, err := handler.module.Create(r.Context(), req.Timestamp, req.Name, claims.UserID, claims.OrgID)
|
funnel, err := handler.module.Create(r.Context(), req.Timestamp, req.Name, valuer.MustNewUUID(claims.UserID), valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
@ -64,7 +65,7 @@ func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
funnel, err := handler.module.Get(r.Context(), req.FunnelID.String())
|
funnel, err := handler.module.Get(r.Context(), req.FunnelID, valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
@ -89,14 +90,14 @@ func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
|
|||||||
funnel.Description = req.Description
|
funnel.Description = req.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handler.module.Update(r.Context(), funnel, claims.UserID); err != nil {
|
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
"failed to update funnel in database: %v", err))
|
"failed to update funnel in database: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID.String())
|
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
@ -130,7 +131,7 @@ func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
funnelID := vars["funnel_id"]
|
funnelID := vars["funnel_id"]
|
||||||
|
|
||||||
funnel, err := handler.module.Get(r.Context(), funnelID)
|
funnel, err := handler.module.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
@ -148,14 +149,14 @@ func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
|
|||||||
funnel.Description = req.Description
|
funnel.Description = req.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handler.module.Update(r.Context(), funnel, claims.UserID); err != nil {
|
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
"failed to update funnel in database: %v", err))
|
"failed to update funnel in database: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID.String())
|
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
@ -174,7 +175,7 @@ func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
funnels, err := handler.module.List(r.Context(), claims.OrgID)
|
funnels, err := handler.module.List(r.Context(), valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
@ -194,15 +195,15 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
funnelID := vars["funnel_id"]
|
funnelID := vars["funnel_id"]
|
||||||
|
|
||||||
funnel, err := handler.module.Get(r.Context(), funnelID)
|
claims, _ := tf.GetClaims(r) // Ignore error as email is optional
|
||||||
|
|
||||||
|
funnel, err := handler.module.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
"funnel not found: %v", err))
|
"funnel not found: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, _ := tf.GetClaims(r) // Ignore error as email is optional
|
|
||||||
response := tf.ConstructFunnelResponse(funnel, claims)
|
response := tf.ConstructFunnelResponse(funnel, claims)
|
||||||
render.Success(rw, http.StatusOK, response)
|
render.Success(rw, http.StatusOK, response)
|
||||||
}
|
}
|
||||||
@ -211,7 +212,9 @@ func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
funnelID := vars["funnel_id"]
|
funnelID := vars["funnel_id"]
|
||||||
|
|
||||||
if err := handler.module.Delete(r.Context(), funnelID); err != nil {
|
claims, _ := tf.GetClaims(r)
|
||||||
|
|
||||||
|
if err := handler.module.Delete(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID)); err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
"failed to delete funnel: %v", err))
|
"failed to delete funnel: %v", err))
|
||||||
@ -236,7 +239,7 @@ func (handler *handler) Save(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
funnel, err := handler.module.Get(r.Context(), req.FunnelID.String())
|
funnel, err := handler.module.Get(r.Context(), req.FunnelID, valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
@ -264,14 +267,14 @@ func (handler *handler) Save(rw http.ResponseWriter, r *http.Request) {
|
|||||||
funnel.UpdatedBy = claims.UserID
|
funnel.UpdatedBy = claims.UserID
|
||||||
funnel.Description = req.Description
|
funnel.Description = req.Description
|
||||||
|
|
||||||
if err := handler.module.Save(r.Context(), funnel, funnel.UpdatedBy, claims.OrgID); err != nil {
|
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.OrgID)); err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
"failed to save funnel: %v", err))
|
"failed to save funnel: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
createdAtMillis, updatedAtMillis, extraDataFromDB, err := handler.module.GetFunnelMetadata(r.Context(), funnel.ID.String())
|
createdAtMillis, updatedAtMillis, extraDataFromDB, err := handler.module.GetFunnelMetadata(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
errors.CodeInvalidInput,
|
errors.CodeInvalidInput,
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunnel"
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -22,238 +22,50 @@ type MockModule struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockModule) Create(ctx context.Context, timestamp int64, name string, userID string, orgID string) (*traceFunnels.StorableFunnel, error) {
|
func (m *MockModule) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
args := m.Called(ctx, timestamp, name, userID, orgID)
|
args := m.Called(ctx, timestamp, name, userID, orgID)
|
||||||
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockModule) Get(ctx context.Context, funnelID string) (*traceFunnels.StorableFunnel, error) {
|
func (m *MockModule) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
args := m.Called(ctx, funnelID)
|
args := m.Called(ctx, funnelID, orgID)
|
||||||
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockModule) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID string) error {
|
func (m *MockModule) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
|
||||||
args := m.Called(ctx, funnel, userID)
|
args := m.Called(ctx, funnel, userID)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockModule) List(ctx context.Context, orgID string) ([]*traceFunnels.StorableFunnel, error) {
|
func (m *MockModule) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||||
args := m.Called(ctx, orgID)
|
args := m.Called(ctx, orgID)
|
||||||
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
|
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockModule) Delete(ctx context.Context, funnelID string) error {
|
func (m *MockModule) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
|
||||||
args := m.Called(ctx, funnelID)
|
args := m.Called(ctx, funnelID, orgID)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockModule) Save(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID string, orgID string) error {
|
func (m *MockModule) Save(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID, orgID valuer.UUID) error {
|
||||||
args := m.Called(ctx, funnel, userID, orgID)
|
args := m.Called(ctx, funnel, userID, orgID)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockModule) GetFunnelMetadata(ctx context.Context, funnelID string) (int64, int64, string, error) {
|
func (m *MockModule) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
|
||||||
args := m.Called(ctx, funnelID)
|
args := m.Called(ctx, funnelID, orgID)
|
||||||
return args.Get(0).(int64), args.Get(1).(int64), args.String(2), args.Error(3)
|
return args.Get(0).(int64), args.Get(1).(int64), args.String(2), args.Error(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandler_New(t *testing.T) {
|
|
||||||
mockModule := new(MockModule)
|
|
||||||
handler := NewHandler(mockModule)
|
|
||||||
|
|
||||||
reqBody := traceFunnels.PostableFunnel{
|
|
||||||
Name: "test-funnel",
|
|
||||||
Timestamp: time.Now().UnixMilli(),
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBody, _ := json.Marshal(reqBody)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/trace-funnels/new", bytes.NewBuffer(jsonBody))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
orgID := valuer.GenerateUUID().String()
|
|
||||||
claims := authtypes.Claims{
|
|
||||||
UserID: "user-123",
|
|
||||||
OrgID: orgID,
|
|
||||||
Email: "test@example.com",
|
|
||||||
}
|
|
||||||
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
funnelID := valuer.GenerateUUID()
|
|
||||||
expectedFunnel := &traceFunnels.StorableFunnel{
|
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
|
||||||
Identifiable: types.Identifiable{
|
|
||||||
ID: funnelID,
|
|
||||||
},
|
|
||||||
Name: reqBody.Name,
|
|
||||||
OrgID: valuer.MustNewUUID(orgID),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockModule.On("List", req.Context(), orgID).Return([]*traceFunnels.StorableFunnel{}, nil)
|
|
||||||
mockModule.On("Create", req.Context(), reqBody.Timestamp, reqBody.Name, "user-123", orgID).Return(expectedFunnel, nil)
|
|
||||||
|
|
||||||
handler.New(rr, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, rr.Code)
|
|
||||||
|
|
||||||
var response struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Data traceFunnels.GettableFunnel `json:"data"`
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "success", response.Status)
|
|
||||||
assert.Equal(t, reqBody.Name, response.Data.FunnelName)
|
|
||||||
assert.Equal(t, orgID, response.Data.OrgID)
|
|
||||||
assert.Equal(t, "test@example.com", response.Data.UserEmail)
|
|
||||||
|
|
||||||
mockModule.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandler_Update(t *testing.T) {
|
|
||||||
mockModule := new(MockModule)
|
|
||||||
handler := NewHandler(mockModule)
|
|
||||||
|
|
||||||
// Create a valid UUID for the funnel ID
|
|
||||||
funnelID := valuer.GenerateUUID()
|
|
||||||
orgID := valuer.GenerateUUID().String()
|
|
||||||
|
|
||||||
reqBody := traceFunnels.PostableFunnel{
|
|
||||||
FunnelID: funnelID,
|
|
||||||
Name: "updated-funnel",
|
|
||||||
Steps: []*traceFunnels.FunnelStep{
|
|
||||||
{
|
|
||||||
ID: valuer.GenerateUUID(),
|
|
||||||
Name: "Step 1",
|
|
||||||
ServiceName: "test-service",
|
|
||||||
SpanName: "test-span",
|
|
||||||
Order: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: valuer.GenerateUUID(),
|
|
||||||
Name: "Step 2",
|
|
||||||
ServiceName: "test-service",
|
|
||||||
SpanName: "test-span-2",
|
|
||||||
Order: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Timestamp: time.Now().UnixMilli(),
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(reqBody)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPut, "/api/v1/trace-funnels/steps/update", bytes.NewBuffer(body))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// Set up context with claims
|
|
||||||
claims := authtypes.Claims{
|
|
||||||
UserID: "user-123",
|
|
||||||
OrgID: orgID,
|
|
||||||
Email: "test@example.com",
|
|
||||||
}
|
|
||||||
ctx := authtypes.NewContextWithClaims(req.Context(), claims)
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Set up mock expectations
|
|
||||||
existingFunnel := &traceFunnels.StorableFunnel{
|
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
|
||||||
Identifiable: types.Identifiable{
|
|
||||||
ID: funnelID,
|
|
||||||
},
|
|
||||||
Name: "test-funnel",
|
|
||||||
OrgID: valuer.MustNewUUID(orgID),
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
UserAuditable: types.UserAuditable{
|
|
||||||
CreatedBy: "user-123",
|
|
||||||
UpdatedBy: "user-123",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CreatedByUser: &types.User{
|
|
||||||
ID: "user-123",
|
|
||||||
Email: "test@example.com",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedFunnel := &traceFunnels.StorableFunnel{
|
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
|
||||||
Identifiable: types.Identifiable{
|
|
||||||
ID: funnelID,
|
|
||||||
},
|
|
||||||
Name: reqBody.Name,
|
|
||||||
OrgID: valuer.MustNewUUID(orgID),
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Unix(0, reqBody.Timestamp*1000000),
|
|
||||||
},
|
|
||||||
UserAuditable: types.UserAuditable{
|
|
||||||
CreatedBy: "user-123",
|
|
||||||
UpdatedBy: "user-123",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Steps: reqBody.Steps,
|
|
||||||
CreatedByUser: &types.User{
|
|
||||||
ID: "user-123",
|
|
||||||
Email: "test@example.com",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// First Get call to validate the funnel exists
|
|
||||||
mockModule.On("Get", req.Context(), funnelID.String()).Return(existingFunnel, nil).Once()
|
|
||||||
// List call to check for name conflicts
|
|
||||||
mockModule.On("List", req.Context(), orgID).Return([]*traceFunnels.StorableFunnel{}, nil).Once()
|
|
||||||
// Update call to save the changes
|
|
||||||
mockModule.On("Update", req.Context(), mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
|
|
||||||
return f.Name == reqBody.Name &&
|
|
||||||
f.ID.String() == funnelID.String() &&
|
|
||||||
len(f.Steps) == len(reqBody.Steps) &&
|
|
||||||
f.Steps[0].Name == reqBody.Steps[0].Name &&
|
|
||||||
f.Steps[0].ServiceName == reqBody.Steps[0].ServiceName &&
|
|
||||||
f.Steps[0].SpanName == reqBody.Steps[0].SpanName &&
|
|
||||||
f.Steps[1].Name == reqBody.Steps[1].Name &&
|
|
||||||
f.Steps[1].ServiceName == reqBody.Steps[1].ServiceName &&
|
|
||||||
f.Steps[1].SpanName == reqBody.Steps[1].SpanName &&
|
|
||||||
f.UpdatedAt.UnixNano()/1000000 == reqBody.Timestamp &&
|
|
||||||
f.UpdatedBy == "user-123"
|
|
||||||
}), "user-123").Return(nil).Once()
|
|
||||||
// Second Get call to get the updated funnel for the response
|
|
||||||
mockModule.On("Get", req.Context(), funnelID.String()).Return(updatedFunnel, nil).Once()
|
|
||||||
|
|
||||||
handler.UpdateSteps(rr, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, rr.Code)
|
|
||||||
|
|
||||||
var response struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Data traceFunnels.GettableFunnel `json:"data"`
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "success", response.Status)
|
|
||||||
assert.Equal(t, "updated-funnel", response.Data.FunnelName)
|
|
||||||
assert.Equal(t, funnelID.String(), response.Data.FunnelID)
|
|
||||||
assert.Equal(t, "test@example.com", response.Data.UserEmail)
|
|
||||||
|
|
||||||
mockModule.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandler_List(t *testing.T) {
|
func TestHandler_List(t *testing.T) {
|
||||||
mockModule := new(MockModule)
|
mockModule := new(MockModule)
|
||||||
handler := NewHandler(mockModule)
|
handler := NewHandler(mockModule)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/list", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/list", nil)
|
||||||
|
|
||||||
orgID := valuer.GenerateUUID().String()
|
orgID := valuer.GenerateUUID()
|
||||||
claims := authtypes.Claims{
|
claims := authtypes.Claims{
|
||||||
OrgID: orgID,
|
OrgID: orgID.String(),
|
||||||
}
|
}
|
||||||
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
|
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
|
||||||
|
|
||||||
@ -263,22 +75,18 @@ func TestHandler_List(t *testing.T) {
|
|||||||
funnel2ID := valuer.GenerateUUID()
|
funnel2ID := valuer.GenerateUUID()
|
||||||
expectedFunnels := []*traceFunnels.StorableFunnel{
|
expectedFunnels := []*traceFunnels.StorableFunnel{
|
||||||
{
|
{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Identifiable: types.Identifiable{
|
||||||
Identifiable: types.Identifiable{
|
ID: funnel1ID,
|
||||||
ID: funnel1ID,
|
|
||||||
},
|
|
||||||
Name: "funnel-1",
|
|
||||||
OrgID: valuer.MustNewUUID(orgID),
|
|
||||||
},
|
},
|
||||||
|
Name: "funnel-1",
|
||||||
|
OrgID: orgID,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Identifiable: types.Identifiable{
|
||||||
Identifiable: types.Identifiable{
|
ID: funnel2ID,
|
||||||
ID: funnel2ID,
|
|
||||||
},
|
|
||||||
Name: "funnel-2",
|
|
||||||
OrgID: valuer.MustNewUUID(orgID),
|
|
||||||
},
|
},
|
||||||
|
Name: "funnel-2",
|
||||||
|
OrgID: orgID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,22 +115,24 @@ func TestHandler_Get(t *testing.T) {
|
|||||||
handler := NewHandler(mockModule)
|
handler := NewHandler(mockModule)
|
||||||
|
|
||||||
funnelID := valuer.GenerateUUID()
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/"+funnelID.String(), nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/"+funnelID.String(), nil)
|
||||||
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
|
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
|
||||||
|
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), authtypes.Claims{
|
||||||
|
OrgID: orgID.String(),
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
expectedFunnel := &traceFunnels.StorableFunnel{
|
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Identifiable: types.Identifiable{
|
||||||
Identifiable: types.Identifiable{
|
ID: funnelID,
|
||||||
ID: funnelID,
|
|
||||||
},
|
|
||||||
Name: "test-funnel",
|
|
||||||
OrgID: valuer.GenerateUUID(),
|
|
||||||
},
|
},
|
||||||
|
Name: "test-funnel",
|
||||||
|
OrgID: orgID,
|
||||||
}
|
}
|
||||||
|
|
||||||
mockModule.On("Get", req.Context(), funnelID.String()).Return(expectedFunnel, nil)
|
mockModule.On("Get", req.Context(), funnelID, orgID).Return(expectedFunnel, nil)
|
||||||
|
|
||||||
handler.Get(rr, req)
|
handler.Get(rr, req)
|
||||||
|
|
||||||
@ -346,12 +156,16 @@ func TestHandler_Delete(t *testing.T) {
|
|||||||
handler := NewHandler(mockModule)
|
handler := NewHandler(mockModule)
|
||||||
|
|
||||||
funnelID := valuer.GenerateUUID()
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/trace-funnels/"+funnelID.String(), nil)
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/trace-funnels/"+funnelID.String(), nil)
|
||||||
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
|
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
|
||||||
|
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), authtypes.Claims{
|
||||||
|
OrgID: orgID.String(),
|
||||||
|
}))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
mockModule.On("Delete", req.Context(), funnelID.String()).Return(nil)
|
mockModule.On("Delete", req.Context(), funnelID, orgID).Return(nil)
|
||||||
|
|
||||||
handler.Delete(rr, req)
|
handler.Delete(rr, req)
|
||||||
|
|
||||||
@ -375,34 +189,33 @@ func TestHandler_Save(t *testing.T) {
|
|||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/trace-funnels/save", bytes.NewBuffer(jsonBody))
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/trace-funnels/save", bytes.NewBuffer(jsonBody))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
orgID := valuer.GenerateUUID().String()
|
orgID := valuer.GenerateUUID()
|
||||||
|
userID := valuer.GenerateUUID()
|
||||||
claims := authtypes.Claims{
|
claims := authtypes.Claims{
|
||||||
UserID: "user-123",
|
UserID: userID.String(),
|
||||||
OrgID: orgID,
|
OrgID: orgID.String(),
|
||||||
}
|
}
|
||||||
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
|
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
existingFunnel := &traceFunnels.StorableFunnel{
|
existingFunnel := &traceFunnels.StorableFunnel{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Identifiable: types.Identifiable{
|
||||||
Identifiable: types.Identifiable{
|
ID: reqBody.FunnelID,
|
||||||
ID: reqBody.FunnelID,
|
|
||||||
},
|
|
||||||
Name: "test-funnel",
|
|
||||||
OrgID: valuer.MustNewUUID(orgID),
|
|
||||||
},
|
},
|
||||||
|
Name: "test-funnel",
|
||||||
|
OrgID: orgID,
|
||||||
}
|
}
|
||||||
|
|
||||||
mockModule.On("Get", req.Context(), reqBody.FunnelID.String()).Return(existingFunnel, nil)
|
mockModule.On("Get", req.Context(), reqBody.FunnelID, orgID).Return(existingFunnel, nil)
|
||||||
mockModule.On("Save", req.Context(), mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
|
mockModule.On("Update", req.Context(), mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
|
||||||
return f.ID.String() == reqBody.FunnelID.String() &&
|
return f.ID.String() == reqBody.FunnelID.String() &&
|
||||||
f.Name == existingFunnel.Name &&
|
f.Name == existingFunnel.Name &&
|
||||||
f.Description == reqBody.Description &&
|
f.Description == reqBody.Description &&
|
||||||
f.UpdatedBy == "user-123" &&
|
f.UpdatedBy == userID.String() &&
|
||||||
f.OrgID.String() == orgID
|
f.OrgID.String() == orgID.String()
|
||||||
}), "user-123", orgID).Return(nil)
|
}), orgID).Return(nil)
|
||||||
mockModule.On("GetFunnelMetadata", req.Context(), reqBody.FunnelID.String()).Return(int64(0), int64(0), reqBody.Description, nil)
|
mockModule.On("GetFunnelMetadata", req.Context(), reqBody.FunnelID, orgID).Return(int64(0), int64(0), reqBody.Description, nil)
|
||||||
|
|
||||||
handler.Save(rr, req)
|
handler.Save(rr, req)
|
||||||
|
|
||||||
|
@ -3,11 +3,11 @@ package impltracefunnel
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunnel"
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,25 +21,18 @@ func NewModule(store traceFunnels.FunnelStore) tracefunnel.Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (module *module) Create(ctx context.Context, timestamp int64, name string, userID string, orgID string) (*traceFunnels.StorableFunnel, error) {
|
func (module *module) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
orgUUID, err := valuer.NewUUID(orgID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid org ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
funnel := &traceFunnels.StorableFunnel{
|
funnel := &traceFunnels.StorableFunnel{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Name: name,
|
||||||
Name: name,
|
OrgID: orgID,
|
||||||
OrgID: orgUUID,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
funnel.CreatedAt = time.Unix(0, timestamp*1000000) // Convert to nanoseconds
|
funnel.CreatedAt = time.Unix(0, timestamp*1000000) // Convert to nanoseconds
|
||||||
funnel.CreatedBy = userID
|
funnel.CreatedBy = userID.String()
|
||||||
|
|
||||||
// Set up the user relationship
|
// Set up the user relationship
|
||||||
funnel.CreatedByUser = &types.User{
|
funnel.CreatedByUser = &types.User{
|
||||||
Identifiable: types.Identifiable{
|
Identifiable: types.Identifiable{
|
||||||
ID: valuer.MustNewUUID(userID),
|
ID: userID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,28 +60,19 @@ func (module *module) Create(ctx context.Context, timestamp int64, name string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get gets a funnel by ID
|
// Get gets a funnel by ID
|
||||||
func (module *module) Get(ctx context.Context, funnelID string) (*traceFunnels.StorableFunnel, error) {
|
func (module *module) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
uuid, err := valuer.NewUUID(funnelID)
|
return module.store.Get(ctx, funnelID, orgID)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid funnel ID: %v", err)
|
|
||||||
}
|
|
||||||
return module.store.Get(ctx, uuid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates a funnel
|
// Update updates a funnel
|
||||||
func (module *module) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID string) error {
|
func (module *module) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
|
||||||
funnel.UpdatedBy = userID
|
funnel.UpdatedBy = userID.String()
|
||||||
return module.store.Update(ctx, funnel)
|
return module.store.Update(ctx, funnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List lists all funnels for an organization
|
// List lists all funnels for an organization
|
||||||
func (module *module) List(ctx context.Context, orgID string) ([]*traceFunnels.StorableFunnel, error) {
|
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||||
orgUUID, err := valuer.NewUUID(orgID)
|
funnels, err := module.store.List(ctx, orgID)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid org ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
funnels, err := module.store.List(ctx, orgUUID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list funnels: %v", err)
|
return nil, fmt.Errorf("failed to list funnels: %v", err)
|
||||||
}
|
}
|
||||||
@ -97,34 +81,13 @@ func (module *module) List(ctx context.Context, orgID string) ([]*traceFunnels.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes a funnel
|
// Delete deletes a funnel
|
||||||
func (module *module) Delete(ctx context.Context, funnelID string) error {
|
func (module *module) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
|
||||||
uuid, err := valuer.NewUUID(funnelID)
|
return module.store.Delete(ctx, funnelID, orgID)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid funnel ID: %v", err)
|
|
||||||
}
|
|
||||||
return module.store.Delete(ctx, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save saves a funnel
|
|
||||||
func (module *module) Save(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID string, orgID string) error {
|
|
||||||
orgUUID, err := valuer.NewUUID(orgID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid org ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
funnel.UpdatedBy = userID
|
|
||||||
funnel.OrgID = orgUUID
|
|
||||||
return module.store.Update(ctx, funnel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFunnelMetadata gets metadata for a funnel
|
// GetFunnelMetadata gets metadata for a funnel
|
||||||
func (module *module) GetFunnelMetadata(ctx context.Context, funnelID string) (int64, int64, string, error) {
|
func (module *module) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
|
||||||
uuid, err := valuer.NewUUID(funnelID)
|
funnel, err := module.store.Get(ctx, funnelID, orgID)
|
||||||
if err != nil {
|
|
||||||
return 0, 0, "", fmt.Errorf("invalid funnel ID: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
funnel, err := module.store.Get(ctx, uuid)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, "", err
|
return 0, 0, "", err
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunnel"
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,21 +19,36 @@ func NewStore(sqlstore sqlstore.SQLStore) traceFunnels.FunnelStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (store *store) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
func (store *store) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||||
_, err := store.
|
// Check if a funnel with the same name already exists in the organization
|
||||||
|
exists, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model((*traceFunnels.StorableFunnel)(nil)).
|
||||||
|
Where("name = ? AND org_id = ?", funnel.Name, funnel.OrgID.String()).
|
||||||
|
Exists(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check for existing funnel: %v", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return store.sqlstore.WrapAlreadyExistsErrf(nil, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnel.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = store.
|
||||||
sqlstore.
|
sqlstore.
|
||||||
BunDB().
|
BunDB().
|
||||||
NewInsert().
|
NewInsert().
|
||||||
Model(funnel).
|
Model(funnel).
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return store.sqlstore.WrapAlreadyExistsErrf(err, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnel.Name)
|
return fmt.Errorf("failed to create funnel: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves a funnel by ID
|
// Get retrieves a funnel by ID
|
||||||
func (store *store) Get(ctx context.Context, uuid valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
func (store *store) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
funnel := &traceFunnels.StorableFunnel{}
|
funnel := &traceFunnels.StorableFunnel{}
|
||||||
err := store.
|
err := store.
|
||||||
sqlstore.
|
sqlstore.
|
||||||
@ -41,7 +56,7 @@ func (store *store) Get(ctx context.Context, uuid valuer.UUID) (*traceFunnels.St
|
|||||||
NewSelect().
|
NewSelect().
|
||||||
Model(funnel).
|
Model(funnel).
|
||||||
Relation("CreatedByUser").
|
Relation("CreatedByUser").
|
||||||
Where("?TableAlias.id = ?", uuid).
|
Where("?TableAlias.id = ? AND ?TableAlias.org_id = ?", uuid.String(), orgID.String()).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get funnel: %v", err)
|
return nil, fmt.Errorf("failed to get funnel: %v", err)
|
||||||
@ -75,7 +90,7 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnel
|
|||||||
NewSelect().
|
NewSelect().
|
||||||
Model(&funnels).
|
Model(&funnels).
|
||||||
Relation("CreatedByUser").
|
Relation("CreatedByUser").
|
||||||
Where("?TableAlias.org_id = ?", orgID).
|
Where("?TableAlias.org_id = ?", orgID.String()).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list funnels: %v", err)
|
return nil, fmt.Errorf("failed to list funnels: %v", err)
|
||||||
@ -84,13 +99,14 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes a funnel by ID
|
// Delete removes a funnel by ID
|
||||||
func (store *store) Delete(ctx context.Context, uuid valuer.UUID) error {
|
func (store *store) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
|
||||||
_, err := store.
|
_, err := store.
|
||||||
sqlstore.
|
sqlstore.
|
||||||
BunDB().
|
BunDB().
|
||||||
NewDelete().
|
NewDelete().
|
||||||
Model((*traceFunnels.StorableFunnel)(nil)).
|
Model((*traceFunnels.StorableFunnel)(nil)).
|
||||||
Where("id = ?", uuid).Exec(ctx)
|
Where("id = ? AND org_id = ?", funnelID.String(), orgID.String()).
|
||||||
|
Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete funnel: %v", err)
|
return fmt.Errorf("failed to delete funnel: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2,26 +2,25 @@ package tracefunnel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunnel"
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Module defines the interface for trace funnel operations
|
// Module defines the interface for trace funnel operations
|
||||||
type Module interface {
|
type Module interface {
|
||||||
Create(ctx context.Context, timestamp int64, name string, userID string, orgID string) (*traceFunnels.StorableFunnel, error)
|
Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
|
||||||
|
|
||||||
Get(ctx context.Context, funnelID string) (*traceFunnels.StorableFunnel, error)
|
Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
|
||||||
|
|
||||||
Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID string) error
|
Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error
|
||||||
|
|
||||||
List(ctx context.Context, orgID string) ([]*traceFunnels.StorableFunnel, error)
|
List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error)
|
||||||
|
|
||||||
Delete(ctx context.Context, funnelID string) error
|
Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error
|
||||||
|
|
||||||
Save(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID string, orgID string) error
|
GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error)
|
||||||
|
|
||||||
GetFunnelMetadata(ctx context.Context, funnelID string) (int64, int64, string, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
|
@ -2,12 +2,13 @@ package tracefunneltest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunnel"
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@ -22,8 +23,8 @@ func (m *MockStore) Create(ctx context.Context, funnel *traceFunnels.StorableFun
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockStore) Get(ctx context.Context, uuid valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
func (m *MockStore) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
args := m.Called(ctx, uuid)
|
args := m.Called(ctx, uuid, orgID)
|
||||||
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,8 +38,8 @@ func (m *MockStore) Update(ctx context.Context, funnel *traceFunnels.StorableFun
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockStore) Delete(ctx context.Context, uuid valuer.UUID) error {
|
func (m *MockStore) Delete(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) error {
|
||||||
args := m.Called(ctx, uuid)
|
args := m.Called(ctx, uuid, orgID)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,23 +51,23 @@ func TestModule_Create(t *testing.T) {
|
|||||||
timestamp := time.Now().UnixMilli()
|
timestamp := time.Now().UnixMilli()
|
||||||
name := "test-funnel"
|
name := "test-funnel"
|
||||||
userID := valuer.GenerateUUID()
|
userID := valuer.GenerateUUID()
|
||||||
orgID := valuer.GenerateUUID().String()
|
orgID := valuer.GenerateUUID()
|
||||||
|
|
||||||
mockStore.On("Create", ctx, mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
|
mockStore.On("Create", ctx, mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
|
||||||
return f.Name == name &&
|
return f.Name == name &&
|
||||||
f.CreatedBy == userID.String() &&
|
f.CreatedBy == userID.String() &&
|
||||||
f.OrgID.String() == orgID &&
|
f.OrgID == orgID &&
|
||||||
f.CreatedByUser != nil &&
|
f.CreatedByUser != nil &&
|
||||||
f.CreatedByUser.ID == userID &&
|
f.CreatedByUser.ID == userID &&
|
||||||
f.CreatedAt.UnixNano()/1000000 == timestamp
|
f.CreatedAt.UnixNano()/1000000 == timestamp
|
||||||
})).Return(nil)
|
})).Return(nil)
|
||||||
|
|
||||||
funnel, err := module.Create(ctx, timestamp, name, userID.String(), orgID)
|
funnel, err := module.Create(ctx, timestamp, name, userID, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, funnel)
|
assert.NotNil(t, funnel)
|
||||||
assert.Equal(t, name, funnel.Name)
|
assert.Equal(t, name, funnel.Name)
|
||||||
assert.Equal(t, userID.String(), funnel.CreatedBy)
|
assert.Equal(t, userID.String(), funnel.CreatedBy)
|
||||||
assert.Equal(t, orgID, funnel.OrgID.String())
|
assert.Equal(t, orgID, funnel.OrgID)
|
||||||
assert.NotNil(t, funnel.CreatedByUser)
|
assert.NotNil(t, funnel.CreatedByUser)
|
||||||
assert.Equal(t, userID, funnel.CreatedByUser.ID)
|
assert.Equal(t, userID, funnel.CreatedByUser.ID)
|
||||||
|
|
||||||
@ -78,16 +79,15 @@ func TestModule_Get(t *testing.T) {
|
|||||||
module := impltracefunnel.NewModule(mockStore)
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
funnelID := valuer.GenerateUUID().String()
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
expectedFunnel := &traceFunnels.StorableFunnel{
|
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Name: "test-funnel",
|
||||||
Name: "test-funnel",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mockStore.On("Get", ctx, mock.AnythingOfType("valuer.UUID")).Return(expectedFunnel, nil)
|
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
|
||||||
|
|
||||||
funnel, err := module.Get(ctx, funnelID)
|
funnel, err := module.Get(ctx, funnelID, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expectedFunnel, funnel)
|
assert.Equal(t, expectedFunnel, funnel)
|
||||||
|
|
||||||
@ -99,18 +99,16 @@ func TestModule_Update(t *testing.T) {
|
|||||||
module := impltracefunnel.NewModule(mockStore)
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userID := "user-123"
|
userID := valuer.GenerateUUID()
|
||||||
funnel := &traceFunnels.StorableFunnel{
|
funnel := &traceFunnels.StorableFunnel{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Name: "test-funnel",
|
||||||
Name: "test-funnel",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mockStore.On("Update", ctx, funnel).Return(nil)
|
mockStore.On("Update", ctx, funnel).Return(nil)
|
||||||
|
|
||||||
err := module.Update(ctx, funnel, userID)
|
err := module.Update(ctx, funnel, userID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userID, funnel.UpdatedBy)
|
assert.Equal(t, userID.String(), funnel.UpdatedBy)
|
||||||
|
|
||||||
mockStore.AssertExpectations(t)
|
mockStore.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
@ -120,24 +118,19 @@ func TestModule_List(t *testing.T) {
|
|||||||
module := impltracefunnel.NewModule(mockStore)
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
orgID := valuer.GenerateUUID().String()
|
orgID := valuer.GenerateUUID()
|
||||||
orgUUID := valuer.MustNewUUID(orgID)
|
|
||||||
expectedFunnels := []*traceFunnels.StorableFunnel{
|
expectedFunnels := []*traceFunnels.StorableFunnel{
|
||||||
{
|
{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Name: "funnel-1",
|
||||||
Name: "funnel-1",
|
OrgID: orgID,
|
||||||
OrgID: orgUUID,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Name: "funnel-2",
|
||||||
Name: "funnel-2",
|
OrgID: orgID,
|
||||||
OrgID: orgUUID,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
mockStore.On("List", ctx, orgUUID).Return(expectedFunnels, nil)
|
mockStore.On("List", ctx, orgID).Return(expectedFunnels, nil)
|
||||||
|
|
||||||
funnels, err := module.List(ctx, orgID)
|
funnels, err := module.List(ctx, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -152,59 +145,36 @@ func TestModule_Delete(t *testing.T) {
|
|||||||
module := impltracefunnel.NewModule(mockStore)
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
funnelID := valuer.GenerateUUID().String()
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
|
||||||
mockStore.On("Delete", ctx, mock.AnythingOfType("valuer.UUID")).Return(nil)
|
mockStore.On("Delete", ctx, funnelID, orgID).Return(nil)
|
||||||
|
|
||||||
err := module.Delete(ctx, funnelID)
|
err := module.Delete(ctx, funnelID, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
mockStore.AssertExpectations(t)
|
mockStore.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestModule_Save(t *testing.T) {
|
|
||||||
mockStore := new(MockStore)
|
|
||||||
module := impltracefunnel.NewModule(mockStore)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
userID := "user-123"
|
|
||||||
orgID := valuer.GenerateUUID().String()
|
|
||||||
funnel := &traceFunnels.StorableFunnel{
|
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
|
||||||
Name: "test-funnel",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockStore.On("Update", ctx, funnel).Return(nil)
|
|
||||||
|
|
||||||
err := module.Save(ctx, funnel, userID, orgID)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, userID, funnel.UpdatedBy)
|
|
||||||
assert.Equal(t, orgID, funnel.OrgID.String())
|
|
||||||
|
|
||||||
mockStore.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestModule_GetFunnelMetadata(t *testing.T) {
|
func TestModule_GetFunnelMetadata(t *testing.T) {
|
||||||
mockStore := new(MockStore)
|
mockStore := new(MockStore)
|
||||||
module := impltracefunnel.NewModule(mockStore)
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
funnelID := valuer.GenerateUUID().String()
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
expectedFunnel := &traceFunnels.StorableFunnel{
|
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||||
BaseMetadata: traceFunnels.BaseMetadata{
|
Description: "test description",
|
||||||
Description: "test description",
|
TimeAuditable: types.TimeAuditable{
|
||||||
TimeAuditable: types.TimeAuditable{
|
CreatedAt: now,
|
||||||
CreatedAt: now,
|
UpdatedAt: now,
|
||||||
UpdatedAt: now,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
mockStore.On("Get", ctx, mock.AnythingOfType("valuer.UUID")).Return(expectedFunnel, nil)
|
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
|
||||||
|
|
||||||
createdAt, updatedAt, description, err := module.GetFunnelMetadata(ctx, funnelID)
|
createdAt, updatedAt, description, err := module.GetFunnelMetadata(ctx, funnelID, orgID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, now.UnixNano()/1000000, createdAt)
|
assert.Equal(t, now.UnixNano()/1000000, createdAt)
|
||||||
assert.Equal(t, now.UnixNano()/1000000, updatedAt)
|
assert.Equal(t, now.UnixNano()/1000000, updatedAt)
|
||||||
|
@ -5233,19 +5233,15 @@ func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
// RegisterTraceFunnelsRoutes adds trace funnels routes
|
// RegisterTraceFunnelsRoutes adds trace funnels routes
|
||||||
func (aH *APIHandler) RegisterTraceFunnelsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
func (aH *APIHandler) RegisterTraceFunnelsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||||
// Main trace funnels router
|
// Main trace funnels router
|
||||||
traceFunnelsRouter := router.PathPrefix("/api/v1/trace-funnels").Subrouter()
|
traceFunnelsRouter := router.PathPrefix("/api/v1/orgs/me/trace-funnels").Subrouter()
|
||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
traceFunnelsRouter.HandleFunc("/new",
|
traceFunnelsRouter.HandleFunc("",
|
||||||
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.New)).
|
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.New)).
|
||||||
Methods(http.MethodPost)
|
Methods(http.MethodPost)
|
||||||
traceFunnelsRouter.HandleFunc("/list",
|
traceFunnelsRouter.HandleFunc("",
|
||||||
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.List)).
|
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.List)).
|
||||||
Methods(http.MethodGet)
|
Methods(http.MethodGet)
|
||||||
traceFunnelsRouter.HandleFunc("/steps/update",
|
|
||||||
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.UpdateSteps)).
|
|
||||||
Methods(http.MethodPut)
|
|
||||||
|
|
||||||
traceFunnelsRouter.HandleFunc("/{funnel_id}",
|
traceFunnelsRouter.HandleFunc("/{funnel_id}",
|
||||||
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.Get)).
|
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.Get)).
|
||||||
Methods(http.MethodGet)
|
Methods(http.MethodGet)
|
||||||
|
@ -3,6 +3,7 @@ package sqlmigration
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
@ -12,19 +13,15 @@ import (
|
|||||||
"github.com/uptrace/bun/migrate"
|
"github.com/uptrace/bun/migrate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BaseMetadata struct {
|
// Funnel Core Data Structure (Funnel and FunnelStep)
|
||||||
|
type Funnel struct {
|
||||||
|
bun.BaseModel `bun:"table:trace_funnel"`
|
||||||
types.Identifiable // funnel id
|
types.Identifiable // funnel id
|
||||||
types.TimeAuditable
|
types.TimeAuditable
|
||||||
types.UserAuditable
|
types.UserAuditable
|
||||||
Name string `json:"funnel_name" bun:"name,type:text,notnull"` // funnel name
|
Name string `json:"funnel_name" bun:"name,type:text,notnull"` // funnel name
|
||||||
Description string `json:"description" bun:"description,type:text"` // funnel description
|
Description string `json:"description" bun:"description,type:text"` // funnel description
|
||||||
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
||||||
}
|
|
||||||
|
|
||||||
// Funnel Core Data Structure (Funnel and FunnelStep)
|
|
||||||
type Funnel struct {
|
|
||||||
bun.BaseModel `bun:"table:trace_funnel"`
|
|
||||||
BaseMetadata
|
|
||||||
Steps []FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
Steps []FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
||||||
Tags string `json:"tags" bun:"tags,type:text"`
|
Tags string `json:"tags" bun:"tags,type:text"`
|
||||||
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package tracefunnel
|
package tracefunneltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
type FunnelStore interface {
|
type FunnelStore interface {
|
||||||
Create(context.Context, *StorableFunnel) error
|
Create(context.Context, *StorableFunnel) error
|
||||||
Get(context.Context, valuer.UUID) (*StorableFunnel, error)
|
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableFunnel, error)
|
||||||
List(context.Context, valuer.UUID) ([]*StorableFunnel, error)
|
List(context.Context, valuer.UUID) ([]*StorableFunnel, error)
|
||||||
Update(context.Context, *StorableFunnel) error
|
Update(context.Context, *StorableFunnel) error
|
||||||
Delete(context.Context, valuer.UUID) error
|
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package tracefunnel
|
package tracefunneltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
@ -12,20 +12,18 @@ var (
|
|||||||
ErrFunnelAlreadyExists = errors.MustNewCode("funnel_already_exists")
|
ErrFunnelAlreadyExists = errors.MustNewCode("funnel_already_exists")
|
||||||
)
|
)
|
||||||
|
|
||||||
// BaseMetadata metadata for funnels
|
|
||||||
type BaseMetadata struct {
|
type BaseMetadata struct {
|
||||||
types.Identifiable // funnel id
|
|
||||||
types.TimeAuditable
|
|
||||||
types.UserAuditable
|
|
||||||
Name string `json:"funnel_name" bun:"name,type:text,notnull"` // funnel name
|
|
||||||
Description string `json:"description" bun:"description,type:text"` // funnel description
|
|
||||||
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorableFunnel Core Data Structure (StorableFunnel and FunnelStep)
|
// StorableFunnel Core Data Structure (StorableFunnel and FunnelStep)
|
||||||
type StorableFunnel struct {
|
type StorableFunnel struct {
|
||||||
|
types.Identifiable
|
||||||
|
types.TimeAuditable
|
||||||
|
types.UserAuditable
|
||||||
bun.BaseModel `bun:"table:trace_funnel"`
|
bun.BaseModel `bun:"table:trace_funnel"`
|
||||||
BaseMetadata
|
Name string `json:"funnel_name" bun:"name,type:text,notnull"`
|
||||||
|
Description string `json:"description" bun:"description,type:text"`
|
||||||
|
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
||||||
Steps []*FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
Steps []*FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
||||||
Tags string `json:"tags" bun:"tags,type:text"`
|
Tags string `json:"tags" bun:"tags,type:text"`
|
||||||
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
||||||
@ -84,8 +82,8 @@ type TimeRange struct {
|
|||||||
// StepTransitionRequest represents a request for step transition analytics
|
// StepTransitionRequest represents a request for step transition analytics
|
||||||
type StepTransitionRequest struct {
|
type StepTransitionRequest struct {
|
||||||
TimeRange
|
TimeRange
|
||||||
StepAOrder int64 `json:"step_start,omitempty"`
|
StepStart int64 `json:"step_start,omitempty"`
|
||||||
StepBOrder int64 `json:"step_end,omitempty"`
|
StepEnd int64 `json:"step_end,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserInfo represents basic user information
|
// UserInfo represents basic user information
|
@ -1,4 +1,4 @@
|
|||||||
package tracefunnel
|
package tracefunneltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -48,14 +48,33 @@ func ValidateFunnelSteps(steps []*FunnelStep) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeFunnelSteps normalizes step orders to be sequential starting from 1.
|
// NormalizeFunnelSteps normalizes step orders to be sequential starting from 1.
|
||||||
// Returns a new slice with normalized step orders, leaving the input slice unchanged.
|
// The function takes a slice of pointers to FunnelStep and returns a new slice with normalized step orders.
|
||||||
|
// The input slice is left unchanged. If the input slice contains nil pointers, they will be filtered out.
|
||||||
|
// Returns an empty slice if the input is empty or contains only nil pointers.
|
||||||
func NormalizeFunnelSteps(steps []*FunnelStep) []*FunnelStep {
|
func NormalizeFunnelSteps(steps []*FunnelStep) []*FunnelStep {
|
||||||
if len(steps) == 0 {
|
if len(steps) == 0 {
|
||||||
return []*FunnelStep{}
|
return []*FunnelStep{}
|
||||||
}
|
}
|
||||||
|
|
||||||
newSteps := make([]*FunnelStep, len(steps))
|
// Filter out nil pointers and create a new slice
|
||||||
copy(newSteps, steps)
|
validSteps := make([]*FunnelStep, 0, len(steps))
|
||||||
|
for _, step := range steps {
|
||||||
|
if step != nil {
|
||||||
|
validSteps = append(validSteps, step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validSteps) == 0 {
|
||||||
|
return []*FunnelStep{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a defensive copy of the valid steps
|
||||||
|
newSteps := make([]*FunnelStep, len(validSteps))
|
||||||
|
for i, step := range validSteps {
|
||||||
|
// Create a copy of each step to avoid modifying the original
|
||||||
|
stepCopy := *step
|
||||||
|
newSteps[i] = &stepCopy
|
||||||
|
}
|
||||||
|
|
||||||
sort.Slice(newSteps, func(i, j int) bool {
|
sort.Slice(newSteps, func(i, j int) bool {
|
||||||
return newSteps[i].Order < newSteps[j].Order
|
return newSteps[i].Order < newSteps[j].Order
|
@ -1,4 +1,4 @@
|
|||||||
package tracefunnel
|
package tracefunneltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -90,12 +90,12 @@ func TestValidateTimestampIsMilliseconds(t *testing.T) {
|
|||||||
func TestValidateFunnelSteps(t *testing.T) {
|
func TestValidateFunnelSteps(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
steps []FunnelStep
|
steps []*FunnelStep
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid steps",
|
name: "valid steps",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
@ -115,7 +115,7 @@ func TestValidateFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "too few steps",
|
name: "too few steps",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
@ -128,7 +128,7 @@ func TestValidateFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing service name",
|
name: "missing service name",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
@ -147,7 +147,7 @@ func TestValidateFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing span name",
|
name: "missing span name",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
@ -166,7 +166,7 @@ func TestValidateFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "negative order",
|
name: "negative order",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
@ -201,12 +201,12 @@ func TestValidateFunnelSteps(t *testing.T) {
|
|||||||
func TestNormalizeFunnelSteps(t *testing.T) {
|
func TestNormalizeFunnelSteps(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
steps []FunnelStep
|
steps []*FunnelStep
|
||||||
expected []FunnelStep
|
expected []*FunnelStep
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "already normalized steps",
|
name: "already normalized steps",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
@ -222,7 +222,7 @@ func TestNormalizeFunnelSteps(t *testing.T) {
|
|||||||
Order: 2,
|
Order: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: []FunnelStep{
|
expected: []*FunnelStep{
|
||||||
{
|
{
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
ServiceName: "test-service",
|
ServiceName: "test-service",
|
||||||
@ -239,7 +239,7 @@ func TestNormalizeFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unordered steps",
|
name: "unordered steps",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Name: "Step 2",
|
Name: "Step 2",
|
||||||
@ -255,7 +255,7 @@ func TestNormalizeFunnelSteps(t *testing.T) {
|
|||||||
Order: 1,
|
Order: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: []FunnelStep{
|
expected: []*FunnelStep{
|
||||||
{
|
{
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
ServiceName: "test-service",
|
ServiceName: "test-service",
|
||||||
@ -272,7 +272,7 @@ func TestNormalizeFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "steps with gaps in order",
|
name: "steps with gaps in order",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
@ -295,7 +295,7 @@ func TestNormalizeFunnelSteps(t *testing.T) {
|
|||||||
Order: 2,
|
Order: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: []FunnelStep{
|
expected: []*FunnelStep{
|
||||||
{
|
{
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
ServiceName: "test-service",
|
ServiceName: "test-service",
|
||||||
@ -316,17 +316,58 @@ func TestNormalizeFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "steps with nil pointers",
|
||||||
|
steps: []*FunnelStep{
|
||||||
|
{
|
||||||
|
ID: valuer.GenerateUUID(),
|
||||||
|
Name: "Step 1",
|
||||||
|
ServiceName: "test-service",
|
||||||
|
SpanName: "test-span",
|
||||||
|
Order: 1,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
{
|
||||||
|
ID: valuer.GenerateUUID(),
|
||||||
|
Name: "Step 2",
|
||||||
|
ServiceName: "test-service",
|
||||||
|
SpanName: "test-span-2",
|
||||||
|
Order: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []*FunnelStep{
|
||||||
|
{
|
||||||
|
Name: "Step 1",
|
||||||
|
ServiceName: "test-service",
|
||||||
|
SpanName: "test-span",
|
||||||
|
Order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Step 2",
|
||||||
|
ServiceName: "test-service",
|
||||||
|
SpanName: "test-span-2",
|
||||||
|
Order: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty steps",
|
||||||
|
steps: []*FunnelStep{},
|
||||||
|
expected: []*FunnelStep{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all nil steps",
|
||||||
|
steps: []*FunnelStep{nil, nil},
|
||||||
|
expected: []*FunnelStep{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Make a copy of the steps to avoid modifying the original
|
result := NormalizeFunnelSteps(tt.steps)
|
||||||
steps := make([]FunnelStep, len(tt.steps))
|
|
||||||
copy(steps, tt.steps)
|
|
||||||
|
|
||||||
result := NormalizeFunnelSteps(steps)
|
|
||||||
|
|
||||||
// Compare only the relevant fields
|
// Compare only the relevant fields
|
||||||
|
assert.Len(t, result, len(tt.expected))
|
||||||
for i := range result {
|
for i := range result {
|
||||||
assert.Equal(t, tt.expected[i].Name, result[i].Name)
|
assert.Equal(t, tt.expected[i].Name, result[i].Name)
|
||||||
assert.Equal(t, tt.expected[i].ServiceName, result[i].ServiceName)
|
assert.Equal(t, tt.expected[i].ServiceName, result[i].ServiceName)
|
||||||
@ -425,6 +466,7 @@ func TestConstructFunnelResponse(t *testing.T) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
funnelID := valuer.GenerateUUID()
|
funnelID := valuer.GenerateUUID()
|
||||||
orgID := valuer.GenerateUUID()
|
orgID := valuer.GenerateUUID()
|
||||||
|
userID := valuer.GenerateUUID()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -435,24 +477,24 @@ func TestConstructFunnelResponse(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "with user email from funnel",
|
name: "with user email from funnel",
|
||||||
funnel: &StorableFunnel{
|
funnel: &StorableFunnel{
|
||||||
BaseMetadata: BaseMetadata{
|
Identifiable: types.Identifiable{
|
||||||
Identifiable: types.Identifiable{
|
ID: funnelID,
|
||||||
ID: funnelID,
|
|
||||||
},
|
|
||||||
Name: "test-funnel",
|
|
||||||
OrgID: orgID,
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
},
|
|
||||||
UserAuditable: types.UserAuditable{
|
|
||||||
CreatedBy: "user-123",
|
|
||||||
UpdatedBy: "user-123",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
TimeAuditable: types.TimeAuditable{
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
},
|
||||||
|
UserAuditable: types.UserAuditable{
|
||||||
|
CreatedBy: userID.String(),
|
||||||
|
UpdatedBy: userID.String(),
|
||||||
|
},
|
||||||
|
Name: "test-funnel",
|
||||||
|
OrgID: orgID,
|
||||||
CreatedByUser: &types.User{
|
CreatedByUser: &types.User{
|
||||||
Identifiable: types.Identifiable{ID: valuer.MustNewUUID("user-123")},
|
Identifiable: types.Identifiable{
|
||||||
Email: "funnel@example.com",
|
ID: userID,
|
||||||
|
},
|
||||||
|
Email: "funnel@example.com",
|
||||||
},
|
},
|
||||||
Steps: []*FunnelStep{
|
Steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
@ -465,7 +507,7 @@ func TestConstructFunnelResponse(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
claims: &authtypes.Claims{
|
claims: &authtypes.Claims{
|
||||||
UserID: "user-123",
|
UserID: userID.String(),
|
||||||
OrgID: orgID.String(),
|
OrgID: orgID.String(),
|
||||||
Email: "claims@example.com",
|
Email: "claims@example.com",
|
||||||
},
|
},
|
||||||
@ -481,9 +523,9 @@ func TestConstructFunnelResponse(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
CreatedAt: now.UnixNano() / 1000000,
|
CreatedAt: now.UnixNano() / 1000000,
|
||||||
CreatedBy: "user-123",
|
CreatedBy: userID.String(),
|
||||||
UpdatedAt: now.UnixNano() / 1000000,
|
UpdatedAt: now.UnixNano() / 1000000,
|
||||||
UpdatedBy: "user-123",
|
UpdatedBy: userID.String(),
|
||||||
OrgID: orgID.String(),
|
OrgID: orgID.String(),
|
||||||
UserEmail: "funnel@example.com",
|
UserEmail: "funnel@example.com",
|
||||||
},
|
},
|
||||||
@ -491,21 +533,19 @@ func TestConstructFunnelResponse(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "with user email from claims",
|
name: "with user email from claims",
|
||||||
funnel: &StorableFunnel{
|
funnel: &StorableFunnel{
|
||||||
BaseMetadata: BaseMetadata{
|
Identifiable: types.Identifiable{
|
||||||
Identifiable: types.Identifiable{
|
ID: funnelID,
|
||||||
ID: funnelID,
|
|
||||||
},
|
|
||||||
Name: "test-funnel",
|
|
||||||
OrgID: orgID,
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
},
|
|
||||||
UserAuditable: types.UserAuditable{
|
|
||||||
CreatedBy: "user-123",
|
|
||||||
UpdatedBy: "user-123",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
TimeAuditable: types.TimeAuditable{
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
},
|
||||||
|
UserAuditable: types.UserAuditable{
|
||||||
|
CreatedBy: userID.String(),
|
||||||
|
UpdatedBy: userID.String(),
|
||||||
|
},
|
||||||
|
Name: "test-funnel",
|
||||||
|
OrgID: orgID,
|
||||||
Steps: []*FunnelStep{
|
Steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
@ -517,7 +557,7 @@ func TestConstructFunnelResponse(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
claims: &authtypes.Claims{
|
claims: &authtypes.Claims{
|
||||||
UserID: "user-123",
|
UserID: userID.String(),
|
||||||
OrgID: orgID.String(),
|
OrgID: orgID.String(),
|
||||||
Email: "claims@example.com",
|
Email: "claims@example.com",
|
||||||
},
|
},
|
||||||
@ -533,9 +573,9 @@ func TestConstructFunnelResponse(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
CreatedAt: now.UnixNano() / 1000000,
|
CreatedAt: now.UnixNano() / 1000000,
|
||||||
CreatedBy: "user-123",
|
CreatedBy: userID.String(),
|
||||||
UpdatedAt: now.UnixNano() / 1000000,
|
UpdatedAt: now.UnixNano() / 1000000,
|
||||||
UpdatedBy: "user-123",
|
UpdatedBy: userID.String(),
|
||||||
OrgID: orgID.String(),
|
OrgID: orgID.String(),
|
||||||
UserEmail: "claims@example.com",
|
UserEmail: "claims@example.com",
|
||||||
},
|
},
|
||||||
@ -572,12 +612,12 @@ func TestConstructFunnelResponse(t *testing.T) {
|
|||||||
func TestProcessFunnelSteps(t *testing.T) {
|
func TestProcessFunnelSteps(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
steps []FunnelStep
|
steps []*FunnelStep
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid steps with missing IDs",
|
name: "valid steps with missing IDs",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
ServiceName: "test-service",
|
ServiceName: "test-service",
|
||||||
@ -595,7 +635,7 @@ func TestProcessFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid steps - missing service name",
|
name: "invalid steps - missing service name",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
SpanName: "test-span",
|
SpanName: "test-span",
|
||||||
@ -612,7 +652,7 @@ func TestProcessFunnelSteps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid steps - negative order",
|
name: "invalid steps - negative order",
|
||||||
steps: []FunnelStep{
|
steps: []*FunnelStep{
|
||||||
{
|
{
|
||||||
Name: "Step 1",
|
Name: "Step 1",
|
||||||
ServiceName: "test-service",
|
ServiceName: "test-service",
|
Loading…
x
Reference in New Issue
Block a user