From a2cf2c65ab545ec68a4002c62d381146a66be54a Mon Sep 17 00:00:00 2001 From: Ewe Zi Yi <36802364+deadlycoconuts@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:00:41 +0800 Subject: [PATCH] Add field selector to list experiments API (#44) * Add field selector to list experiments API * Refactor experiment service by creating more helper functions * Specify table fields when UI queries API for experiments list * Fix refactoring changes * Update experiment context to query more experiments at once * Remove all experiment fields from required field * Make list experiments return paginated responses when page values are not specified * Simplify api call to list experiments to only be performed once * Fix changes to experiment models struct * Update treatment service with changes to experiment model struct * Refactor pagination to only take place when fields are not specified * Fix bug whereby query fields were not passed to experiment service * Make api call to display number of experiments retrieve only 1 field * Simplify API calls to list experiments endpoint * Fix bug whereby query fields were not passed to experiment service * Revert changes to experiments config group by modifying experiments controller behaviour * Refactor configs and contexts * Refactor fieldNamesSet by using imported set --- api/experiments.yaml | 9 + api/schema.yaml | 22 +-- clients/management/managementclient.go | 20 +++ common/api/schema/schema.go | 122 +++++++------ management-service/api/api.go | 128 ++++++++------ .../controller/experiment_controller.go | 20 ++- .../controller/experiment_controller_test.go | 13 +- management-service/models/experiment.go | 100 +++++++++-- management-service/models/experiment_test.go | 91 ++++++++-- management-service/pagination/pagination.go | 2 +- .../services/experiment_service.go | 164 +++++++++++++----- .../services/experiment_service_test.go | 54 ++++-- .../fetch_treatment_it_test.go | 21 +-- treatment-service/models/storage_test.go | 33 ++-- treatment-service/models/typeconverter.go | 137 ++++++++++----- .../models/typeconverter_test.go | 71 +++++--- .../testhelper/mockmanagement/api.go | 15 ++ .../mockmanagement/controller/experiment.go | 24 +-- .../mockmanagement/server/server_test.go | 66 ++++--- .../mockmanagement/service/store.go | 30 +++- ui/src/components/table/BasicTable.js | 2 +- ui/src/config.js | 1 + .../experiments/list/ListExperimentsView.js | 1 + ui/src/providers/experiment/context.js | 32 +--- .../standard_ensembler/LinkedRoutesTable.js | 13 +- 25 files changed, 791 insertions(+), 400 deletions(-) diff --git a/api/experiments.yaml b/api/experiments.yaml index c6c8d725..4f5f73a3 100644 --- a/api/experiments.yaml +++ b/api/experiments.yaml @@ -331,6 +331,15 @@ paths: in: query schema: type: boolean + - name: fields + description: | + A selector to restrict the list of returned objects by their fields. If unset, all the fields will be returned. + Paginated responses will be returned if both or either of `page` and `page_size` parameters are provided. + in: query + schema: + type: array + items: + $ref: 'schema.yaml#/components/schemas/ExperimentField' responses: 200: $ref: '#/components/responses/ListExperimentsSuccess' diff --git a/api/schema.yaml b/api/schema.yaml index fba21bec..ae335745 100644 --- a/api/schema.yaml +++ b/api/schema.yaml @@ -145,25 +145,19 @@ components: $ref: '#/components/schemas/SelectedTreatmentData' metadata: $ref: '#/components/schemas/SelectedTreatmentMetadata' - Experiment: - required: - - project_id - - description - - end_time + ExperimentField: + type: string + enum: - id - - interval - name - - segment - - start_time - - status + - type - status_friendly - - treatments - tier - - type - - created_at + - start_time + - end_time - updated_at - - updated_by - - version + - treatments + Experiment: type: object properties: description: diff --git a/clients/management/managementclient.go b/clients/management/managementclient.go index 9e03e0ec..63ed575a 100644 --- a/clients/management/managementclient.go +++ b/clients/management/managementclient.go @@ -326,6 +326,10 @@ type ListExperimentsParams struct { // controls whether or not weak segmenter matches (experiments where the segmenter is optional) should be returned IncludeWeakMatch *bool `json:"include_weak_match,omitempty"` + + // A selector to restrict the list of returned objects by their fields. If unset, all the fields will be returned. + // Paginated responses will be returned if both or either of `page` and `page_size` parameters are provided. + Fields *[]externalRef0.ExperimentField `json:"fields,omitempty"` } // ListExperimentHistoryParams defines parameters for ListExperimentHistory. @@ -1456,6 +1460,22 @@ func NewListExperimentsRequest(server string, projectId int64, params *ListExper } + if params.Fields != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "fields", runtime.ParamLocationQuery, *params.Fields); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + queryURL.RawQuery = queryValues.Encode() req, err := http.NewRequest("GET", queryURL.String(), nil) diff --git a/common/api/schema/schema.go b/common/api/schema/schema.go index 65cf9434..40fe4a12 100644 --- a/common/api/schema/schema.go +++ b/common/api/schema/schema.go @@ -18,6 +18,27 @@ import ( "github.com/pkg/errors" ) +// Defines values for ExperimentField. +const ( + ExperimentFieldEndTime ExperimentField = "end_time" + + ExperimentFieldId ExperimentField = "id" + + ExperimentFieldName ExperimentField = "name" + + ExperimentFieldStartTime ExperimentField = "start_time" + + ExperimentFieldStatusFriendly ExperimentField = "status_friendly" + + ExperimentFieldTier ExperimentField = "tier" + + ExperimentFieldTreatments ExperimentField = "treatments" + + ExperimentFieldType ExperimentField = "type" + + ExperimentFieldUpdatedAt ExperimentField = "updated_at" +) + // Defines values for ExperimentStatus. const ( ExperimentStatusActive ExperimentStatus = "active" @@ -105,29 +126,32 @@ type Error struct { // Experiment defines model for Experiment. type Experiment struct { - CreatedAt time.Time `json:"created_at"` - Description *string `json:"description"` - EndTime time.Time `json:"end_time"` - Id int64 `json:"id"` - Interval *int32 `json:"interval"` - Name string `json:"name"` - ProjectId int64 `json:"project_id"` - Segment ExperimentSegment `json:"segment"` - StartTime time.Time `json:"start_time"` - Status ExperimentStatus `json:"status"` + CreatedAt *time.Time `json:"created_at,omitempty"` + Description *string `json:"description"` + EndTime *time.Time `json:"end_time,omitempty"` + Id *int64 `json:"id,omitempty"` + Interval *int32 `json:"interval"` + Name *string `json:"name,omitempty"` + ProjectId *int64 `json:"project_id,omitempty"` + Segment *ExperimentSegment `json:"segment,omitempty"` + StartTime *time.Time `json:"start_time,omitempty"` + Status *ExperimentStatus `json:"status,omitempty"` // The user-friendly classification of experiment statuses. The categories are // self-explanatory. Note that the current time plays a role in the definition // of some of these statuses. - StatusFriendly ExperimentStatusFriendly `json:"status_friendly"` - Tier ExperimentTier `json:"tier"` - Treatments []ExperimentTreatment `json:"treatments"` - Type ExperimentType `json:"type"` - UpdatedAt time.Time `json:"updated_at"` - UpdatedBy string `json:"updated_by"` - Version int64 `json:"version"` + StatusFriendly *ExperimentStatusFriendly `json:"status_friendly,omitempty"` + Tier *ExperimentTier `json:"tier,omitempty"` + Treatments *[]ExperimentTreatment `json:"treatments,omitempty"` + Type *ExperimentType `json:"type,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + UpdatedBy *string `json:"updated_by,omitempty"` + Version *int64 `json:"version,omitempty"` } +// ExperimentField defines model for ExperimentField. +type ExperimentField string + // ExperimentHistory defines model for ExperimentHistory. type ExperimentHistory struct { CreatedAt time.Time `json:"created_at"` @@ -486,38 +510,38 @@ func (a SegmenterOptions) MarshalJSON() ([]byte, error) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "N5f/BwAA//+48xNDnioAAA==", + "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", + "WSVuYWh22r85/T8AAP//DYlTE2EqAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/management-service/api/api.go b/management-service/api/api.go index e36e9960..5290e17c 100644 --- a/management-service/api/api.go +++ b/management-service/api/api.go @@ -328,6 +328,10 @@ type ListExperimentsParams struct { // controls whether or not weak segmenter matches (experiments where the segmenter is optional) should be returned IncludeWeakMatch *bool `json:"include_weak_match,omitempty"` + + // A selector to restrict the list of returned objects by their fields. If unset, all the fields will be returned. + // Paginated responses will be returned if both or either of `page` and `page_size` parameters are provided. + Fields *[]externalRef0.ExperimentField `json:"fields,omitempty"` } // ListExperimentHistoryParams defines parameters for ListExperimentHistory. @@ -775,6 +779,18 @@ func (siw *ServerInterfaceWrapper) ListExperiments(w http.ResponseWriter, r *htt return } + // ------------- Optional query parameter "fields" ------------- + if paramValue := r.URL.Query().Get("fields"); paramValue != "" { + paramsSet["fields"] = true + + } + + err = runtime.BindQueryParameter("form", true, false, "fields", r.URL.Query(), ¶ms.Fields) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid format for parameter fields: %s", err), http.StatusBadRequest) + return + } + allSegmenters := map[string]interface{}{} for k, v := range r.URL.Query() { if _, contains := paramsSet[k]; !contains { @@ -2216,64 +2232,64 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/management-service/controller/experiment_controller.go b/management-service/controller/experiment_controller.go index f82dc08f..53673210 100644 --- a/management-service/controller/experiment_controller.go +++ b/management-service/controller/experiment_controller.go @@ -87,8 +87,12 @@ func (e ExperimentController) ListExperiments(w http.ResponseWriter, r *http.Req return } var expsResp []schema.Experiment + var fields []models.ExperimentField + if listExperimentParams.Fields != nil { + fields = *listExperimentParams.Fields + } for _, exp := range exps { - expsResp = append(expsResp, exp.ToApiSchema(segmenterTypes)) + expsResp = append(expsResp, exp.ToApiSchema(segmenterTypes, fields...)) } Ok(w, expsResp, ToPagingSchema(paging)) @@ -334,7 +338,7 @@ func (e ExperimentController) toListExperimentParams(params api.ListExperimentsP } } - return &services.ListExperimentsParams{ + finalParams := services.ListExperimentsParams{ PaginationOptions: pagination.PaginationOptions{ Page: params.Page, PageSize: params.PageSize, @@ -350,5 +354,15 @@ func (e ExperimentController) toListExperimentParams(params api.ListExperimentsP StartTime: params.StartTime, Segment: validSegmentParam, IncludeWeakMatch: params.IncludeWeakMatch != nil && *params.IncludeWeakMatch, - }, nil + } + + if params.Fields != nil { + var fields []models.ExperimentField + for _, field := range *params.Fields { + fields = append(fields, models.ExperimentField(field)) + } + finalParams.Fields = &fields + } + + return &finalParams, nil } diff --git a/management-service/controller/experiment_controller_test.go b/management-service/controller/experiment_controller_test.go index 3470089d..50e25ba4 100644 --- a/management-service/controller/experiment_controller_test.go +++ b/management-service/controller/experiment_controller_test.go @@ -152,13 +152,12 @@ func (s *ExperimentControllerTestSuite) SetupSuite() { Segment: models.ExperimentSegment{"days_of_week": []string{"1"}}, }).Return([]*models.Experiment{testExperiment}, nil, nil) expSvc. - On("ListExperiments", int64(2), - services.ListExperimentsParams{ - Status: emptyStatus, - StatusFriendly: []services.ExperimentStatusFriendly{}, - Type: emptyType, - Segment: models.ExperimentSegment{}}). - Return([]*models.Experiment{testExperiment}, nil, nil) + On("ListExperiments", int64(2), services.ListExperimentsParams{ + Status: emptyStatus, + StatusFriendly: []services.ExperimentStatusFriendly{}, + Type: emptyType, + Segment: models.ExperimentSegment{}, + }).Return([]*models.Experiment{testExperiment}, nil, nil) expSvc. On("CreateExperiment", models.Settings{ProjectID: models.ID(2)}, diff --git a/management-service/models/experiment.go b/management-service/models/experiment.go index 8399caf0..2ede1964 100644 --- a/management-service/models/experiment.go +++ b/management-service/models/experiment.go @@ -14,6 +14,7 @@ import ( type ExperimentStatus string type ExperimentType string type ExperimentTier string +type ExperimentField string const ( ExperimentStatusActive ExperimentStatus = "active" @@ -35,6 +36,30 @@ const ( ExperimentTierOverride ExperimentTier = "override" ) +// Defines values for ExperimentField. +const ( + ExperimentFieldEndTime ExperimentField = "end_time" + + ExperimentFieldId ExperimentField = "id" + + ExperimentFieldName ExperimentField = "name" + + ExperimentFieldStartTime ExperimentField = "start_time" + + ExperimentFieldStatusFriendly ExperimentField = "status_friendly" + + // ExperimentFieldStatus is only used for querying the db because status_friendly does not exist as a column + ExperimentFieldStatus ExperimentField = "status" + + ExperimentFieldTier ExperimentField = "tier" + + ExperimentFieldTreatments ExperimentField = "treatments" + + ExperimentFieldType ExperimentField = "type" + + ExperimentFieldUpdatedAt ExperimentField = "updated_at" +) + type Experiment struct { Model @@ -83,25 +108,68 @@ func (e *Experiment) AfterFind(tx *gorm.DB) error { // ToApiSchema converts the experiment DB model to a format compatible with the // OpenAPI specifications. -func (e *Experiment) ToApiSchema(segmentersType map[string]schema.SegmenterType) schema.Experiment { +func (e *Experiment) ToApiSchema(segmentersType map[string]schema.SegmenterType, fields ...ExperimentField) schema.Experiment { + experiment := schema.Experiment{} + + // Only return requested fields + if fields != nil { + for _, field := range fields { + switch field { + case ExperimentFieldName: + experiment.Name = &e.Name + case ExperimentFieldId: + id := e.ID.ToApiSchema() + experiment.Id = &id + case ExperimentFieldType: + experimentType := schema.ExperimentType(e.Type) + experiment.Type = &experimentType + case ExperimentFieldStatusFriendly: + statusFriendly := getExperimentStatusFriendly(e.StartTime, e.EndTime, e.Status) + experiment.StatusFriendly = &statusFriendly + case ExperimentFieldTier: + tier := schema.ExperimentTier(e.Tier) + experiment.Tier = &tier + case ExperimentFieldStartTime: + experiment.StartTime = &e.StartTime + case ExperimentFieldEndTime: + experiment.EndTime = &e.EndTime + case ExperimentFieldUpdatedAt: + experiment.UpdatedAt = &e.UpdatedAt + case ExperimentFieldTreatments: + treatments := e.Treatments.ToApiSchema() + experiment.Treatments = &treatments + } + } + return experiment + } + + id := e.ID.ToApiSchema() + projectId := e.ProjectID.ToApiSchema() + segment := e.Segment.ToApiSchema(segmentersType) + status := schema.ExperimentStatus(e.Status) + statusFriendly := getExperimentStatusFriendly(e.StartTime, e.EndTime, e.Status) + treatments := e.Treatments.ToApiSchema() + experimentType := schema.ExperimentType(e.Type) + tier := schema.ExperimentTier(e.Tier) + return schema.Experiment{ Description: e.Description, - EndTime: e.EndTime, - Id: e.ID.ToApiSchema(), + EndTime: &e.EndTime, + Id: &id, Interval: e.Interval, - Name: e.Name, - ProjectId: e.ProjectID.ToApiSchema(), - Segment: e.Segment.ToApiSchema(segmentersType), - Status: schema.ExperimentStatus(e.Status), - StatusFriendly: getExperimentStatusFriendly(e.StartTime, e.EndTime, e.Status), - Treatments: e.Treatments.ToApiSchema(), - Type: schema.ExperimentType(e.Type), - Tier: schema.ExperimentTier(e.Tier), - StartTime: e.StartTime, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - UpdatedBy: e.UpdatedBy, - Version: e.Version, + Name: &e.Name, + ProjectId: &projectId, + Segment: &segment, + Status: &status, + StatusFriendly: &statusFriendly, + Treatments: &treatments, + Type: &experimentType, + Tier: &tier, + StartTime: &e.StartTime, + CreatedAt: &e.CreatedAt, + UpdatedAt: &e.UpdatedAt, + UpdatedBy: &e.UpdatedBy, + Version: &e.Version, } } diff --git a/management-service/models/experiment_test.go b/management-service/models/experiment_test.go index 95aac8c7..93c18fc2 100644 --- a/management-service/models/experiment_test.go +++ b/management-service/models/experiment_test.go @@ -54,22 +54,36 @@ func TestExperimentToApiSchema(t *testing.T) { "string_segmenter": schema.SegmenterTypeString, } + id := int64(5) + projectId := int64(1) + createdAt := time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC) + updatedAt := time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC) + updatedBy := "admin" + endTime := time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC) + startTime := time.Date(2022, 2, 2, 1, 1, 1, 1, time.UTC) + name := "test-exp" + status := schema.ExperimentStatusActive + statusFriendly := schema.ExperimentStatusFriendlyCompleted + experimentType := schema.ExperimentTypeSwitchback + tier := schema.ExperimentTierDefault + version := int64(2) + assert.Equal(t, schema.Experiment{ - Id: int64(5), - ProjectId: int64(1), - CreatedAt: time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC), - UpdatedAt: time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC), - UpdatedBy: "admin", - EndTime: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC), - StartTime: time.Date(2022, 2, 2, 1, 1, 1, 1, time.UTC), - Name: "test-exp", + Id: &id, + ProjectId: &projectId, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + UpdatedBy: &updatedBy, + EndTime: &endTime, + StartTime: &startTime, + Name: &name, Description: &testExperimentDescription, Interval: &testExperimentInterval, - Status: schema.ExperimentStatusActive, - StatusFriendly: schema.ExperimentStatusFriendlyCompleted, - Type: schema.ExperimentTypeSwitchback, - Tier: schema.ExperimentTierDefault, - Treatments: []schema.ExperimentTreatment{ + Status: &status, + StatusFriendly: &statusFriendly, + Type: &experimentType, + Tier: &tier, + Treatments: &[]schema.ExperimentTreatment{ { Configuration: map[string]interface{}{ "config-1": "value", @@ -79,13 +93,60 @@ func TestExperimentToApiSchema(t *testing.T) { Traffic: &testExperimentTraffic, }, }, - Segment: schema.ExperimentSegment{ + Segment: &schema.ExperimentSegment{ "string_segmenter": []string{"seg-1"}, }, - Version: 2, + Version: &version, }, testExperiment.ToApiSchema(segmenterTypes)) } +func TestExperimentToApiSchemaWithFields(t *testing.T) { + segmenterTypes := map[string]schema.SegmenterType{ + "string_segmenter": schema.SegmenterTypeString, + } + + fields := []ExperimentField{ + ExperimentFieldId, + ExperimentFieldName, + ExperimentFieldType, + ExperimentFieldStatusFriendly, + ExperimentFieldTier, + ExperimentFieldStartTime, + ExperimentFieldEndTime, + ExperimentFieldUpdatedAt, + ExperimentFieldTreatments, + } + id := int64(5) + updatedAt := time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC) + endTime := time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC) + startTime := time.Date(2022, 2, 2, 1, 1, 1, 1, time.UTC) + name := "test-exp" + statusFriendly := schema.ExperimentStatusFriendlyCompleted + experimentType := schema.ExperimentTypeSwitchback + tier := schema.ExperimentTierDefault + + assert.Equal(t, schema.Experiment{ + Id: &id, + UpdatedAt: &updatedAt, + EndTime: &endTime, + StartTime: &startTime, + Name: &name, + StatusFriendly: &statusFriendly, + Type: &experimentType, + Tier: &tier, + Treatments: &[]schema.ExperimentTreatment{ + { + Configuration: map[string]interface{}{ + "config-1": "value", + "config-2": 2, + }, + Name: "control", + Traffic: &testExperimentTraffic, + }, + }, + }, testExperiment.ToApiSchema(segmenterTypes, fields...)) +} + func TestExperimentToApiSchemaStatusFriendly(t *testing.T) { tests := map[string]struct { startTime time.Time diff --git a/management-service/pagination/pagination.go b/management-service/pagination/pagination.go index e45b1fc6..16f52002 100644 --- a/management-service/pagination/pagination.go +++ b/management-service/pagination/pagination.go @@ -7,7 +7,7 @@ import ( ) var ( - MaxPageSize int32 = 10 + MaxPageSize int32 = 50 DefaultPageSize int32 = 10 DefaultPage int32 = 1 ) diff --git a/management-service/services/experiment_service.go b/management-service/services/experiment_service.go index 6c609cc6..db842a14 100644 --- a/management-service/services/experiment_service.go +++ b/management-service/services/experiment_service.go @@ -66,6 +66,7 @@ type ListExperimentsParams struct { StartTime *time.Time `json:"start_time,omitempty"` Segment models.ExperimentSegment `json:"segment,omitempty"` IncludeWeakMatch bool `json:"include_weak_match"` + Fields *[]models.ExperimentField `json:"fields,omitempty"` } type ExperimentService interface { @@ -110,8 +111,18 @@ func (svc *experimentService) ListExperiments( projectId int64, params ListExperimentsParams, ) ([]*models.Experiment, *pagination.Paging, error) { + var err error var exps []*models.Experiment - query := svc.query(). + + query := svc.query() + + // Handle Field values + query, err = svc.filterFieldValues(query, params) + if err != nil { + return nil, nil, err + } + + query = query. Where("project_id = ?", projectId). Order("updated_at desc") @@ -119,34 +130,17 @@ func (svc *experimentService) ListExperiments( if params.Status != nil { query = query.Where("status = ?", params.Status) } + // Handle StatusFriendly values if len(params.StatusFriendly) > 0 { query = svc.filterExperimentStatusFriendly(query, params.StatusFriendly) } - if params.StartTime != nil && !params.StartTime.IsZero() && (params.EndTime == nil || params.EndTime.IsZero()) { - return nil, nil, errors.Newf(errors.BadInput, "end_time parameter must be supplied as well") - } - if params.EndTime != nil && !params.EndTime.IsZero() && (params.StartTime == nil || params.StartTime.IsZero()) { - return nil, nil, errors.Newf(errors.BadInput, "start_time parameter must be supplied as well") - } - if params.StartTime != nil && !params.StartTime.IsZero() && params.EndTime != nil && !params.EndTime.IsZero() { - // Find experiments that are at least partially running in this window. - if params.StartTime.Equal(*params.EndTime) { - // To filter active experiments at a given timestamp (such as current timestamp), - // it needs to be passed in for both the start and end time. - query = query.Where("tstzrange(start_time, end_time, '[)') @> tstzrange(?, ?, '[]')", params.StartTime, params.EndTime) - } else { - // One of the following should match: - // * the start_time parameter should fall within the experiment's [start and end) times - // * the end_time parameter should fall within the experiment's (start and end) times - // * the experiment starts and ends within the [start_time and end_time) duration - query = query.Where( - svc.query(). - Where("tstzrange(start_time, end_time, '[)') @> tstzrange(?, ?, '[]')", params.StartTime, params.StartTime). - Or("tstzrange(start_time, end_time, '()') @> tstzrange(?, ?, '[]')", params.EndTime, params.EndTime). - Or("tstzrange(?, ?, '[]') @> tstzrange(start_time, end_time, '[)')", params.StartTime, params.EndTime), - ) - } + + // Handle Start and EndTime values + query, err = svc.filterStartEndTimeValues(query, params) + if err != nil { + return nil, nil, err } + if params.Tier != nil { query = query.Where("tier = ?", params.Tier) } @@ -172,22 +166,24 @@ func (svc *experimentService) ListExperiments( // Pagination var pagingResponse *pagination.Paging var count int64 - err := pagination.ValidatePaginationParams(params.Page, params.PageSize) - if err != nil { - return nil, nil, err - } - pageOpts := pagination.NewPaginationOptions(params.Page, params.PageSize) - // Count total - query.Model(&exps).Count(&count) - // Add offset and limit - query = query.Offset(int((*pageOpts.Page - 1) * *pageOpts.PageSize)) - query = query.Limit(int(*pageOpts.PageSize)) - // Format opts into paging response - pagingResponse = pagination.ToPaging(pageOpts, int(count)) - if pagingResponse.Page > 1 && pagingResponse.Pages < pagingResponse.Page { - // Invalid query - total pages is less than the requested page - return nil, nil, errors.Newf(errors.BadInput, - "Requested page number %d exceeds total pages: %d.", pagingResponse.Page, pagingResponse.Pages) + if params.Fields == nil || params.Page != nil || params.PageSize != nil { + err = pagination.ValidatePaginationParams(params.Page, params.PageSize) + if err != nil { + return nil, nil, err + } + pageOpts := pagination.NewPaginationOptions(params.Page, params.PageSize) + // Count total + query.Model(&exps).Count(&count) + // Add offset and limit + query = query.Offset(int((*pageOpts.Page - 1) * *pageOpts.PageSize)) + query = query.Limit(int(*pageOpts.PageSize)) + // Format opts into paging response + pagingResponse = pagination.ToPaging(pageOpts, int(count)) + if pagingResponse.Page > 1 && pagingResponse.Pages < pagingResponse.Page { + // Invalid query - total pages is less than the requested page + return nil, nil, errors.Newf(errors.BadInput, + "Requested page number %d exceeds total pages: %d.", pagingResponse.Page, pagingResponse.Pages) + } } // Filter experiments @@ -555,6 +551,42 @@ func (svc *experimentService) save(exp *models.Experiment) (*models.Experiment, return svc.GetDBRecord(exp.ProjectID, exp.ID) } +func (svc *experimentService) filterFieldValues(query *gorm.DB, params ListExperimentsParams) (*gorm.DB, error) { + if params.Fields != nil && len(*params.Fields) != 0 { + err := validateListExperimentFieldNames(*params.Fields) + if err != nil { + return nil, err + } + + // Stores a set of unique field names which are unique column names to be selected in the db query + fieldNamesSet := set.New() + for _, field := range *params.Fields { + fieldName := field + // Add ExperimentFieldStatus to the query because status_friendly does not exist in the db as a column + if field == models.ExperimentFieldStatusFriendly { + // Add ExperimentFieldStartTime and ExperimentFieldEndTime to the query as they are required for + // determining the statusFriendly field; these two fields may or may not be returned depending on + // what is actually specified in params.Fields + fieldNamesSet.Insert(string(models.ExperimentFieldStartTime)) + fieldNamesSet.Insert(string(models.ExperimentFieldEndTime)) + + fieldName = models.ExperimentFieldStatus + } + //fieldNamesSet[string(fieldName)] = struct{}{} + fieldNamesSet.Insert(string(fieldName)) + } + + // Retrieve a slice of unique strings from fieldNamesSet; query.Select only accepts []string + var fieldNames []string + fieldNamesSet.Do(func(fieldName interface{}) { + fieldNames = append(fieldNames, fmt.Sprint(fieldName)) + }) + + query = query.Select(fieldNames) + } + return query, nil +} + func (svc *experimentService) filterExperimentStatusFriendly(query *gorm.DB, statusesFriendly []ExperimentStatusFriendly) *gorm.DB { orPredicates := svc.query().Where("false") // start with false and build OR query dynamically for _, statusFriendly := range statusesFriendly { @@ -578,6 +610,35 @@ func (svc *experimentService) filterExperimentStatusFriendly(query *gorm.DB, sta return query.Where(orPredicates) } +func (svc *experimentService) filterStartEndTimeValues(query *gorm.DB, params ListExperimentsParams) (*gorm.DB, error) { + if params.StartTime != nil && !params.StartTime.IsZero() && (params.EndTime == nil || params.EndTime.IsZero()) { + return nil, errors.Newf(errors.BadInput, "end_time parameter must be supplied as well") + } + if params.EndTime != nil && !params.EndTime.IsZero() && (params.StartTime == nil || params.StartTime.IsZero()) { + return nil, errors.Newf(errors.BadInput, "start_time parameter must be supplied as well") + } + if params.StartTime != nil && !params.StartTime.IsZero() && params.EndTime != nil && !params.EndTime.IsZero() { + // Find experiments that are at least partially running in this window. + if params.StartTime.Equal(*params.EndTime) { + // To filter active experiments at a given timestamp (such as current timestamp), + // it needs to be passed in for both the start and end time. + query = query.Where("tstzrange(start_time, end_time, '[)') @> tstzrange(?, ?, '[]')", params.StartTime, params.EndTime) + } else { + // One of the following should match: + // * the start_time parameter should fall within the experiment's [start and end) times + // * the end_time parameter should fall within the experiment's (start and end) times + // * the experiment starts and ends within the [start_time and end_time) duration + query = query.Where( + svc.query(). + Where("tstzrange(start_time, end_time, '[)') @> tstzrange(?, ?, '[]')", params.StartTime, params.StartTime). + Or("tstzrange(start_time, end_time, '()') @> tstzrange(?, ?, '[]')", params.EndTime, params.EndTime). + Or("tstzrange(?, ?, '[]') @> tstzrange(start_time, end_time, '[)')", params.StartTime, params.EndTime), + ) + } + } + return query, nil +} + func (svc *experimentService) filterSegmenterValues(query *gorm.DB, segment models.ExperimentSegment, includeWeakMatch bool) *gorm.DB { // No need to format the segmenter values according to their types since we're storing all values in string for name, values := range segment { @@ -586,6 +647,27 @@ func (svc *experimentService) filterSegmenterValues(query *gorm.DB, segment mode return query } +func validateListExperimentFieldNames(fields []models.ExperimentField) error { + allowedFieldList := []interface{}{ + models.ExperimentFieldId, + models.ExperimentFieldName, + models.ExperimentFieldStartTime, + models.ExperimentFieldEndTime, + models.ExperimentFieldTier, + models.ExperimentFieldType, + models.ExperimentFieldStatusFriendly, + models.ExperimentFieldUpdatedAt, + models.ExperimentFieldTreatments, + } + allowedFields := set.New(allowedFieldList...) + for _, field := range fields { + if !allowedFields.Has(field) { + return fmt.Errorf("field %s is not supported, fields should only be name and/or id", field) + } + } + return nil +} + func (svc *experimentService) validateExperimentOrthogonality( projectId int64, experimentId *int64, diff --git a/management-service/services/experiment_service_test.go b/management-service/services/experiment_service_test.go index 848c1e50..db126b7d 100644 --- a/management-service/services/experiment_service_test.go +++ b/management-service/services/experiment_service_test.go @@ -163,28 +163,30 @@ func testListExperiments(s *ExperimentServiceTestSuite) { stringSegmenter := []string{"seg-1"} boolSegmenter := []string{"true"} + var nilPagingResponse *pagination.Paging + // All experiments under a settings - expResponsesList, pagingResponse, err := svc.ListExperiments(1, services.ListExperimentsParams{}) + actualResponsesList, pagingResponse, err := svc.ListExperiments(1, services.ListExperimentsParams{}) s.Suite.Require().NoError(err) tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 3}, pagingResponse) - tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1], s.Experiments[2]}, expResponsesList) + tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1], s.Experiments[2]}, actualResponsesList) // No experiments filtered - expResponsesList, pagingResponse, err = svc.ListExperiments(3, services.ListExperimentsParams{}) + actualResponsesList, pagingResponse, err = svc.ListExperiments(3, services.ListExperimentsParams{}) s.Suite.Require().NoError(err) tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 0, Total: 0}, pagingResponse) - tu.AssertEqualValues(t, []*models.Experiment{}, expResponsesList) + tu.AssertEqualValues(t, []*models.Experiment{}, actualResponsesList) // Filter by a single parameter - expResponsesList, pagingResponse, err = svc.ListExperiments(1, + actualResponsesList, pagingResponse, err = svc.ListExperiments(1, services.ListExperimentsParams{Status: &testStatus}, ) s.Suite.Require().NoError(err) tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 2}, pagingResponse) - tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, expResponsesList) + tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, actualResponsesList) // Filter by all parameters - expResponsesList, pagingResponse, err = svc.ListExperiments(1, services.ListExperimentsParams{ + actualResponsesList, pagingResponse, err = svc.ListExperiments(1, services.ListExperimentsParams{ Type: &testExpType, Status: &testStatus, StartTime: &testStartTime, @@ -208,11 +210,11 @@ func testListExperiments(s *ExperimentServiceTestSuite) { Pages: 1, Total: 1, }, pagingResponse) - tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0]}, expResponsesList) + tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0]}, actualResponsesList) // Use the same start and end times testExactTimestamp := time.Date(2021, 2, 2, 3, 5, 7, 0, time.UTC) - expResponsesList, pagingResponse, err = svc.ListExperiments(1, + actualResponsesList, pagingResponse, err = svc.ListExperiments(1, services.ListExperimentsParams{ StartTime: &testExactTimestamp, EndTime: &testExactTimestamp, @@ -220,10 +222,10 @@ func testListExperiments(s *ExperimentServiceTestSuite) { ) s.Suite.Require().NoError(err) tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 1}, pagingResponse) - tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[2]}, expResponsesList) + tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[2]}, actualResponsesList) // Partial match of segmenter on multiple experiments - expResponsesList, pagingResponse, err = svc.ListExperiments(1, + actualResponsesList, pagingResponse, err = svc.ListExperiments(1, services.ListExperimentsParams{ Segment: models.ExperimentSegment{ "float_segmenter": float2Segmenter, @@ -234,11 +236,11 @@ func testListExperiments(s *ExperimentServiceTestSuite) { tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 2}, pagingResponse) tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1]}, - expResponsesList, + actualResponsesList, ) // Weak match of segmenters - expResponsesList, pagingResponse, err = svc.ListExperiments(1, + actualResponsesList, pagingResponse, err = svc.ListExperiments(1, services.ListExperimentsParams{ Segment: models.ExperimentSegment{ "float_segmenter": floatSegmenter, @@ -250,21 +252,21 @@ func testListExperiments(s *ExperimentServiceTestSuite) { tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 2}, pagingResponse) tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, - expResponsesList, + actualResponsesList, ) // Match name or description testDesc := "-1" - expResponsesList, _, err = svc.ListExperiments(1, + actualResponsesList, _, err = svc.ListExperiments(1, services.ListExperimentsParams{ Search: &testDesc, }, ) s.Suite.Require().NoError(err) - tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, expResponsesList) + tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, actualResponsesList) // Match friendly status + start time - expResponsesList, _, err = svc.ListExperiments(1, + actualResponsesList, _, err = svc.ListExperiments(1, services.ListExperimentsParams{ StatusFriendly: []services.ExperimentStatusFriendly{ services.ExperimentStatusFriendlyCompleted, @@ -275,7 +277,23 @@ func testListExperiments(s *ExperimentServiceTestSuite) { }, ) s.Suite.Require().NoError(err) - tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1]}, expResponsesList) + tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1]}, actualResponsesList) + + // Specify selected fields in ListExperimentsParams + actualResponsesList, pagingResponse, err = svc.ListExperiments(1, + services.ListExperimentsParams{ + Fields: &[]models.ExperimentField{ + models.ExperimentFieldName, + }, + }, + ) + s.Suite.Require().NoError(err) + tu.AssertEqualValues(t, nilPagingResponse, pagingResponse) + tu.AssertEqualValues(t, []*models.Experiment{ + {Name: s.Experiments[0].Name}, + {Name: s.Experiments[1].Name}, + {Name: s.Experiments[2].Name}, + }, actualResponsesList) } func testCreateUpdateExperiment(s *ExperimentServiceTestSuite) { diff --git a/treatment-service/integration-test/fetch_treatment_it_test.go b/treatment-service/integration-test/fetch_treatment_it_test.go index ab0ad840..3ba247fd 100644 --- a/treatment-service/integration-test/fetch_treatment_it_test.go +++ b/treatment-service/integration-test/fetch_treatment_it_test.go @@ -204,23 +204,24 @@ func generateExperiments() []schema.Experiment { experimentType := val["type"].(schema.ExperimentType) startTime := val["start-time"].(time.Time) endTime := val["end-time"].(time.Time) + version := int64(1) experiment := schema.Experiment{ - Id: id, - ProjectId: projectId, + Id: &id, + ProjectId: &projectId, Description: &description, - EndTime: endTime, + EndTime: &endTime, Interval: &interval, - Name: name, - Segment: schema.ExperimentSegment{ + Name: &name, + Segment: &schema.ExperimentSegment{ "days_of_week": &daysOfWeek, "hours_of_day": &hoursOfDay, "s2_ids": &s2Ids, }, - StartTime: startTime, - Treatments: treatments, - Type: experimentType, - Version: int64(1), + StartTime: &startTime, + Treatments: &treatments, + Type: &experimentType, + Version: &version, } experiments = append(experiments, experiment) } @@ -811,5 +812,5 @@ func (suite *TreatmentServiceTestSuite) TestLocalStorage() { resp, err := suite.managementServiceClient.GetExperimentWithResponse(suite.ctx, 1, 1) suite.Require().NoError(err) - suite.Require().Equal(resp.JSON200.Data.Name, storage.Experiments[1][0].Experiment.Name) + suite.Require().Equal(storage.Experiments[1][0].Experiment.Name, *resp.JSON200.Data.Name) } diff --git a/treatment-service/models/storage_test.go b/treatment-service/models/storage_test.go index 3d4ad331..2a1cf728 100644 --- a/treatment-service/models/storage_test.go +++ b/treatment-service/models/storage_test.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "math/rand" "net/http" "os" @@ -51,18 +51,25 @@ func newTestXPExperiment( {Configuration: make(map[string]interface{}), Name: "default", Traffic: &traffic}, } + id := int64(rand.Intn(100)) + nameString := name.String() + status := schema.ExperimentStatusActive + experimentType := schema.ExperimentTypeAB + updatedAt := time.Time{} + updatedBy := "" + return schema.Experiment{ - ProjectId: projectId, - Id: int64(rand.Intn(100)), - StartTime: startTime, - EndTime: endTime, - Name: name.String(), - Status: "active", - Segment: segment, - Treatments: treatments, - Type: "A/B", - UpdatedAt: time.Time{}, - UpdatedBy: "", + ProjectId: &projectId, + Id: &id, + StartTime: &startTime, + EndTime: &endTime, + Name: &nameString, + Status: &status, + Segment: &segment, + Treatments: &treatments, + Type: &experimentType, + UpdatedAt: &updatedAt, + UpdatedBy: &updatedBy, Interval: &interval, } } @@ -110,7 +117,7 @@ func (suite *LocalStorageLookupSuite) SetupTest() { Return(&http.Response{ StatusCode: 200, Header: map[string][]string{"Content-Type": {"json"}}, - Body: ioutil.NopCloser(bytes.NewBufferString(`{"data" : []}`)), + Body: io.NopCloser(bytes.NewBufferString(`{"data" : []}`)), }, nil) suite.storage = LocalStorage{ diff --git a/treatment-service/models/typeconverter.go b/treatment-service/models/typeconverter.go index 1ce81c4f..c9b80860 100644 --- a/treatment-service/models/typeconverter.go +++ b/treatment-service/models/typeconverter.go @@ -2,6 +2,7 @@ package models import ( "reflect" + "time" "github.com/caraml-dev/xp/common/api/schema" _pubsub "github.com/caraml-dev/xp/common/pubsub" @@ -66,72 +67,112 @@ func OpenAPIExperimentSpecToProtobuf( "default": _pubsub.Experiment_Default, "override": _pubsub.Experiment_Override, } + var status _pubsub.Experiment_Status + if xpExperiment.Status != nil { + status = statusConverter[*xpExperiment.Status] + } + + var experimentType _pubsub.Experiment_Type + if xpExperiment.Type != nil { + experimentType = typeConverter[*xpExperiment.Type] + } + + var tier _pubsub.Experiment_Tier + if xpExperiment.Tier != nil { + tier = tierConverter[*xpExperiment.Tier] + } + segments := make(map[string]*_segmenters.ListSegmenterValue) - for key, val := range xpExperiment.Segment { - vals := val.([]interface{}) - switch segmentersType[key] { - case "string": - stringVals := []string{} - for _, val := range vals { - stringVals = append(stringVals, val.(string)) - } - segments[key] = _utils.StringSliceToListSegmenterValue(&stringVals) - case "integer": - intVals := []int64{} - for _, val := range vals { - reflectedVal := reflect.ValueOf(val) - switch reflectedVal.Kind() { - case reflect.Float32, reflect.Float64: - intVals = append(intVals, int64(reflectedVal.Float())) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - intVals = append(intVals, reflectedVal.Int()) + + if xpExperiment.Segment != nil { + for key, val := range *xpExperiment.Segment { + vals := val.([]interface{}) + switch segmentersType[key] { + case "string": + stringVals := []string{} + for _, val := range vals { + stringVals = append(stringVals, val.(string)) } + segments[key] = _utils.StringSliceToListSegmenterValue(&stringVals) + case "integer": + intVals := []int64{} + for _, val := range vals { + reflectedVal := reflect.ValueOf(val) + switch reflectedVal.Kind() { + case reflect.Float32, reflect.Float64: + intVals = append(intVals, int64(reflectedVal.Float())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + intVals = append(intVals, reflectedVal.Int()) + } + } + segments[key] = _utils.Int64ListToListSegmenterValue(&intVals) + case "real": + floatVals := []float64{} + for _, val := range vals { + floatVals = append(floatVals, val.(float64)) + } + segments[key] = _utils.FloatListToListSegmenterValue(&floatVals) + case "bool": + boolVals := []bool{} + for _, val := range vals { + boolVals = append(boolVals, val.(bool)) + } + segments[key] = _utils.BoolSliceToListSegmenterValue(&boolVals) + default: + segments[key] = nil } - segments[key] = _utils.Int64ListToListSegmenterValue(&intVals) - case "real": - floatVals := []float64{} - for _, val := range vals { - floatVals = append(floatVals, val.(float64)) - } - segments[key] = _utils.FloatListToListSegmenterValue(&floatVals) - case "bool": - boolVals := []bool{} - for _, val := range vals { - boolVals = append(boolVals, val.(bool)) - } - segments[key] = _utils.BoolSliceToListSegmenterValue(&boolVals) - default: - segments[key] = nil } } treatments := make([]*_pubsub.ExperimentTreatment, 0) - for _, t := range xpExperiment.Treatments { - treatment, err := openAPIExperimentTreatmentSpecToProtobuf(t) - if err != nil { - return nil, err + if xpExperiment.Treatments != nil { + for _, t := range *xpExperiment.Treatments { + treatment, err := openAPIExperimentTreatmentSpecToProtobuf(t) + if err != nil { + return nil, err + } + treatments = append(treatments, treatment) } - treatments = append(treatments, treatment) } interval := int32(0) if xpExperiment.Interval != nil { interval = *xpExperiment.Interval } + var version int64 + if xpExperiment.Version != nil { + version = *xpExperiment.Version + } + + var startTime time.Time + if xpExperiment.StartTime != nil { + startTime = *xpExperiment.StartTime + } + + var endTime time.Time + if xpExperiment.EndTime != nil { + endTime = *xpExperiment.EndTime + } + + var updatedAt time.Time + if xpExperiment.UpdatedAt != nil { + updatedAt = *xpExperiment.UpdatedAt + } + return &_pubsub.Experiment{ - Id: xpExperiment.Id, - ProjectId: xpExperiment.ProjectId, - Status: statusConverter[xpExperiment.Status], - Name: xpExperiment.Name, - Type: typeConverter[xpExperiment.Type], - Tier: tierConverter[xpExperiment.Tier], + Id: *xpExperiment.Id, + ProjectId: *xpExperiment.ProjectId, + Status: status, + Name: *xpExperiment.Name, + Type: experimentType, + Tier: tier, Interval: interval, Segments: segments, Treatments: treatments, - StartTime: ×tamppb.Timestamp{Seconds: xpExperiment.StartTime.Unix()}, - EndTime: ×tamppb.Timestamp{Seconds: xpExperiment.EndTime.Unix()}, - UpdatedAt: ×tamppb.Timestamp{Seconds: xpExperiment.UpdatedAt.Unix()}, - Version: xpExperiment.Version, + StartTime: ×tamppb.Timestamp{Seconds: startTime.Unix()}, + EndTime: ×tamppb.Timestamp{Seconds: endTime.Unix()}, + UpdatedAt: ×tamppb.Timestamp{Seconds: updatedAt.Unix()}, + Version: version, }, nil } diff --git a/treatment-service/models/typeconverter_test.go b/treatment-service/models/typeconverter_test.go index 025378f4..2c6f0c81 100644 --- a/treatment-service/models/typeconverter_test.go +++ b/treatment-service/models/typeconverter_test.go @@ -70,6 +70,20 @@ func TestOpenAPIProjectSettingsSpecToProtobuf(t *testing.T) { } func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { + projectId := int64(1) + id := int64(2) + name := "experiment-1" + statusActive := schema.ExperimentStatusActive + statusInactive := schema.ExperimentStatusInactive + tierDefault := schema.ExperimentTierDefault + tierOverride := schema.ExperimentTierOverride + typeAB := schema.ExperimentTypeAB + typeSwitchback := schema.ExperimentTypeSwitchback + version := int64(2) + startTime := time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC) + endTime := time.Date(2022, 1, 1, 2, 3, 4, 0, time.UTC) + createdAt := time.Date(2020, 1, 1, 2, 3, 4, 0, time.UTC) + updatedAt := time.Date(2020, 2, 1, 2, 3, 4, 0, time.UTC) traffic100 := int32(100) interval := int32(60) segmentersType := map[string]schema.SegmenterType{ @@ -78,6 +92,7 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { pubsubCfg, _ := structpb.NewStruct(map[string]interface{}{ "key": "value", }) + tests := []struct { Name string Experiment schema.Experiment @@ -87,14 +102,14 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { { Name: "active default a/b experiment", Experiment: schema.Experiment{ - ProjectId: 1, - Id: 2, - Name: "experiment-1", - Segment: schema.ExperimentSegment{ + ProjectId: &projectId, + Id: &id, + Name: &name, + Segment: &schema.ExperimentSegment{ "string_segmenter": []interface{}{"ID"}, }, - Status: schema.ExperimentStatusActive, - Treatments: []schema.ExperimentTreatment{ + Status: &statusActive, + Treatments: &[]schema.ExperimentTreatment{ { Name: "default", Configuration: map[string]interface{}{ @@ -103,13 +118,13 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { Traffic: &traffic100, }, }, - Tier: schema.ExperimentTierDefault, - Type: schema.ExperimentTypeAB, - StartTime: time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC), - EndTime: time.Date(2022, 1, 1, 2, 3, 4, 0, time.UTC), - CreatedAt: time.Date(2020, 1, 1, 2, 3, 4, 0, time.UTC), - UpdatedAt: time.Date(2020, 2, 1, 2, 3, 4, 0, time.UTC), - Version: 2, + Tier: &tierDefault, + Type: &typeAB, + StartTime: &startTime, + EndTime: &endTime, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + Version: &version, }, Expected: &pubsub.Experiment{ ProjectId: 1, @@ -142,24 +157,24 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { { Name: "inactive override switchback experiment", Experiment: schema.Experiment{ - ProjectId: 3, - Id: 4, + ProjectId: &projectId, + Id: &id, Interval: &interval, - Name: "experiment-2", - Status: schema.ExperimentStatusInactive, - Tier: schema.ExperimentTierOverride, - Type: schema.ExperimentTypeSwitchback, - StartTime: time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC), - EndTime: time.Date(2022, 1, 1, 2, 3, 4, 0, time.UTC), - CreatedAt: time.Date(2020, 1, 1, 2, 3, 4, 0, time.UTC), - UpdatedAt: time.Date(2020, 2, 1, 2, 3, 4, 0, time.UTC), - Version: 1, + Name: &name, + Status: &statusInactive, + Tier: &tierOverride, + Type: &typeSwitchback, + StartTime: &startTime, + EndTime: &endTime, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + Version: &version, }, Expected: &pubsub.Experiment{ - ProjectId: 3, - Id: 4, + ProjectId: 1, + Id: 2, Interval: 60, - Name: "experiment-2", + Name: "experiment-1", Segments: map[string]*_segmenters.ListSegmenterValue{}, Status: pubsub.Experiment_Inactive, Treatments: []*pubsub.ExperimentTreatment{}, @@ -168,7 +183,7 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { StartTime: timestamppb.New(time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC)), EndTime: timestamppb.New(time.Date(2022, 1, 1, 2, 3, 4, 0, time.UTC)), UpdatedAt: timestamppb.New(time.Date(2020, 2, 1, 2, 3, 4, 0, time.UTC)), - Version: 1, + Version: 2, }, }, } diff --git a/treatment-service/testhelper/mockmanagement/api.go b/treatment-service/testhelper/mockmanagement/api.go index 1ed094e0..752f88fc 100644 --- a/treatment-service/testhelper/mockmanagement/api.go +++ b/treatment-service/testhelper/mockmanagement/api.go @@ -212,6 +212,10 @@ type ListExperimentsParams struct { // controls whether or not weak segmenter matches (experiments where the segmenter is optional) should be returned IncludeWeakMatch *bool `json:"include_weak_match,omitempty"` + + // A selector to restrict the list of returned objects by their fields. If unset, all the fields will be returned. + // Paginated responses will be returned if both or either of `page` and `page_size` parameters are provided. + Fields *[]externalRef0.ExperimentField `json:"fields,omitempty"` } // ListExperimentHistoryParams defines parameters for ListExperimentHistory. @@ -525,6 +529,17 @@ func (siw *ServerInterfaceWrapper) ListExperiments(w http.ResponseWriter, r *htt return } + // ------------- Optional query parameter "fields" ------------- + if paramValue := r.URL.Query().Get("fields"); paramValue != "" { + + } + + err = runtime.BindQueryParameter("form", true, false, "fields", r.URL.Query(), ¶ms.Fields) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid format for parameter fields: %s", err), http.StatusBadRequest) + return + } + var handler = func(w http.ResponseWriter, r *http.Request) { siw.Handler.ListExperiments(w, r, projectId, params) } diff --git a/treatment-service/testhelper/mockmanagement/controller/experiment.go b/treatment-service/testhelper/mockmanagement/controller/experiment.go index 80e86b36..4821515e 100644 --- a/treatment-service/testhelper/mockmanagement/controller/experiment.go +++ b/treatment-service/testhelper/mockmanagement/controller/experiment.go @@ -39,17 +39,17 @@ func (e Experiment) CreateExperiment(w http.ResponseWriter, r *http.Request, pro return } experiment := schema.Experiment{ - ProjectId: projectId, + ProjectId: &projectId, Description: requestBody.Description, - EndTime: requestBody.EndTime, + EndTime: &requestBody.EndTime, Interval: requestBody.Interval, - Name: requestBody.Name, - Segment: requestBody.Segment, - StartTime: requestBody.StartTime, - Status: requestBody.Status, - Treatments: requestBody.Treatments, - Type: requestBody.Type, - UpdatedBy: *requestBody.UpdatedBy, + Name: &requestBody.Name, + Segment: &requestBody.Segment, + StartTime: &requestBody.StartTime, + Status: &requestBody.Status, + Treatments: &requestBody.Treatments, + Type: &requestBody.Type, + UpdatedBy: requestBody.UpdatedBy, } createdExperiment, err := e.ExperimentStore.CreateExperiment(experiment) if err != nil { @@ -86,7 +86,8 @@ func (e Experiment) EnableExperiment(w http.ResponseWriter, r *http.Request, pro NotFound(w, err) return } - experiment.Status = schema.ExperimentStatusActive + status := schema.ExperimentStatusActive + experiment.Status = &status updatedExperiment, err := e.ExperimentStore.UpdateExperiment(projectId, experimentId, experiment) if err != nil { BadRequest(w, err) @@ -102,7 +103,8 @@ func (e Experiment) DisableExperiment(w http.ResponseWriter, r *http.Request, pr NotFound(w, err) return } - experiment.Status = schema.ExperimentStatusInactive + status := schema.ExperimentStatusInactive + experiment.Status = &status updatedExperiment, err := e.ExperimentStore.UpdateExperiment(projectId, experimentId, experiment) if err != nil { BadRequest(w, err) diff --git a/treatment-service/testhelper/mockmanagement/server/server_test.go b/treatment-service/testhelper/mockmanagement/server/server_test.go index 9b002c56..5c9df2fe 100644 --- a/treatment-service/testhelper/mockmanagement/server/server_test.go +++ b/treatment-service/testhelper/mockmanagement/server/server_test.go @@ -77,24 +77,28 @@ func newRandomProjectSettingsOfSize(size int) []schema.ProjectSettings { func newRandomExperiment(projectId int64) schema.Experiment { description := randomString() + endTime := time.Now().Add(time.Duration(24) * time.Hour) interval := int32(rand.Int()) + name := randomString() + startTime := time.Now() daysOfWeek := []interface{}{int64(rand.Intn(7))} hoursOfDay := []interface{}{int64(rand.Intn(24))} s2Ids := []interface{}{int64(rand.Int())} traffic := int32(rand.Int()) + experimentType := newRandomExperimentType() return schema.Experiment{ - ProjectId: projectId, + ProjectId: &projectId, Description: &description, - EndTime: time.Now().Add(time.Duration(24) * time.Hour), + EndTime: &endTime, Interval: &interval, - Name: randomString(), - Segment: schema.ExperimentSegment{ + Name: &name, + Segment: &schema.ExperimentSegment{ "days_of_week": daysOfWeek, "hours_of_day": hoursOfDay, "s2_ids": s2Ids, }, - StartTime: time.Now(), - Treatments: []schema.ExperimentTreatment{ + StartTime: &startTime, + Treatments: &[]schema.ExperimentTreatment{ { Configuration: map[string]interface{}{ "config": randomBool(), @@ -103,7 +107,7 @@ func newRandomExperiment(projectId int64) schema.Experiment { Traffic: &traffic, }, }, - Type: newRandomExperimentType(), + Type: &experimentType, } } @@ -220,13 +224,13 @@ func (suite *ManagementServiceTestSuite) TestExperimentCreation() { updater := randomString() body := management.CreateExperimentJSONRequestBody{ Description: expectedExperiment.Description, - EndTime: expectedExperiment.EndTime, + EndTime: *expectedExperiment.EndTime, Interval: expectedExperiment.Interval, - Name: expectedExperiment.Name, - Segment: expectedExperiment.Segment, - StartTime: expectedExperiment.StartTime, - Treatments: expectedExperiment.Treatments, - Type: expectedExperiment.Type, + Name: *expectedExperiment.Name, + Segment: *expectedExperiment.Segment, + StartTime: *expectedExperiment.StartTime, + Treatments: *expectedExperiment.Treatments, + Type: *expectedExperiment.Type, UpdatedBy: &updater, } response, err := suite.client.CreateExperimentWithResponse(suite.ctx, 1, body) @@ -236,14 +240,14 @@ func (suite *ManagementServiceTestSuite) TestExperimentCreation() { suite.Require().Equal(expectedExperiment.Name, createdExperiment.Name) suite.Require().Len(suite.store.Experiments, 1) suite.Require().Equal(suite.store.Experiments[0].Name, createdExperiment.Name) - suite.Require().Equal(schema.ExperimentStatusActive, createdExperiment.Status) + suite.Require().Equal(schema.ExperimentStatusActive, *createdExperiment.Status) suite.Require().Equal(expectedExperiment.Treatments, createdExperiment.Treatments) - suite.Require().Equal(updater, createdExperiment.UpdatedBy) + suite.Require().Equal(updater, *createdExperiment.UpdatedBy) publishedUpdate, err := getLastPublishedUpdate(suite.ctx, 1*time.Second, suite.subscription) suite.Require().NoError(err) suite.Require().NotNil(publishedUpdate.Update) - suite.Require().Equal(expectedExperiment.Name, publishedUpdate.GetExperimentCreated().GetExperiment().Name) + suite.Require().Equal(*expectedExperiment.Name, publishedUpdate.GetExperimentCreated().GetExperiment().Name) } func (suite *ManagementServiceTestSuite) TestListExperiment() { @@ -259,9 +263,10 @@ func (suite *ManagementServiceTestSuite) TestListExperiment() { func (suite *ManagementServiceTestSuite) TestGetExperiment() { projectId := suite.projectSettings[0].ProjectId experiment := newRandomExperiment(projectId) - experiment.Id = int64(rand.Int()) + id := int64(rand.Int()) + experiment.Id = &id suite.store.Experiments = []schema.Experiment{experiment} - response, err := suite.client.GetExperimentWithResponse(suite.ctx, projectId, experiment.Id) + response, err := suite.client.GetExperimentWithResponse(suite.ctx, projectId, *experiment.Id) suite.Require().NoError(err) suite.Require().Equal(experiment.Treatments, response.JSON200.Data.Treatments) } @@ -269,13 +274,14 @@ func (suite *ManagementServiceTestSuite) TestGetExperiment() { func (suite *ManagementServiceTestSuite) TestExperimentUpdate() { projectId := suite.projectSettings[0].ProjectId experiment := newRandomExperiment(projectId) - experiment.Id = int64(rand.Int()) + id := int64(rand.Int()) + experiment.Id = &id suite.store.Experiments = []schema.Experiment{experiment} newDescription := "updated" params := management.UpdateExperimentJSONRequestBody{ Description: &newDescription, } - response, err := suite.client.UpdateExperimentWithResponse(suite.ctx, projectId, experiment.Id, params) + response, err := suite.client.UpdateExperimentWithResponse(suite.ctx, projectId, *experiment.Id, params) suite.Require().NoError(err) suite.Require().Equal(params.Description, response.JSON200.Data.Description) } @@ -309,34 +315,38 @@ func (suite *ManagementServiceTestSuite) TestUpdateProjectSettings() { func (suite *ManagementServiceTestSuite) TestEnableExperiment() { projectId := suite.projectSettings[0].ProjectId experiment := newRandomExperiment(projectId) - experiment.Id = int64(rand.Int()) - experiment.Status = schema.ExperimentStatusInactive + id := int64(rand.Int()) + experiment.Id = &id + status := schema.ExperimentStatusInactive + experiment.Status = &status suite.store.Experiments = []schema.Experiment{experiment} - response, err := suite.client.EnableExperimentWithResponse(suite.ctx, projectId, experiment.Id) + response, err := suite.client.EnableExperimentWithResponse(suite.ctx, projectId, *experiment.Id) suite.Require().NoError(err) suite.Require().Equal(http.StatusOK, response.StatusCode()) publishedUpdate, err := getLastPublishedUpdate(suite.ctx, 1*time.Second, suite.subscription) suite.Require().NoError(err) suite.Require().NotNil(publishedUpdate.Update) - suite.Require().Equal(experiment.Id, publishedUpdate.GetExperimentUpdated().GetExperiment().Id) + suite.Require().Equal(*experiment.Id, publishedUpdate.GetExperimentUpdated().GetExperiment().Id) suite.Require().Equal(_pubsub.Experiment_Active, publishedUpdate.GetExperimentUpdated().GetExperiment().Status) } func (suite *ManagementServiceTestSuite) TestDisableExperiment() { projectId := suite.projectSettings[0].ProjectId experiment := newRandomExperiment(projectId) - experiment.Id = int64(rand.Int()) - experiment.Status = schema.ExperimentStatusActive + id := int64(rand.Int()) + experiment.Id = &id + status := schema.ExperimentStatusActive + experiment.Status = &status suite.store.Experiments = []schema.Experiment{experiment} - response, err := suite.client.DisableExperimentWithResponse(suite.ctx, projectId, experiment.Id) + response, err := suite.client.DisableExperimentWithResponse(suite.ctx, projectId, *experiment.Id) suite.Require().NoError(err) suite.Require().Equal(http.StatusOK, response.StatusCode()) publishedUpdate, err := getLastPublishedUpdate(suite.ctx, 1*time.Second, suite.subscription) suite.Require().NoError(err) suite.Require().NotNil(publishedUpdate.Update) - suite.Require().Equal(experiment.Id, publishedUpdate.GetExperimentUpdated().GetExperiment().Id) + suite.Require().Equal(*experiment.Id, publishedUpdate.GetExperimentUpdated().GetExperiment().Id) suite.Require().Equal(_pubsub.Experiment_Inactive, publishedUpdate.GetExperimentUpdated().GetExperiment().Status) } diff --git a/treatment-service/testhelper/mockmanagement/service/store.go b/treatment-service/testhelper/mockmanagement/service/store.go index e8fc2061..78214a09 100644 --- a/treatment-service/testhelper/mockmanagement/service/store.go +++ b/treatment-service/testhelper/mockmanagement/service/store.go @@ -37,7 +37,7 @@ func (i *InMemoryStore) GetExperiment(projectId int64, experimentId int64) (sche i.RLock() defer i.RUnlock() for _, experiment := range i.Experiments { - if experiment.ProjectId == projectId && experiment.Id == experimentId { + if *experiment.ProjectId == projectId && *experiment.Id == experimentId { return experiment, nil } } @@ -52,7 +52,7 @@ func (i *InMemoryStore) ListExperiments(projectId int64, params api.ListExperime defer i.RUnlock() projectExperiments := make([]schema.Experiment, 0) for _, experiment := range i.Experiments { - if experiment.ProjectId == projectId { + if *experiment.ProjectId == projectId { projectExperiments = append(projectExperiments, experiment) } } @@ -62,10 +62,15 @@ func (i *InMemoryStore) ListExperiments(projectId int64, params api.ListExperime func (i *InMemoryStore) CreateExperiment(experiment schema.Experiment) (schema.Experiment, error) { i.Lock() defer i.Unlock() - experiment.Id = int64(len(i.Experiments)) + 1 - experiment.Status = schema.ExperimentStatusActive - experiment.UpdatedAt = time.Now() - experiment.Version = 1 + id := int64(len(i.Experiments)) + 1 + status := schema.ExperimentStatusActive + updatedAt := time.Now() + version := int64(1) + + experiment.Id = &id + experiment.Status = &status + experiment.UpdatedAt = &updatedAt + experiment.Version = &version i.Experiments = append(i.Experiments, experiment) err := i.MessageQueue.PublishNewExperiment(experiment, i.SegmentersTypes) @@ -79,10 +84,17 @@ func (i *InMemoryStore) CreateExperiment(experiment schema.Experiment) (schema.E func (i *InMemoryStore) UpdateExperiment(projectId int64, experimentId int64, experiment schema.Experiment) (schema.Experiment, error) { i.Lock() defer i.Unlock() - experiment.UpdatedAt = time.Now() + updatedAt := time.Now() + + experiment.UpdatedAt = &updatedAt for index, e := range i.Experiments { - if experiment.ProjectId == projectId && e.Id == experimentId { - experiment.Version = e.Version + 1 + if experiment.ProjectId != nil && e.Id != nil && *experiment.ProjectId == projectId && *e.Id == experimentId { + var version int64 + if e.Version != nil { + version = *e.Version + } + version = version + 1 + experiment.Version = &version i.Experiments[index] = experiment } } diff --git a/ui/src/components/table/BasicTable.js b/ui/src/components/table/BasicTable.js index 6a25d12e..7c7f471e 100644 --- a/ui/src/components/table/BasicTable.js +++ b/ui/src/components/table/BasicTable.js @@ -43,7 +43,7 @@ export const BasicTable = ({ pagination={{ pageIndex: page.index, pageSize: page.size, - showPerPageOptions: false, + showPerPageOptions: true, totalItemCount, }} onChange={({ page = {} }) => onPaginationChange(page)} diff --git a/ui/src/config.js b/ui/src/config.js index 3d7ce133..7d667663 100644 --- a/ui/src/config.js +++ b/ui/src/config.js @@ -36,6 +36,7 @@ export const appConfig = { : [{ href: "https://github.com/caraml-dev/xp", label: "XP User Guide" }], pagination: { defaultPageSize: 10, + experimentContextPageSize: 50, }, tables: { defaultTextSize: "s", diff --git a/ui/src/experiments/list/ListExperimentsView.js b/ui/src/experiments/list/ListExperimentsView.js index 512222ce..b49bb1ab 100644 --- a/ui/src/experiments/list/ListExperimentsView.js +++ b/ui/src/experiments/list/ListExperimentsView.js @@ -50,6 +50,7 @@ const ListExperimentsComponent = ({ projectId }) => { query: { page: page.index + 1, page_size: page.size, + fields: ["id", "type", "name", "status_friendly", "tier", "start_time", "end_time", "updated_at"], ...getProcessedFilters(), }, }, diff --git a/ui/src/providers/experiment/context.js b/ui/src/providers/experiment/context.js index 39843a76..c004af5f 100644 --- a/ui/src/providers/experiment/context.js +++ b/ui/src/providers/experiment/context.js @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useState} from "react"; +import React, { useMemo } from "react"; import { useXpApi } from "hooks/useXpApi"; import moment from "moment"; @@ -9,10 +9,6 @@ const ExperimentContext = React.createContext({}); export const ExperimentContextProvider = ({ projectId, children }) => { const { appConfig } = useConfig(); - const [isAllExperimentsLoaded, setIsAllExperimentsLoaded] = useState(false); - const [pageIndex, setPageIndex] = useState(0); - const [allExperiments, setAllExperiments] = useState([]); - const { start_time, end_time } = useMemo( () => { let current_time = moment.utc(); @@ -23,38 +19,24 @@ export const ExperimentContextProvider = ({ projectId, children }) => { [appConfig] ); - const [{ data: { data: experiments, paging }, isLoaded }] = useXpApi( + const [{ data: { data: scheduledAndRunningExperiments }, isLoaded }] = useXpApi( `/projects/${projectId}/experiments`, { query: { start_time: start_time, end_time: end_time, - page: pageIndex + 1, - page_size: appConfig.pagination.defaultPageSize, - status: "active" + fields: ["id", "name","status_friendly", "treatments"], + status_friendly: ["running", "scheduled"] }, }, - { data: [], paging: { total: 0 } } + { data: [] } ); - useEffect(() => { - if (isLoaded) { - if (!!experiments && !isAllExperimentsLoaded) { - setAllExperiments((curExperiments) => [...curExperiments, ...experiments]); - } - if (paging.pages > paging.page) { - setPageIndex(paging.page); - } else { - setIsAllExperimentsLoaded(true); - } - } - }, [isLoaded, experiments, paging, isAllExperimentsLoaded]); - return ( {children} diff --git a/ui/src/turing/components/form/standard_ensembler/LinkedRoutesTable.js b/ui/src/turing/components/form/standard_ensembler/LinkedRoutesTable.js index d81b4ce3..1033ecad 100644 --- a/ui/src/turing/components/form/standard_ensembler/LinkedRoutesTable.js +++ b/ui/src/turing/components/form/standard_ensembler/LinkedRoutesTable.js @@ -9,7 +9,6 @@ import { EuiTextColor, } from "@elastic/eui"; -import { getExperimentStatus } from "services/experiment/ExperimentStatus"; import { LinkedExperimentsContextMenu } from "./LinkedExperimentsContextMenu"; import ExperimentContext from "providers/experiment/context"; @@ -18,7 +17,7 @@ export const LinkedRoutesTable = ({ routes, treatmentConfigRouteNamePath, }) => { - const { allExperiments, isAllExperimentsLoaded } = useContext(ExperimentContext) + const { scheduledAndRunningExperiments, isLoaded } = useContext(ExperimentContext) const [routeToExperimentMappings, setRouteToExperimentMappings] = useState(routes.reduce((m, r) => {m[r.id] = {running: {}, scheduled: {}}; return m}, {})); @@ -30,19 +29,19 @@ export const LinkedRoutesTable = ({ // reset loaded routeToExperimentMappings if treatmentConfigRouteNamePath or routes changes useEffect(() => { - if (isAllExperimentsLoaded) { + if (isLoaded) { let newRouteToExperimentMappings = routes.reduce((m, r) => {m[r.id] = {running: {}, scheduled: {}}; return m}, {}); - for (let experiment of allExperiments) { + for (let experiment of scheduledAndRunningExperiments) { for (let treatment of experiment.treatments) { let configRouteName = getRouteName(treatment.configuration, treatmentConfigRouteNamePath); if (typeof configRouteName === 'string' && configRouteName in newRouteToExperimentMappings) { - newRouteToExperimentMappings[configRouteName][getExperimentStatus(experiment).label.toLowerCase()][experiment.id] = experiment; + newRouteToExperimentMappings[configRouteName][experiment.status_friendly][experiment.id] = experiment; } } } setRouteToExperimentMappings(newRouteToExperimentMappings); } - }, [treatmentConfigRouteNamePath, stringifiedRoutes, routes, isAllExperimentsLoaded, allExperiments]); + }, [treatmentConfigRouteNamePath, stringifiedRoutes, routes, isLoaded, scheduledAndRunningExperiments]); const columns = [ { @@ -99,7 +98,7 @@ export const LinkedRoutesTable = ({ }, ]; - return isAllExperimentsLoaded ? ( + return isLoaded ? ( r.id !== "")}