Compare commits

...

2 Commits

Author SHA1 Message Date
Cursor Agent
76715c82a9 Fix: Add created_by column selection and set CreatedByName for all history items
- Add COALESCE(created_by, '') as created_by to ColumnExpr in GetConfigHistory, GetConfigVersion, and GetLatestVersion to ensure created_by is selected
- Move CreatedByName assignment outside the idx=1 loop to include index 0 (most recent config version)
2026-03-12 07:12:42 +00:00
vikrantgupta25
933d1e0e85 chore(auditable): cleanup the user auditables with emails instead of ids 2026-03-12 12:37:46 +05:30
16 changed files with 282 additions and 629 deletions

View File

@@ -21,7 +21,7 @@ func NewHandler(module tracefunnel.Module) tracefunnel.Handler {
return &handler{module: module}
}
func (handler *handler) New(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
var req tf.PostableFunnel
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
@@ -34,7 +34,7 @@ func (handler *handler) New(rw http.ResponseWriter, r *http.Request) {
return
}
funnel, err := handler.module.Create(r.Context(), req.Timestamp, req.Name, valuer.MustNewUUID(claims.UserID), valuer.MustNewUUID(claims.OrgID))
funnel, err := handler.module.Create(r.Context(), req.Name, claims.Email, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
@@ -42,7 +42,7 @@ func (handler *handler) New(rw http.ResponseWriter, r *http.Request) {
return
}
response := tf.ConstructFunnelResponse(funnel, &claims)
response := tf.ConstructFunnelResponse(funnel)
render.Success(rw, http.StatusOK, response)
}
@@ -59,12 +59,6 @@ func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
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,
@@ -79,33 +73,15 @@ func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
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 {
funnel.Update(req.Name, req.Description, steps, claims.Email)
if err := handler.module.Update(r.Context(), funnel); 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)
response := tf.ConstructFunnelResponse(funnel)
render.Success(rw, http.StatusOK, response)
}
@@ -122,12 +98,6 @@ func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
return
}
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
if err != nil {
render.Error(rw, err)
return
}
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
@@ -139,32 +109,15 @@ func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
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 {
funnel.Update(req.Name, req.Description, nil, claims.Email)
if err := handler.module.Update(r.Context(), funnel); 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)
response := tf.ConstructFunnelResponse(funnel)
render.Success(rw, http.StatusOK, response)
}
@@ -185,7 +138,7 @@ func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
var response []tf.GettableFunnel
for _, f := range funnels {
response = append(response, tf.ConstructFunnelResponse(f, &claims))
response = append(response, tf.ConstructFunnelResponse(f))
}
render.Success(rw, http.StatusOK, response)
@@ -209,7 +162,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
"funnel not found: %v", err))
return
}
response := tf.ConstructFunnelResponse(funnel, &claims)
response := tf.ConstructFunnelResponse(funnel)
render.Success(rw, http.StatusOK, response)
}

View File

@@ -1,173 +0,0 @@
package impltracefunnel
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockModule struct {
mock.Mock
}
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)
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockModule) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, funnelID, orgID)
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockModule) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
args := m.Called(ctx, funnel, userID)
return args.Error(0)
}
func (m *MockModule) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockModule) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
args := m.Called(ctx, funnelID, orgID)
return args.Error(0)
}
func (m *MockModule) Save(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID, orgID valuer.UUID) error {
args := m.Called(ctx, funnel, userID, orgID)
return args.Error(0)
}
func (m *MockModule) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
args := m.Called(ctx, funnelID, orgID)
return args.Get(0).(int64), args.Get(1).(int64), args.String(2), args.Error(3)
}
func TestHandler_List(t *testing.T) {
mockModule := new(MockModule)
handler := NewHandler(mockModule)
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/list", nil)
orgID := valuer.GenerateUUID()
claims := authtypes.Claims{
OrgID: orgID.String(),
}
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
rr := httptest.NewRecorder()
funnel1ID := valuer.GenerateUUID()
funnel2ID := valuer.GenerateUUID()
expectedFunnels := []*traceFunnels.StorableFunnel{
{
Identifiable: types.Identifiable{
ID: funnel1ID,
},
Name: "funnel-1",
OrgID: orgID,
},
{
Identifiable: types.Identifiable{
ID: funnel2ID,
},
Name: "funnel-2",
OrgID: orgID,
},
}
mockModule.On("List", req.Context(), orgID).Return(expectedFunnels, nil)
handler.List(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.Len(t, response.Data, 2)
assert.Equal(t, "funnel-1", response.Data[0].FunnelName)
assert.Equal(t, "funnel-2", response.Data[1].FunnelName)
mockModule.AssertExpectations(t)
}
func TestHandler_Get(t *testing.T) {
mockModule := new(MockModule)
handler := NewHandler(mockModule)
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/"+funnelID.String(), nil)
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()
expectedFunnel := &traceFunnels.StorableFunnel{
Identifiable: types.Identifiable{
ID: funnelID,
},
Name: "test-funnel",
OrgID: orgID,
}
mockModule.On("Get", req.Context(), funnelID, orgID).Return(expectedFunnel, nil)
handler.Get(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, "test-funnel", response.Data.FunnelName)
assert.Equal(t, expectedFunnel.OrgID.String(), response.Data.OrgID)
mockModule.AssertExpectations(t)
}
func TestHandler_Delete(t *testing.T) {
mockModule := new(MockModule)
handler := NewHandler(mockModule)
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
req := httptest.NewRequest(http.MethodDelete, "/api/v1/trace-funnels/"+funnelID.String(), nil)
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()
mockModule.On("Delete", req.Context(), funnelID, orgID).Return(nil)
handler.Delete(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
mockModule.AssertExpectations(t)
}

View File

@@ -2,11 +2,10 @@ package impltracefunnel
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -21,58 +20,25 @@ func NewModule(store traceFunnels.FunnelStore) tracefunnel.Module {
}
}
func (module *module) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
funnel := &traceFunnels.StorableFunnel{
Name: name,
OrgID: orgID,
}
funnel.CreatedAt = time.Unix(0, timestamp*1000000) // Convert to nanoseconds
funnel.CreatedBy = userID.String()
// Set up the user relationship
funnel.CreatedByUser = &types.User{
Identifiable: types.Identifiable{
ID: userID,
},
}
if funnel.ID.IsZero() {
funnel.ID = valuer.GenerateUUID()
}
if funnel.CreatedAt.IsZero() {
funnel.CreatedAt = time.Now()
}
if funnel.UpdatedAt.IsZero() {
funnel.UpdatedAt = time.Now()
}
// Set created_by if CreatedByUser is present
if funnel.CreatedByUser != nil {
funnel.CreatedBy = funnel.CreatedByUser.Identifiable.ID.String()
}
err := module.store.Create(ctx, funnel)
func (module *module) Create(ctx context.Context, name string, createdBy string, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
storable := tracefunneltypes.NewStorableFunnel(name, "", nil, "", createdBy, orgID)
err := module.store.Create(ctx, storable)
if err != nil {
return nil, err
}
return funnel, nil
return storable, nil
}
// Get gets a funnel by ID
func (module *module) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
return module.store.Get(ctx, funnelID, orgID)
func (module *module) Get(ctx context.Context, id valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
return module.store.Get(ctx, id, orgID)
}
// Update updates a funnel
func (module *module) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
funnel.UpdatedBy = userID.String()
func (module *module) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
return module.store.Update(ctx, funnel)
}
// List lists all funnels for an organization
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
funnels, err := module.store.List(ctx, orgID)
if err != nil {
@@ -82,14 +48,12 @@ func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunn
return funnels, nil
}
// Delete deletes a funnel
func (module *module) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
return module.store.Delete(ctx, funnelID, orgID)
func (module *module) Delete(ctx context.Context, id valuer.UUID, orgID valuer.UUID) error {
return module.store.Delete(ctx, id, orgID)
}
// GetFunnelMetadata gets metadata for a funnel
func (module *module) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
funnel, err := module.store.Get(ctx, funnelID, orgID)
func (module *module) GetFunnelMetadata(ctx context.Context, id valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
funnel, err := module.store.Get(ctx, id, orgID)
if err != nil {
return 0, 0, "", err
}

View File

@@ -1,96 +0,0 @@
package impltracefunnel
import (
"context"
"fmt"
"testing"
"github.com/SigNoz/signoz/pkg/errors"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Test that Create method properly validates duplicate names
func TestModule_Create_DuplicateNameValidation(t *testing.T) {
mockStore := new(MockStore)
module := NewModule(mockStore)
ctx := context.Background()
timestamp := int64(1234567890)
name := "Duplicate Funnel"
userID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
// Mock store to return "already exists" error
expectedErr := errors.Wrapf(nil, errors.TypeAlreadyExists, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", name)
mockStore.On("Create", ctx, mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
return f.Name == name && f.OrgID == orgID
})).Return(expectedErr)
funnel, err := module.Create(ctx, timestamp, name, userID, orgID)
assert.Error(t, err)
assert.Nil(t, funnel)
assert.Contains(t, err.Error(), fmt.Sprintf("a funnel with name '%s' already exists in this organization", name))
mockStore.AssertExpectations(t)
}
// Test that Update method properly validates duplicate names
func TestModule_Update_DuplicateNameValidation(t *testing.T) {
mockStore := new(MockStore)
module := NewModule(mockStore)
ctx := context.Background()
userID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
funnelName := "Duplicate Name"
funnel := &traceFunnels.StorableFunnel{
Name: funnelName,
OrgID: orgID,
}
funnel.ID = valuer.GenerateUUID()
// Mock store to return "already exists" error
expectedErr := errors.Wrapf(nil, errors.TypeAlreadyExists, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnelName)
mockStore.On("Update", ctx, funnel).Return(expectedErr)
err := module.Update(ctx, funnel, userID)
assert.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("a funnel with name '%s' already exists in this organization", funnelName))
assert.Equal(t, userID.String(), funnel.UpdatedBy) // Should still set UpdatedBy
mockStore.AssertExpectations(t)
}
// MockStore for testing
type MockStore struct {
mock.Mock
}
func (m *MockStore) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
args := m.Called(ctx, funnel)
return args.Error(0)
}
func (m *MockStore) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, uuid, orgID)
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockStore) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockStore) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
args := m.Called(ctx, funnel)
return args.Error(0)
}
func (m *MockStore) Delete(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) error {
args := m.Called(ctx, uuid, orgID)
return args.Error(0)
}

View File

@@ -2,19 +2,20 @@ package tracefunnel
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
"net/http"
"github.com/SigNoz/signoz/pkg/valuer"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
)
// Module defines the interface for trace funnel operations
type Module interface {
Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
Create(ctx context.Context, name string, userID string, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error
Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error
List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error)
@@ -24,7 +25,7 @@ type Module interface {
}
type Handler interface {
New(http.ResponseWriter, *http.Request)
Create(http.ResponseWriter, *http.Request)
UpdateSteps(http.ResponseWriter, *http.Request)

View File

@@ -1,183 +0,0 @@
package tracefunneltest
import (
"context"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
"github.com/SigNoz/signoz/pkg/types"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockStore struct {
mock.Mock
}
func (m *MockStore) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
args := m.Called(ctx, funnel)
return args.Error(0)
}
func (m *MockStore) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, uuid, orgID)
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockStore) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockStore) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
args := m.Called(ctx, funnel)
return args.Error(0)
}
func (m *MockStore) Delete(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) error {
args := m.Called(ctx, uuid, orgID)
return args.Error(0)
}
func TestModule_Create(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
timestamp := time.Now().UnixMilli()
name := "test-funnel"
userID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
mockStore.On("Create", ctx, mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
return f.Name == name &&
f.CreatedBy == userID.String() &&
f.OrgID == orgID &&
f.CreatedByUser != nil &&
f.CreatedByUser.ID == userID &&
f.CreatedAt.UnixNano()/1000000 == timestamp
})).Return(nil)
funnel, err := module.Create(ctx, timestamp, name, userID, orgID)
assert.NoError(t, err)
assert.NotNil(t, funnel)
assert.Equal(t, name, funnel.Name)
assert.Equal(t, userID.String(), funnel.CreatedBy)
assert.Equal(t, orgID, funnel.OrgID)
assert.NotNil(t, funnel.CreatedByUser)
assert.Equal(t, userID, funnel.CreatedByUser.ID)
mockStore.AssertExpectations(t)
}
func TestModule_Get(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
expectedFunnel := &traceFunnels.StorableFunnel{
Name: "test-funnel",
}
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
funnel, err := module.Get(ctx, funnelID, orgID)
assert.NoError(t, err)
assert.Equal(t, expectedFunnel, funnel)
mockStore.AssertExpectations(t)
}
func TestModule_Update(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
userID := valuer.GenerateUUID()
funnel := &traceFunnels.StorableFunnel{
Name: "test-funnel",
}
mockStore.On("Update", ctx, funnel).Return(nil)
err := module.Update(ctx, funnel, userID)
assert.NoError(t, err)
assert.Equal(t, userID.String(), funnel.UpdatedBy)
mockStore.AssertExpectations(t)
}
func TestModule_List(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
orgID := valuer.GenerateUUID()
expectedFunnels := []*traceFunnels.StorableFunnel{
{
Name: "funnel-1",
OrgID: orgID,
},
{
Name: "funnel-2",
OrgID: orgID,
},
}
mockStore.On("List", ctx, orgID).Return(expectedFunnels, nil)
funnels, err := module.List(ctx, orgID)
assert.NoError(t, err)
assert.Len(t, funnels, 2)
assert.Equal(t, expectedFunnels, funnels)
mockStore.AssertExpectations(t)
}
func TestModule_Delete(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
mockStore.On("Delete", ctx, funnelID, orgID).Return(nil)
err := module.Delete(ctx, funnelID, orgID)
assert.NoError(t, err)
mockStore.AssertExpectations(t)
}
func TestModule_GetFunnelMetadata(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
now := time.Now()
expectedFunnel := &traceFunnels.StorableFunnel{
Description: "test description",
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
UpdatedAt: now,
},
}
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
createdAt, updatedAt, description, err := module.GetFunnelMetadata(ctx, funnelID, orgID)
assert.NoError(t, err)
assert.Equal(t, now.UnixNano()/1000000, createdAt)
assert.Equal(t, now.UnixNano()/1000000, updatedAt)
assert.Equal(t, "test description", description)
mockStore.AssertExpectations(t)
}

View File

@@ -40,7 +40,6 @@ func (r *Repo) GetConfigHistory(
Model(&c).
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at").
ColumnExpr("COALESCE(created_by, '') as created_by").
ColumnExpr(`COALESCE((SELECT display_name FROM users WHERE users.id = acv.created_by), 'unknown') as created_by_name`).
ColumnExpr("COALESCE(hash, '') as hash, COALESCE(config, '{}') as config").
Where("acv.element_type = ?", typ).
Where("acv.org_id = ?", orgId).
@@ -52,6 +51,10 @@ func (r *Repo) GetConfigHistory(
return nil, errors.WrapInternalf(err, CodeConfigHistoryGetFailed, "failed to get config history")
}
for idx := range c {
c[idx].CreatedByName = c[idx].CreatedBy
}
incompleteStatuses := []opamptypes.DeployStatus{opamptypes.DeployInitiated, opamptypes.Deploying}
for idx := 1; idx < len(c); idx++ {
if slices.Contains(incompleteStatuses, c[idx].DeployStatus) {
@@ -70,7 +73,6 @@ func (r *Repo) GetConfigVersion(
Model(&c).
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at").
ColumnExpr("COALESCE(created_by, '') as created_by").
ColumnExpr(`COALESCE((SELECT display_name FROM users WHERE users.id = acv.created_by), 'unknown') as created_by_name`).
ColumnExpr("COALESCE(hash, '') as hash, COALESCE(config, '{}') as config").
Where("acv.element_type = ?", typ).
Where("acv.version = ?", v).
@@ -84,6 +86,7 @@ func (r *Repo) GetConfigVersion(
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get config version")
}
c.CreatedByName = c.CreatedBy
return &c, nil
}
@@ -95,7 +98,6 @@ func (r *Repo) GetLatestVersion(
Model(&c).
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at").
ColumnExpr("COALESCE(created_by, '') as created_by").
ColumnExpr(`COALESCE((SELECT display_name FROM users WHERE users.id = acv.created_by), 'unknown') as created_by_name`).
Where("acv.element_type = ?", typ).
Where("acv.org_id = ?", orgId).
Where("version = (SELECT MAX(version) FROM agent_config_version WHERE acv.element_type = ?)", typ).
@@ -108,11 +110,12 @@ func (r *Repo) GetLatestVersion(
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get latest config version")
}
c.CreatedByName = c.CreatedBy
return &c, nil
}
func (r *Repo) insertConfig(
ctx context.Context, orgId valuer.UUID, userId valuer.UUID, c *opamptypes.AgentConfigVersion, elements []string,
ctx context.Context, orgId valuer.UUID, c *opamptypes.AgentConfigVersion, elements []string,
) error {
if c.ElementType.StringValue() == "" {

View File

@@ -198,14 +198,14 @@ func GetConfigHistory(
// StartNewVersion launches a new config version for given set of elements
func StartNewVersion(
ctx context.Context, orgId valuer.UUID, userId valuer.UUID, eleType opamptypes.ElementType, elementIds []string,
ctx context.Context, orgId valuer.UUID, createdBy string, eleType opamptypes.ElementType, elementIds []string,
) (*opamptypes.AgentConfigVersion, error) {
// create a new version
cfg := opamptypes.NewAgentConfigVersion(orgId, userId, eleType)
cfg := opamptypes.NewAgentConfigVersion(orgId, createdBy, eleType)
// insert new config and elements into database
err := m.insertConfig(ctx, orgId, userId, cfg, elementIds)
err := m.insertConfig(ctx, orgId, cfg, elementIds)
if err != nil {
return nil, err
}

View File

@@ -4237,14 +4237,8 @@ func (aH *APIHandler) CreateLogsPipeline(w http.ResponseWriter, r *http.Request)
render.Error(w, errv2)
return
}
userID, errv2 := valuer.NewUUID(claims.UserID)
if errv2 != nil {
render.Error(w, errv2)
return
}
req := pipelinetypes.PostablePipelines{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
@@ -4263,7 +4257,7 @@ func (aH *APIHandler) CreateLogsPipeline(w http.ResponseWriter, r *http.Request)
return nil, err
}
return aH.LogsParsingPipelineController.ApplyPipelines(ctx, orgID, userID, postable)
return aH.LogsParsingPipelineController.ApplyPipelines(ctx, orgID, claims.Email, postable)
}
res, err := createPipeline(r.Context(), req.Pipelines)
@@ -5138,7 +5132,7 @@ func (aH *APIHandler) RegisterTraceFunnelsRoutes(router *mux.Router, am *middlew
// API endpoints
traceFunnelsRouter.HandleFunc("/new",
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.New)).
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.Create)).
Methods(http.MethodPost)
traceFunnelsRouter.HandleFunc("/list",
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.List)).

View File

@@ -58,7 +58,7 @@ type PipelinesResponse struct {
func (ic *LogParsingPipelineController) ApplyPipelines(
ctx context.Context,
orgID valuer.UUID,
userID valuer.UUID,
createdBy string,
postable []pipelinetypes.PostablePipeline,
) (*PipelinesResponse, error) {
var pipelines []pipelinetypes.GettablePipeline
@@ -89,7 +89,7 @@ func (ic *LogParsingPipelineController) ApplyPipelines(
elements[i] = p.ID.StringValue()
}
cfg, err := agentConf.StartNewVersion(ctx, orgID, userID, opamptypes.ElementTypeLogPipelines, elements)
cfg, err := agentConf.StartNewVersion(ctx, orgID, createdBy, opamptypes.ElementTypeLogPipelines, elements)
if err != nil || cfg == nil {
return nil, model.InternalError(fmt.Errorf("failed to start new version: %w", err))
}

View File

@@ -172,6 +172,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewMigrateRulesV4ToV5Factory(sqlstore, telemetryStore),
sqlmigration.NewAddStatusUserFactory(sqlstore, sqlschema),
sqlmigration.NewDeprecateUserInviteFactory(sqlstore, sqlschema),
sqlmigration.NewUpdateCreatedByWithEmailFactory(sqlstore, sqlschema),
)
}

View File

@@ -0,0 +1,184 @@
package sqlmigration
import (
"context"
"database/sql"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type updateCreatedByWithEmail struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewUpdateCreatedByWithEmailFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("update_created_by_with_email"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &updateCreatedByWithEmail{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
},
)
}
func (migration *updateCreatedByWithEmail) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *updateCreatedByWithEmail) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
type userRow struct {
ID string `bun:"id"`
Email string `bun:"email"`
}
var users []userRow
err = tx.NewSelect().TableExpr("users").Column("id", "email").Scan(ctx, &users)
if err != nil && err != sql.ErrNoRows {
return err
}
userEmailMap := make(map[string]string, len(users))
for _, u := range users {
userEmailMap[u.ID] = u.Email
}
emails := make([]string, 0, len(userEmailMap))
for _, email := range userEmailMap {
emails = append(emails, email)
}
for id, email := range userEmailMap {
_, err = tx.NewUpdate().
TableExpr("agent_config_version").
Set("created_by = ?", email).
Where("created_by = ?", id).
Exec(ctx)
if err != nil {
return err
}
_, err = tx.NewUpdate().
TableExpr("agent_config_version").
Set("updated_by = ?", email).
Where("updated_by = ?", id).
Exec(ctx)
if err != nil {
return err
}
}
agentCreatedByQuery := tx.NewUpdate().
TableExpr("agent_config_version").
Set("created_by = ''").
Where("created_by != ''")
if len(emails) > 0 {
agentCreatedByQuery = agentCreatedByQuery.Where("created_by NOT IN (?)", bun.In(emails))
}
if _, err = agentCreatedByQuery.Exec(ctx); err != nil {
return err
}
agentUpdatedByQuery := tx.NewUpdate().
TableExpr("agent_config_version").
Set("updated_by = ''").
Where("updated_by != ''")
if len(emails) > 0 {
agentUpdatedByQuery = agentUpdatedByQuery.Where("updated_by NOT IN (?)", bun.In(emails))
}
if _, err = agentUpdatedByQuery.Exec(ctx); err != nil {
return err
}
for id, email := range userEmailMap {
_, err = tx.NewUpdate().
TableExpr("trace_funnel").
Set("created_by = ?", email).
Where("created_by = ?", id).
Exec(ctx)
if err != nil {
return err
}
_, err = tx.NewUpdate().
TableExpr("trace_funnel").
Set("updated_by = ?", email).
Where("updated_by = ?", id).
Exec(ctx)
if err != nil {
return err
}
}
funnelCreatedByQuery := tx.NewUpdate().
TableExpr("trace_funnel").
Set("created_by = ''").
Where("created_by != ''")
if len(emails) > 0 {
funnelCreatedByQuery = funnelCreatedByQuery.Where("created_by NOT IN (?)", bun.In(emails))
}
if _, err = funnelCreatedByQuery.Exec(ctx); err != nil {
return err
}
funnelUpdatedByQuery := tx.NewUpdate().
TableExpr("trace_funnel").
Set("updated_by = ''").
Where("updated_by != ''")
if len(emails) > 0 {
funnelUpdatedByQuery = funnelUpdatedByQuery.Where("updated_by NOT IN (?)", bun.In(emails))
}
if _, err = funnelUpdatedByQuery.Exec(ctx); err != nil {
return err
}
quickFilterTable, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("quick_filter"))
if err != nil {
return err
}
sqls := [][]byte{}
createdByCol := &sqlschema.Column{Name: "created_by"}
dropSQLS := migration.sqlschema.Operator().DropColumn(quickFilterTable, createdByCol)
sqls = append(sqls, dropSQLS...)
updatedByCol := &sqlschema.Column{Name: "updated_by"}
dropSQLS = migration.sqlschema.Operator().DropColumn(quickFilterTable, updatedByCol)
sqls = append(sqls, dropSQLS...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *updateCreatedByWithEmail) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -81,10 +81,7 @@ var (
type AgentConfigVersion struct {
bun.BaseModel `bun:"table:agent_config_version,alias:acv"`
// this is only for reading
// keeping it here since we query the actual data from users table
CreatedByName string `json:"createdByName" bun:"created_by_name,scanonly"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
@@ -98,13 +95,13 @@ type AgentConfigVersion struct {
Config string `json:"config" bun:"config,type:text"`
}
func NewAgentConfigVersion(orgId valuer.UUID, userId valuer.UUID, elementType ElementType) *AgentConfigVersion {
func NewAgentConfigVersion(orgId valuer.UUID, createdBy string, elementType ElementType) *AgentConfigVersion {
return &AgentConfigVersion{
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{CreatedBy: userId.String(), UpdatedBy: userId.String()},
UserAuditable: types.UserAuditable{CreatedBy: createdBy, UpdatedBy: createdBy},
OrgID: orgId,
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
ElementType: elementType,

View File

@@ -1,6 +1,8 @@
package tracefunneltypes
import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types"
@@ -23,7 +25,6 @@ type StorableFunnel struct {
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
Steps []*FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
Tags string `json:"tags" bun:"tags,type:text"`
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
}
type FunnelStep struct {
@@ -83,12 +84,6 @@ type StepTransitionRequest struct {
StepEnd int64 `json:"step_end,omitempty"`
}
// UserInfo represents basic user information
type UserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
}
type FunnelStepFilter struct {
StepNumber int
ServiceName string
@@ -96,3 +91,41 @@ type FunnelStepFilter struct {
LatencyPointer string // "start" or "end"
CustomFilters *v3.FilterSet
}
func NewStorableFunnel(name string, description string, steps []*FunnelStep, tags string, createdBy string, orgID valuer.UUID) *StorableFunnel {
return &StorableFunnel{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: createdBy,
UpdatedBy: createdBy,
},
Name: name,
Description: description,
Steps: steps,
Tags: tags,
OrgID: orgID,
}
}
func (tf *StorableFunnel) Update(name string, description string, steps []*FunnelStep, updatedBy string) {
if name != "" {
tf.Name = name
}
if description != "" {
tf.Description = description
}
if steps != nil {
tf.Steps = steps
}
tf.UpdatedBy = updatedBy
tf.UpdatedAt = time.Now()
}

View File

@@ -5,7 +5,6 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -94,7 +93,7 @@ func ValidateAndConvertTimestamp(timestamp int64) (time.Time, error) {
return time.Unix(0, timestamp*1000000), nil // Convert to nanoseconds
}
func ConstructFunnelResponse(funnel *StorableFunnel, claims *authtypes.Claims) GettableFunnel {
func ConstructFunnelResponse(funnel *StorableFunnel) GettableFunnel {
resp := GettableFunnel{
FunnelName: funnel.Name,
FunnelID: funnel.ID.String(),
@@ -105,12 +104,7 @@ func ConstructFunnelResponse(funnel *StorableFunnel, claims *authtypes.Claims) G
UpdatedBy: funnel.UpdatedBy,
UpdatedAt: funnel.UpdatedAt.UnixNano() / 1000000,
Description: funnel.Description,
}
if funnel.CreatedByUser != nil {
resp.UserEmail = funnel.CreatedByUser.Email.String()
} else if claims != nil {
resp.UserEmail = claims.Email
UserEmail: funnel.CreatedBy,
}
return resp

View File

@@ -5,7 +5,6 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
)
@@ -419,12 +418,10 @@ func TestConstructFunnelResponse(t *testing.T) {
now := time.Now()
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
userID := valuer.GenerateUUID()
tests := []struct {
name string
funnel *StorableFunnel
claims *authtypes.Claims
expected GettableFunnel
}{
{
@@ -438,17 +435,11 @@ func TestConstructFunnelResponse(t *testing.T) {
UpdatedAt: now,
},
UserAuditable: types.UserAuditable{
CreatedBy: userID.String(),
UpdatedBy: userID.String(),
CreatedBy: valuer.MustNewEmail("funnel@example.com").String(),
UpdatedBy: valuer.MustNewEmail("funnel@example.com").String(),
},
Name: "test-funnel",
OrgID: orgID,
CreatedByUser: &types.User{
Identifiable: types.Identifiable{
ID: userID,
},
Email: valuer.MustNewEmail("funnel@example.com"),
},
Steps: []*FunnelStep{
{
ID: valuer.GenerateUUID(),
@@ -459,11 +450,6 @@ func TestConstructFunnelResponse(t *testing.T) {
},
},
},
claims: &authtypes.Claims{
UserID: userID.String(),
OrgID: orgID.String(),
Email: "claims@example.com",
},
expected: GettableFunnel{
FunnelName: "test-funnel",
FunnelID: funnelID.String(),
@@ -476,11 +462,11 @@ func TestConstructFunnelResponse(t *testing.T) {
},
},
CreatedAt: now.UnixNano() / 1000000,
CreatedBy: userID.String(),
CreatedBy: valuer.MustNewEmail("funnel@example.com").String(),
UpdatedAt: now.UnixNano() / 1000000,
UpdatedBy: userID.String(),
UpdatedBy: valuer.MustNewEmail("funnel@example.com").String(),
OrgID: orgID.String(),
UserEmail: "funnel@example.com",
UserEmail: valuer.MustNewEmail("funnel@example.com").String(),
},
},
{
@@ -494,8 +480,8 @@ func TestConstructFunnelResponse(t *testing.T) {
UpdatedAt: now,
},
UserAuditable: types.UserAuditable{
CreatedBy: userID.String(),
UpdatedBy: userID.String(),
CreatedBy: valuer.MustNewEmail("funnel@example.com").String(),
UpdatedBy: valuer.MustNewEmail("funnel@example.com").String(),
},
Name: "test-funnel",
OrgID: orgID,
@@ -509,11 +495,6 @@ func TestConstructFunnelResponse(t *testing.T) {
},
},
},
claims: &authtypes.Claims{
UserID: userID.String(),
OrgID: orgID.String(),
Email: "claims@example.com",
},
expected: GettableFunnel{
FunnelName: "test-funnel",
FunnelID: funnelID.String(),
@@ -526,18 +507,18 @@ func TestConstructFunnelResponse(t *testing.T) {
},
},
CreatedAt: now.UnixNano() / 1000000,
CreatedBy: userID.String(),
CreatedBy: valuer.MustNewEmail("funnel@example.com").String(),
UpdatedAt: now.UnixNano() / 1000000,
UpdatedBy: userID.String(),
UpdatedBy: valuer.MustNewEmail("funnel@example.com").String(),
OrgID: orgID.String(),
UserEmail: "claims@example.com",
UserEmail: valuer.MustNewEmail("funnel@example.com").String(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ConstructFunnelResponse(tt.funnel, tt.claims)
result := ConstructFunnelResponse(tt.funnel)
// Compare top-level fields
assert.Equal(t, tt.expected.FunnelName, result.FunnelName)