From 5483d6b30626140d2eff587182208eebf4fbd7af Mon Sep 17 00:00:00 2001 From: Krithika Sundararajan Date: Tue, 20 Sep 2022 18:07:55 +0800 Subject: [PATCH] Add additional experiment metadata to Fetch Treatment response (#36) * Add switchback_window_id and experiment_version to the fetch treatment response schema * Add version field to experiments table * Treatment service changes for switchback window id and experiment version * Fix plugin test case * Fix file formatting * Address PR comments * Address PR comments - UI formatting Co-authored-by: Krithika Sundararajan --- Makefile | 4 +- api/proto/experiment.proto | 1 + api/schema.yaml | 24 ++++++ .../ManagementClientInterface.go | 17 ++++- .../TreatmentClientInterface.go | 25 +++++-- common/api/schema/schema.go | 75 +++++++++++-------- common/pubsub/experiment.pb.go | 55 ++++++++------ .../controller/experiment_controller_test.go | 9 ++- .../000003_experiment_version.down.sql | 1 + .../000003_experiment_version.up.sql | 10 +++ management-service/models/experiment.go | 5 ++ management-service/models/experiment_test.go | 9 ++- .../services/experiment_history_service.go | 16 +--- .../services/experiment_service.go | 3 + .../services/experiment_service_test.go | 6 ++ .../services/pubsub_publisher_service_test.go | 1 + .../turing/runner/experiment_runner_test.go | 18 +++-- tests/e2e/test_experiment_management.py | 16 ++++ tests/e2e/test_treatment_selection.py | 4 + treatment-service/controller/treatment.go | 8 +- .../fetch_treatment_it_test.go | 26 +++++++ treatment-service/models/storage_test.go | 2 +- treatment-service/models/typeconverter.go | 9 +++ .../models/typeconverter_test.go | 26 +++++++ .../services/treatment_service.go | 41 ++++++---- .../services/treatment_service_test.go | 48 +++++++++--- .../mockmanagement/service/store.go | 6 +- .../components/version_badge/VersionBadge.js | 10 +++ .../details/ExperimentDetailsView.js | 21 +++++- .../details/ExperimentHistoryDetailsView.js | 4 +- .../segmenter_section/SegmenterCard.js | 3 +- .../segmenter_section/SegmenterSettings.js | 7 +- 32 files changed, 383 insertions(+), 127 deletions(-) rename clients/testutils/mocks/{ => management}/ManagementClientInterface.go (98%) rename clients/testutils/mocks/{ => treatment}/TreatmentClientInterface.go (66%) create mode 100644 management-service/database/db-migrations/000003_experiment_version.down.sql create mode 100644 management-service/database/db-migrations/000003_experiment_version.up.sql create mode 100644 ui/src/components/version_badge/VersionBadge.js diff --git a/Makefile b/Makefile index c2544d6d..132ed2da 100644 --- a/Makefile +++ b/Makefile @@ -49,8 +49,8 @@ generate-api: oapi-codegen -templates api/templates/chi -config api/management/server.conf api/experiments.yaml oapi-codegen -config api/mockmanagement/server.conf api/experiments.yaml oapi-codegen -config api/treatment/server.conf api/treatment.yaml - cd clients/management/ && mockery --name=ClientInterface --output=../../clients/testutils/mocks --filename=ManagementClientInterface.go - cd clients/treatment/ && mockery --name=TreatmentClientInterface --output=../../clients/testutils/mocks --filename=TreatmentClientInterface.go + cd clients/management/ && mockery --name=ClientInterface --output=../../clients/testutils/mocks/management --filename=ManagementClientInterface.go + cd clients/treatment/ && mockery --name=ClientInterface --output=../../clients/testutils/mocks/treatment --filename=TreatmentClientInterface.go # ================================== # Setup Management & Treatment Services diff --git a/api/proto/experiment.proto b/api/proto/experiment.proto index acdbe544..a761d152 100644 --- a/api/proto/experiment.proto +++ b/api/proto/experiment.proto @@ -41,6 +41,7 @@ message Experiment { repeated ExperimentTreatment treatments = 11; google.protobuf.Timestamp updated_at = 12; + int64 version = 13; // Experiment version } message ExperimentTreatment { diff --git a/api/schema.yaml b/api/schema.yaml index f33dbcf7..a0af58a4 100644 --- a/api/schema.yaml +++ b/api/schema.yaml @@ -21,6 +21,23 @@ components: configuration: type: object description: Custom configuration associated with the given treatment + SelectedTreatmentMetadata: + required: + - experiment_version + - experiment_type + type: object + properties: + experiment_version: + type: integer + format: int64 + experiment_type: + $ref: '#/components/schemas/ExperimentType' + switchback_window_id: + type: integer + format: int64 + description: | + The window id since the beginning of the current version of the Switchback experiment. + This field will only be set for Switchback experiments and the window id starts at 0. ExperimentTreatment: required: - configuration @@ -116,6 +133,7 @@ components: - experiment_id - experiment_name - treatment + - metadata type: object properties: experiment_id: @@ -125,6 +143,8 @@ components: type: string treatment: $ref: '#/components/schemas/SelectedTreatmentData' + metadata: + $ref: '#/components/schemas/SelectedTreatmentMetadata' Experiment: required: - project_id @@ -142,6 +162,7 @@ components: - created_at - updated_at - updated_by + - version type: object properties: description: @@ -185,6 +206,9 @@ components: nullable: true tier: $ref: '#/components/schemas/ExperimentTier' + version: + type: integer + format: int64 ExperimentHistory: required: - experiment_id diff --git a/clients/testutils/mocks/ManagementClientInterface.go b/clients/testutils/mocks/management/ManagementClientInterface.go similarity index 98% rename from clients/testutils/mocks/ManagementClientInterface.go rename to clients/testutils/mocks/management/ManagementClientInterface.go index d22f8a3b..a77b8b37 100644 --- a/clients/testutils/mocks/ManagementClientInterface.go +++ b/clients/testutils/mocks/management/ManagementClientInterface.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.10.4. DO NOT EDIT. +// Code generated by mockery v2.14.0. DO NOT EDIT. package mocks @@ -1337,3 +1337,18 @@ func (_m *ClientInterface) ValidateEntityWithBody(ctx context.Context, contentTy return r0, r1 } + +type mockConstructorTestingTNewClientInterface interface { + mock.TestingT + Cleanup(func()) +} + +// NewClientInterface creates a new instance of ClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClientInterface(t mockConstructorTestingTNewClientInterface) *ClientInterface { + mock := &ClientInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/clients/testutils/mocks/TreatmentClientInterface.go b/clients/testutils/mocks/treatment/TreatmentClientInterface.go similarity index 66% rename from clients/testutils/mocks/TreatmentClientInterface.go rename to clients/testutils/mocks/treatment/TreatmentClientInterface.go index 119938b6..2a93fbf5 100644 --- a/clients/testutils/mocks/TreatmentClientInterface.go +++ b/clients/testutils/mocks/treatment/TreatmentClientInterface.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.10.4. DO NOT EDIT. +// Code generated by mockery v2.14.0. DO NOT EDIT. package mocks @@ -13,13 +13,13 @@ import ( treatment "github.com/caraml-dev/xp/clients/treatment" ) -// TreatmentClientInterface is an autogenerated mock type for the TreatmentClientInterface type -type TreatmentClientInterface struct { +// ClientInterface is an autogenerated mock type for the ClientInterface type +type ClientInterface struct { mock.Mock } // FetchTreatment provides a mock function with given fields: ctx, projectId, params, body, reqEditors -func (_m *TreatmentClientInterface) FetchTreatment(ctx context.Context, projectId int64, params *treatment.FetchTreatmentParams, body treatment.FetchTreatmentJSONRequestBody, reqEditors ...treatment.RequestEditorFn) (*http.Response, error) { +func (_m *ClientInterface) FetchTreatment(ctx context.Context, projectId int64, params *treatment.FetchTreatmentParams, body treatment.FetchTreatmentJSONRequestBody, reqEditors ...treatment.RequestEditorFn) (*http.Response, error) { _va := make([]interface{}, len(reqEditors)) for _i := range reqEditors { _va[_i] = reqEditors[_i] @@ -49,7 +49,7 @@ func (_m *TreatmentClientInterface) FetchTreatment(ctx context.Context, projectI } // FetchTreatmentWithBody provides a mock function with given fields: ctx, projectId, params, contentType, body, reqEditors -func (_m *TreatmentClientInterface) FetchTreatmentWithBody(ctx context.Context, projectId int64, params *treatment.FetchTreatmentParams, contentType string, body io.Reader, reqEditors ...treatment.RequestEditorFn) (*http.Response, error) { +func (_m *ClientInterface) FetchTreatmentWithBody(ctx context.Context, projectId int64, params *treatment.FetchTreatmentParams, contentType string, body io.Reader, reqEditors ...treatment.RequestEditorFn) (*http.Response, error) { _va := make([]interface{}, len(reqEditors)) for _i := range reqEditors { _va[_i] = reqEditors[_i] @@ -77,3 +77,18 @@ func (_m *TreatmentClientInterface) FetchTreatmentWithBody(ctx context.Context, return r0, r1 } + +type mockConstructorTestingTNewClientInterface interface { + mock.TestingT + Cleanup(func()) +} + +// NewClientInterface creates a new instance of ClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewClientInterface(t mockConstructorTestingTNewClientInterface) *ClientInterface { + mock := &ClientInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/common/api/schema/schema.go b/common/api/schema/schema.go index a4b05836..020f49aa 100644 --- a/common/api/schema/schema.go +++ b/common/api/schema/schema.go @@ -109,6 +109,7 @@ type Experiment struct { Type ExperimentType `json:"type"` UpdatedAt time.Time `json:"updated_at"` UpdatedBy string `json:"updated_by"` + Version int64 `json:"version"` } // ExperimentHistory defines model for ExperimentHistory. @@ -291,9 +292,10 @@ type SegmenterValues interface{} // SelectedTreatment defines model for SelectedTreatment. type SelectedTreatment struct { - ExperimentId int64 `json:"experiment_id"` - ExperimentName string `json:"experiment_name"` - Treatment SelectedTreatmentData `json:"treatment"` + ExperimentId int64 `json:"experiment_id"` + ExperimentName string `json:"experiment_name"` + Metadata SelectedTreatmentMetadata `json:"metadata"` + Treatment SelectedTreatmentData `json:"treatment"` } // SelectedTreatmentData defines model for SelectedTreatmentData. @@ -311,6 +313,16 @@ type SelectedTreatmentData struct { Traffic *int32 `json:"traffic,omitempty"` } +// SelectedTreatmentMetadata defines model for SelectedTreatmentMetadata. +type SelectedTreatmentMetadata struct { + ExperimentType ExperimentType `json:"experiment_type"` + ExperimentVersion int64 `json:"experiment_version"` + + // The window id since the beginning of the current version of the Switchback experiment. + // This field will only be set for Switchback experiments and the window id starts at 0. + SwitchbackWindowId *int64 `json:"switchback_window_id,omitempty"` +} + // Treatment defines model for Treatment. type Treatment struct { Configuration *map[string]interface{} `json:"configuration,omitempty"` @@ -453,33 +465,36 @@ func (a SegmenterOptions) MarshalJSON() ([]byte, error) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/common/pubsub/experiment.pb.go b/common/pubsub/experiment.pb.go index ed45d3e5..055a3250 100644 --- a/common/pubsub/experiment.pb.go +++ b/common/pubsub/experiment.pb.go @@ -272,6 +272,7 @@ type Experiment struct { EndTime *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` Treatments []*ExperimentTreatment `protobuf:"bytes,11,rep,name=treatments,proto3" json:"treatments,omitempty"` UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + Version int64 `protobuf:"varint,13,opt,name=version,proto3" json:"version,omitempty"` // Experiment version } func (x *Experiment) Reset() { @@ -390,6 +391,13 @@ func (x *Experiment) GetUpdatedAt() *timestamppb.Timestamp { return nil } +func (x *Experiment) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + type ExperimentTreatment struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -473,7 +481,7 @@ var file_api_proto_experiment_proto_rawDesc = []byte{ 0x0a, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x75, 0x62, 0x73, 0x75, 0x62, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, - 0x74, 0x22, 0xe5, 0x05, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, + 0x74, 0x22, 0xff, 0x05, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, @@ -507,28 +515,29 @@ var file_api_proto_experiment_proto_rawDesc = []byte{ 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x1a, 0x5b, 0x0a, 0x0d, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, - 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x65, - 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x1f, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x5f, 0x42, - 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x62, 0x61, 0x63, 0x6b, - 0x10, 0x01, 0x22, 0x22, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x0a, 0x06, - 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x49, 0x6e, 0x61, 0x63, - 0x74, 0x69, 0x76, 0x65, 0x10, 0x01, 0x22, 0x21, 0x0a, 0x04, 0x54, 0x69, 0x65, 0x72, 0x12, 0x0b, - 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4f, - 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x10, 0x01, 0x22, 0x74, 0x0a, 0x13, 0x45, 0x78, 0x70, - 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x72, 0x65, 0x61, 0x74, 0x6d, 0x65, 0x6e, 0x74, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x12, 0x2f, - 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, - 0x09, 0x5a, 0x07, 0x2f, 0x70, 0x75, 0x62, 0x73, 0x75, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x65, 0x64, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x5b, + 0x0a, 0x0d, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x34, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1f, 0x0a, 0x04, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x5f, 0x42, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, + 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x62, 0x61, 0x63, 0x6b, 0x10, 0x01, 0x22, 0x22, 0x0a, 0x06, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x49, 0x6e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x10, 0x01, + 0x22, 0x21, 0x0a, 0x04, 0x54, 0x69, 0x65, 0x72, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61, + 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x10, 0x01, 0x22, 0x74, 0x0a, 0x13, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, + 0x74, 0x54, 0x72, 0x65, 0x61, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x07, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, + 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x09, 0x5a, 0x07, 0x2f, 0x70, 0x75, + 0x62, 0x73, 0x75, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/management-service/controller/experiment_controller_test.go b/management-service/controller/experiment_controller_test.go index bc452a93..bff973b6 100644 --- a/management-service/controller/experiment_controller_test.go +++ b/management-service/controller/experiment_controller_test.go @@ -81,7 +81,8 @@ func (s *ExperimentControllerTestSuite) SetupSuite() { "type": "", "start_time": "0001-01-01T00:00:00Z", "updated_at": "0001-01-01T00:00:00Z", - "updated_by": "" + "updated_by": "", + "version": 0 }`, `{ "project_id": 2, @@ -98,7 +99,8 @@ func (s *ExperimentControllerTestSuite) SetupSuite() { "type": "", "start_time": "0001-01-01T00:00:00Z", "updated_at": "0001-01-01T00:00:00Z", - "updated_by": "" + "updated_by": "", + "version": 0 }`, `{ "project_id": 5, @@ -115,7 +117,8 @@ func (s *ExperimentControllerTestSuite) SetupSuite() { "type": "", "start_time": "0001-01-01T00:00:00Z", "updated_at": "0001-01-01T00:00:00Z", - "updated_by": "" + "updated_by": "", + "version": 0 }`, } s.expectedErrorResponseFormat = `{"code":"%[1]v", "error":%[2]v, "message":%[2]v}` diff --git a/management-service/database/db-migrations/000003_experiment_version.down.sql b/management-service/database/db-migrations/000003_experiment_version.down.sql new file mode 100644 index 00000000..8b1e1f34 --- /dev/null +++ b/management-service/database/db-migrations/000003_experiment_version.down.sql @@ -0,0 +1 @@ +ALTER TABLE experiments DROP COLUMN version; diff --git a/management-service/database/db-migrations/000003_experiment_version.up.sql b/management-service/database/db-migrations/000003_experiment_version.up.sql new file mode 100644 index 00000000..27965fe3 --- /dev/null +++ b/management-service/database/db-migrations/000003_experiment_version.up.sql @@ -0,0 +1,10 @@ +ALTER TABLE experiments ADD version integer NOT NULL DEFAULT 1; + +-- Set the version number for existing records +WITH agg_data AS ( + SELECT t1.id AS id, count(*) AS num_history + FROM experiments t1 INNER JOIN experiment_history t2 ON t1.id = t2.experiment_id + GROUP BY t1.id +) +UPDATE experiments SET version = num_history+1 +FROM agg_data WHERE experiments.id = agg_data.id; diff --git a/management-service/models/experiment.go b/management-service/models/experiment.go index c4a80473..a8ba4a91 100644 --- a/management-service/models/experiment.go +++ b/management-service/models/experiment.go @@ -43,6 +43,9 @@ type Experiment struct { // as retrieved from the MLP API. ProjectID ID `json:"project_id"` + // Version is the version number of the experiment, starts at 1 for each experiment. + Version int64 `json:"version"` + // Name is the experiment's name Name string `json:"name"` // Description is an optional value that has additional info on the experiment @@ -86,6 +89,7 @@ func (e *Experiment) ToApiSchema(segmentersType map[string]schema.SegmenterType) CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, UpdatedBy: e.UpdatedBy, + Version: e.Version, } } @@ -144,5 +148,6 @@ func (e *Experiment) ToProtoSchema(segmentersType map[string]schema.SegmenterTyp Type: experimentType, StartTime: startTime, UpdatedAt: updatedAt, + Version: e.Version, }, nil } diff --git a/management-service/models/experiment_test.go b/management-service/models/experiment_test.go index f09c9e55..a795fa44 100644 --- a/management-service/models/experiment_test.go +++ b/management-service/models/experiment_test.go @@ -44,8 +44,9 @@ var testExperiment = Experiment{ Traffic: &testExperimentTraffic, }, }), - Type: ExperimentTypeSwitchback, - Tier: ExperimentTierDefault, + Type: ExperimentTypeSwitchback, + Tier: ExperimentTierDefault, + Version: 2, } func TestExperimentToApiSchema(t *testing.T) { @@ -80,6 +81,7 @@ func TestExperimentToApiSchema(t *testing.T) { Segment: schema.ExperimentSegment{ "string_segmenter": []string{"seg-1"}, }, + Version: 2, }, testExperiment.ToApiSchema(segmenterTypes)) } @@ -108,6 +110,7 @@ func TestExperimentToProtoSchema(t *testing.T) { Segments: map[string]*_segmenters.ListSegmenterValue{ "string_segmenter": _utils.StringSliceToListSegmenterValue(&stringSegment), }, - Tier: _pubsub.Experiment_Default, + Tier: _pubsub.Experiment_Default, + Version: 2, }, protoRecord) } diff --git a/management-service/services/experiment_history_service.go b/management-service/services/experiment_history_service.go index cc456a53..e1f0430d 100644 --- a/management-service/services/experiment_history_service.go +++ b/management-service/services/experiment_history_service.go @@ -81,20 +81,12 @@ func (svc *experimentHistoryService) GetExperimentHistory( } func (svc *experimentHistoryService) CreateExperimentHistory(experiment *models.Experiment) (*models.ExperimentHistory, error) { - var history []*models.ExperimentHistory - var count int64 - // Begin transaction - so that getting the current count and creating the new record are - // done in a single transaction. - tx := svc.db.Begin() - // Get the count of the existing experiment history records - svc.query().Where("experiment_id = ?", experiment.ID).Model(&history).Count(&count) - // Create the new history record - newHistoryRecord, err := svc.save(&models.ExperimentHistory{ + return svc.save(&models.ExperimentHistory{ Model: models.Model{ CreatedAt: experiment.UpdatedAt, }, ExperimentID: experiment.ID, - Version: count + 1, + Version: experiment.Version, Description: experiment.Description, EndTime: experiment.EndTime, Interval: experiment.Interval, @@ -107,10 +99,6 @@ func (svc *experimentHistoryService) CreateExperimentHistory(experiment *models. StartTime: experiment.StartTime, UpdatedBy: experiment.UpdatedBy, }) - if err != nil { - return nil, err - } - return newHistoryRecord, tx.Commit().Error } func (svc *experimentHistoryService) GetDBRecord( diff --git a/management-service/services/experiment_service.go b/management-service/services/experiment_service.go index 83cbab57..90dc75fa 100644 --- a/management-service/services/experiment_service.go +++ b/management-service/services/experiment_service.go @@ -255,6 +255,7 @@ func (svc *experimentService) CreateExperiment( StartTime: expData.StartTime, EndTime: expData.EndTime, UpdatedBy: *expData.UpdatedBy, + Version: 1, } // Validate the experiment against the project settings' treatment schema and validation url @@ -357,6 +358,8 @@ func (svc *experimentService) UpdateExperiment( ProjectID: curExperiment.ProjectID, Name: curExperiment.Name, Type: curExperiment.Type, + // Increment the version + Version: curExperiment.Version + 1, // Add the new data Description: expData.Description, Interval: expData.Interval, diff --git a/management-service/services/experiment_service_test.go b/management-service/services/experiment_service_test.go index 1899d953..3de99550 100644 --- a/management-service/services/experiment_service_test.go +++ b/management-service/services/experiment_service_test.go @@ -335,6 +335,7 @@ func testCreateUpdateExperiment(s *ExperimentServiceTestSuite) { Tier: models.ExperimentTierDefault, Type: models.ExperimentTypeSwitchback, UpdatedBy: updatedBy, + Version: 1, }, *expResponse) // Update Experiment @@ -549,6 +550,7 @@ func createTestExperiments(db *gorm.DB) (models.Settings, []*models.Experiment, CreatedAt: time.Date(2020, 4, 1, 4, 5, 6, 0, time.UTC), UpdatedAt: time.Date(2020, 4, 1, 4, 5, 6, 0, time.UTC), }, + Version: 1, }, { ProjectID: models.ID(1), @@ -567,6 +569,7 @@ func createTestExperiments(db *gorm.DB) (models.Settings, []*models.Experiment, CreatedAt: time.Date(2020, 4, 1, 4, 5, 6, 0, time.UTC), UpdatedAt: time.Date(2020, 4, 1, 4, 5, 6, 0, time.UTC), }, + Version: 1, }, { ProjectID: models.ID(1), @@ -585,6 +588,7 @@ func createTestExperiments(db *gorm.DB) (models.Settings, []*models.Experiment, UpdatedAt: time.Date(2020, 4, 1, 4, 5, 6, 0, time.UTC), }, Description: &description, + Version: 1, }, { ProjectID: models.ID(2), @@ -602,6 +606,7 @@ func createTestExperiments(db *gorm.DB) (models.Settings, []*models.Experiment, CreatedAt: time.Date(2020, 4, 1, 4, 5, 6, 0, time.UTC), UpdatedAt: time.Date(2020, 4, 1, 4, 5, 6, 0, time.UTC), }, + Version: 1, }, } @@ -742,6 +747,7 @@ func setupMockSegmenterService() services.SegmenterService { Treatments: nil, StartTime: time.Date(2021, 2, 2, 3, 5, 7, 0, time.UTC), EndTime: time.Date(2021, 2, 2, 3, 5, 8, 0, time.UTC), + Version: 1, }, }). Return(nil) diff --git a/management-service/services/pubsub_publisher_service_test.go b/management-service/services/pubsub_publisher_service_test.go index ccb5fa64..d0e0d93a 100644 --- a/management-service/services/pubsub_publisher_service_test.go +++ b/management-service/services/pubsub_publisher_service_test.go @@ -345,6 +345,7 @@ func (s *PubSubServiceTestSuite) TestExperimentServiceCreateUpdatePublish() { Type: models.ExperimentTypeSwitchback, Tier: models.ExperimentTierDefault, UpdatedBy: "integration-test", + Version: 1, }, *expResponse) // Check Published Create message diff --git a/plugins/turing/runner/experiment_runner_test.go b/plugins/turing/runner/experiment_runner_test.go index 4c180405..1e499016 100644 --- a/plugins/turing/runner/experiment_runner_test.go +++ b/plugins/turing/runner/experiment_runner_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/caraml-dev/xp/clients/testutils/mocks" + mocks "github.com/caraml-dev/xp/clients/testutils/mocks/treatment" treatmentClient "github.com/caraml-dev/xp/clients/treatment" "github.com/caraml-dev/xp/plugins/turing/config" "github.com/caraml-dev/xp/plugins/turing/internal/testutils" @@ -143,9 +143,9 @@ func TestMissingRequestValue(t *testing.T) { } func TestFetchTreatment(t *testing.T) { - mockTreatmentClientInterface := mocks.TreatmentClientInterface{} - mockTreatmentClient := treatmentClient.ClientWithResponses{ClientInterface: &mockTreatmentClientInterface} - mockTreatmentClientInterface.On("FetchTreatmentWithBody", + mockClientInterface := mocks.ClientInterface{} + mockTreatmentClient := treatmentClient.ClientWithResponses{ClientInterface: &mockClientInterface} + mockClientInterface.On("FetchTreatmentWithBody", context.Background(), int64(1), &treatmentClient.FetchTreatmentParams{PassKey: "abc"}, @@ -158,7 +158,7 @@ func TestFetchTreatment(t *testing.T) { Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)), }, nil) - mockTreatmentClientInterface.On("FetchTreatmentWithBody", + mockClientInterface.On("FetchTreatmentWithBody", context.Background(), int64(2), &treatmentClient.FetchTreatmentParams{PassKey: "abc"}, @@ -180,6 +180,10 @@ func TestFetchTreatment(t *testing.T) { }, "name": "test_experiment-control", "traffic": 50 + }, + "metadata": { + "experiment_version": 1, + "experiment_type": "A/B" } } }`, @@ -240,6 +244,10 @@ func TestFetchTreatment(t *testing.T) { "configuration": {"foo":"bar"}, "name": "test_experiment-control", "traffic": 50 + }, + "metadata": { + "experiment_version": 1, + "experiment_type": "A/B" } }`), }, diff --git a/tests/e2e/test_experiment_management.py b/tests/e2e/test_experiment_management.py index ea26ec6e..bec703b0 100644 --- a/tests/e2e/test_experiment_management.py +++ b/tests/e2e/test_experiment_management.py @@ -75,6 +75,10 @@ def test_simple_experiment_creation(xp_client: XPClient, xp_project): "experiment_id": experiment["id"], "experiment_name": experiment["name"], "treatment": experiment["treatments"][0], + "metadata": { + "experiment_type": "A/B", + "experiment_version": 1, + }, } @@ -325,6 +329,10 @@ def check(): "experiment_id": experiment["id"], "experiment_name": experiment["name"], "treatment": experiment["treatments"][0], + "metadata": { + "experiment_type": "A/B", + "experiment_version": 2, + }, } eventually(check) @@ -377,6 +385,10 @@ def test_all_segmenters(xp_client: XPClient, xp_project): "experiment_id": experiment["id"], "experiment_name": experiment["name"], "treatment": experiment["treatments"][0], + "metadata": { + "experiment_type": "A/B", + "experiment_version": 1, + }, } @@ -466,6 +478,10 @@ def test_custom_segmenters(xp_client: XPClient, xp_project): "experiment_id": experiment["id"], "experiment_name": experiment["name"], "treatment": experiment["treatments"][0], + "metadata": { + "experiment_type": "A/B", + "experiment_version": 1, + }, } # Disable experiments and remove custom segmenter so that it can be deleted diff --git a/tests/e2e/test_treatment_selection.py b/tests/e2e/test_treatment_selection.py index fef311f5..b2c8a307 100644 --- a/tests/e2e/test_treatment_selection.py +++ b/tests/e2e/test_treatment_selection.py @@ -94,6 +94,10 @@ def test_experiment_selection(xp_client: XPClient, xp_project): "experiment_id": exp_5["id"], "experiment_name": exp_5["name"], "treatment": exp_5["treatments"][0], + "metadata": { + "experiment_version": 1, + "experiment_type": "A/B", + }, } diff --git a/treatment-service/controller/treatment.go b/treatment-service/controller/treatment.go index ebfc758d..1ad5ac7f 100644 --- a/treatment-service/controller/treatment.go +++ b/treatment-service/controller/treatment.go @@ -165,7 +165,8 @@ func (t TreatmentController) FetchTreatment(w http.ResponseWriter, r *http.Reque return } - selectedTreatment, err = t.TreatmentService.GetTreatment(filteredExperiment, randomizationKeyValue) + var switchbackWindowId *int64 + selectedTreatment, switchbackWindowId, err = t.TreatmentService.GetTreatment(filteredExperiment, randomizationKeyValue) if err != nil { statusCode = http.StatusInternalServerError ErrorResponse(w, statusCode, err, &requestId) @@ -179,6 +180,11 @@ func (t TreatmentController) FetchTreatment(w http.ResponseWriter, r *http.Reque ExperimentId: filteredExperiment.Id, ExperimentName: filteredExperiment.Name, Treatment: treatmentRepr, + Metadata: schema.SelectedTreatmentMetadata{ + ExperimentVersion: filteredExperiment.Version, + ExperimentType: models.ProtobufExperimentTypeToOpenAPI(filteredExperiment.Type), + SwitchbackWindowId: switchbackWindowId, + }, } response := api.FetchTreatmentSuccess{ Data: &treatment, diff --git a/treatment-service/integration-test/fetch_treatment_it_test.go b/treatment-service/integration-test/fetch_treatment_it_test.go index 4a57695c..ab0ad840 100644 --- a/treatment-service/integration-test/fetch_treatment_it_test.go +++ b/treatment-service/integration-test/fetch_treatment_it_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "log" + "math" "math/rand" "net" "net/http" @@ -219,6 +220,7 @@ func generateExperiments() []schema.Experiment { StartTime: startTime, Treatments: treatments, Type: experimentType, + Version: int64(1), } experiments = append(experiments, experiment) } @@ -437,6 +439,10 @@ func (suite *TreatmentServiceTestSuite) TestAdditionalFilters() { }, Traffic: int32Ptr(50), }, + Metadata: schema.SelectedTreatmentMetadata{ + ExperimentVersion: int64(1), + ExperimentType: schema.ExperimentTypeAB, + }, } suite.Require().NoError(err) @@ -511,6 +517,10 @@ func (suite *TreatmentServiceTestSuite) TestTimeFilter() { }, Traffic: int32Ptr(40), }, + Metadata: schema.SelectedTreatmentMetadata{ + ExperimentVersion: int64(1), + ExperimentType: schema.ExperimentTypeAB, + }, } suite.Require().NoError(err) @@ -578,6 +588,10 @@ func (suite *TreatmentServiceTestSuite) TestExperimentUpdates() { }, Traffic: traffic, }, + Metadata: schema.SelectedTreatmentMetadata{ + ExperimentVersion: int64(1), + ExperimentType: schema.ExperimentTypeAB, + }, } suite.Require().NoError(err) @@ -630,6 +644,10 @@ func (suite *TreatmentServiceTestSuite) TestExperimentUpdates() { }, Traffic: newTraffic, }, + Metadata: schema.SelectedTreatmentMetadata{ + ExperimentVersion: int64(2), + ExperimentType: schema.ExperimentTypeAB, + }, } suite.Require().NoError(err) @@ -761,6 +779,9 @@ func (suite *TreatmentServiceTestSuite) TestAllFiltersSwitchback() { requestReader, ) + // Calculate switchback window id + startTime, _ := getStartEndTime() + windowId := int64(math.Floor(time.Since(startTime).Minutes() / float64(30))) expectedBody := schema.SelectedTreatment{ ExperimentName: "sg-exp-3", ExperimentId: 3, @@ -771,6 +792,11 @@ func (suite *TreatmentServiceTestSuite) TestAllFiltersSwitchback() { }, Traffic: int32Ptr(70), }, + Metadata: schema.SelectedTreatmentMetadata{ + ExperimentVersion: int64(1), + ExperimentType: schema.ExperimentTypeSwitchback, + SwitchbackWindowId: &windowId, + }, } suite.Require().NoError(err) diff --git a/treatment-service/models/storage_test.go b/treatment-service/models/storage_test.go index 1a5080aa..3d4ad331 100644 --- a/treatment-service/models/storage_test.go +++ b/treatment-service/models/storage_test.go @@ -23,7 +23,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" managementClient "github.com/caraml-dev/xp/clients/management" - "github.com/caraml-dev/xp/clients/testutils/mocks" + mocks "github.com/caraml-dev/xp/clients/testutils/mocks/management" "github.com/caraml-dev/xp/common/api/schema" _pubsub "github.com/caraml-dev/xp/common/pubsub" _segmenters "github.com/caraml-dev/xp/common/segmenters" diff --git a/treatment-service/models/typeconverter.go b/treatment-service/models/typeconverter.go index 5d94d6ef..1ce81c4f 100644 --- a/treatment-service/models/typeconverter.go +++ b/treatment-service/models/typeconverter.go @@ -44,6 +44,14 @@ func OpenAPIProjectSettingsSpecToProtobuf(projectSettings schema.ProjectSettings } } +func ProtobufExperimentTypeToOpenAPI(experimentType _pubsub.Experiment_Type) schema.ExperimentType { + conversionMap := map[_pubsub.Experiment_Type]schema.ExperimentType{ + _pubsub.Experiment_A_B: schema.ExperimentTypeAB, + _pubsub.Experiment_Switchback: schema.ExperimentTypeSwitchback, + } + return conversionMap[experimentType] +} + func OpenAPIExperimentSpecToProtobuf( xpExperiment schema.Experiment, segmentersType map[string]schema.SegmenterType, @@ -123,6 +131,7 @@ func OpenAPIExperimentSpecToProtobuf( StartTime: ×tamppb.Timestamp{Seconds: xpExperiment.StartTime.Unix()}, EndTime: ×tamppb.Timestamp{Seconds: xpExperiment.EndTime.Unix()}, UpdatedAt: ×tamppb.Timestamp{Seconds: xpExperiment.UpdatedAt.Unix()}, + Version: xpExperiment.Version, }, nil } diff --git a/treatment-service/models/typeconverter_test.go b/treatment-service/models/typeconverter_test.go index 06a6ac38..025378f4 100644 --- a/treatment-service/models/typeconverter_test.go +++ b/treatment-service/models/typeconverter_test.go @@ -109,6 +109,7 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { 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, }, Expected: &pubsub.Experiment{ ProjectId: 1, @@ -135,6 +136,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: 2, }, }, { @@ -151,6 +153,7 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { 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, }, Expected: &pubsub.Experiment{ ProjectId: 3, @@ -165,6 +168,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, }, }, } @@ -180,3 +184,25 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) { }) } } + +func TestProtobufExperimentTypeToOpenAPI(t *testing.T) { + tests := map[string]struct { + Input pubsub.Experiment_Type + Expected schema.ExperimentType + }{ + "a/b": { + Input: pubsub.Experiment_A_B, + Expected: schema.ExperimentTypeAB, + }, + "switchback": { + Input: pubsub.Experiment_Switchback, + Expected: schema.ExperimentTypeSwitchback, + }, + } + + for name, data := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, data.Expected, models.ProtobufExperimentTypeToOpenAPI(data.Input)) + }) + } +} diff --git a/treatment-service/services/treatment_service.go b/treatment-service/services/treatment_service.go index 205610bf..8419ba19 100644 --- a/treatment-service/services/treatment_service.go +++ b/treatment-service/services/treatment_service.go @@ -14,8 +14,9 @@ import ( ) type TreatmentService interface { - // GetTreatment returns treatment based on provided experiment - GetTreatment(experiment *_pubsub.Experiment, randomizationValue *string) (*_pubsub.ExperimentTreatment, error) + // GetTreatment returns treatment based on provided experiment. If the experiment's type is Switchback, + // the window Id is also returned. + GetTreatment(experiment *_pubsub.Experiment, randomizationValue *string) (*_pubsub.ExperimentTreatment, *int64, error) } type treatmentService struct { @@ -30,28 +31,37 @@ func NewTreatmentService(localStorage *models.LocalStorage) (TreatmentService, e return svc, nil } -func (ts *treatmentService) GetTreatment(experiment *_pubsub.Experiment, randomizationValue *string) (*_pubsub.ExperimentTreatment, error) { +func (ts *treatmentService) GetTreatment(experiment *_pubsub.Experiment, randomizationValue *string) (*_pubsub.ExperimentTreatment, *int64, error) { if experiment == nil { // No experiments found - return &_pubsub.ExperimentTreatment{}, nil + return &_pubsub.ExperimentTreatment{}, nil, nil } + var switchbackWindowId *int64 var treatment *_pubsub.ExperimentTreatment var err error if experiment.Type == _pubsub.Experiment_A_B { if randomizationValue == nil { - return &_pubsub.ExperimentTreatment{}, errors.New("randomization key's value is nil") + return &_pubsub.ExperimentTreatment{}, nil, errors.New("randomization key's value is nil") } treatment, err = getAbExperimentTreatment(experiment.Id, experiment.GetTreatments(), *randomizationValue) } else if experiment.Type == _pubsub.Experiment_Switchback { // TODO: Take into consideration when S2ID Clustering project settings option is switched on - treatment, err = getSwitchbackExperimentTreatment(experiment.StartTime, experiment.Interval, experiment.Id, experiment.GetTreatments(), "") + var windowId int64 + treatment, windowId, err = getSwitchbackExperimentTreatment( + experiment.StartTime, + experiment.Interval, + experiment.Id, + experiment.GetTreatments(), + "", + ) + switchbackWindowId = &windowId } if err != nil { - return &_pubsub.ExperimentTreatment{}, err + return &_pubsub.ExperimentTreatment{}, nil, err } - return treatment, nil + return treatment, switchbackWindowId, nil } func getSwitchbackExperimentTreatment( @@ -60,8 +70,9 @@ func getSwitchbackExperimentTreatment( experimentId int64, treatments []*_pubsub.ExperimentTreatment, randomizationValue string, -) (*_pubsub.ExperimentTreatment, error) { +) (*_pubsub.ExperimentTreatment, int64, error) { timeDifference := time.Since(startTime.AsTime()).Minutes() + treatmentIntervalIndex := int64(math.Floor(timeDifference / float64(interval))) isCyclical := true if treatments[0].Traffic != 0 { @@ -69,24 +80,22 @@ func getSwitchbackExperimentTreatment( } var err error - var treatmentIntervalIndex int // Cyclical Switchback Experiment; Traffic is not specified if isCyclical { - treatmentIntervalIndex = int(math.Floor( + cyclicalIndex := int(math.Floor( math.Mod(timeDifference/float64(interval), float64(len(treatments))), )) - return treatments[treatmentIntervalIndex], nil + return treatments[cyclicalIndex], treatmentIntervalIndex, nil } // Random Switchback Experiment; Traffic is specified - treatmentIntervalIndex = int(math.Floor(timeDifference / float64(interval))) seed := getSwitchbackSeed(experimentId, randomizationValue, treatmentIntervalIndex) selectedTreatment, err := weightedChoice(treatments, seed) if err != nil { - return &_pubsub.ExperimentTreatment{}, err + return &_pubsub.ExperimentTreatment{}, treatmentIntervalIndex, err } - return selectedTreatment, nil + return selectedTreatment, treatmentIntervalIndex, nil } func getAbExperimentTreatment( @@ -147,6 +156,6 @@ func getAbSeed(experimentID int64, randomizationUnit string) string { return fmt.Sprintf("%s-%d", randomizationUnit, experimentID) } -func getSwitchbackSeed(experimentID int64, randomizationUnit string, treatmentIntervalIndex int) string { +func getSwitchbackSeed(experimentID int64, randomizationUnit string, treatmentIntervalIndex int64) string { return fmt.Sprintf("%s-%d-%d", randomizationUnit, treatmentIntervalIndex, experimentID) } diff --git a/treatment-service/services/treatment_service_test.go b/treatment-service/services/treatment_service_test.go index c2211404..ffdc79f6 100644 --- a/treatment-service/services/treatment_service_test.go +++ b/treatment-service/services/treatment_service_test.go @@ -67,9 +67,10 @@ func TestTreatmentServiceTestSuite(t *testing.T) { } func (suite *TreatmentSelectionSuite) TestNoExperiments() { - resp, err := suite.treatmentService.GetTreatment(nil, nil) + resp, windowId, err := suite.treatmentService.GetTreatment(nil, nil) suite.Require().NoError(err) + suite.Require().Nil(windowId) suite.Require().Equal(&_pubsub.ExperimentTreatment{}, resp) } @@ -81,7 +82,7 @@ func (suite *TreatmentSelectionSuite) TestNoRandomizationValue() { }, } experiment := newTestXPExperiment(1, _pubsub.Experiment_A_B, treatment, suite.dayStart, suite.hourStart) - _, err := suite.treatmentService.GetTreatment(&experiment, nil) + _, _, err := suite.treatmentService.GetTreatment(&experiment, nil) suite.Require().Error(err, "randomization key's value is nil") } @@ -105,7 +106,7 @@ func (suite *TreatmentSelectionSuite) TestTreatmentConversion() { } experiment := newTestXPExperiment(1, _pubsub.Experiment_A_B, treatment, suite.dayStart, suite.hourStart) randomizationValue := "" - resp, _ := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp, windowId, _ := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) var experimentTreatment *models.ExperimentTreatment var traffic int32 @@ -128,6 +129,7 @@ func (suite *TreatmentSelectionSuite) TestTreatmentConversion() { } suite.Require().NoError(err) + suite.Require().Nil(windowId) suite.Require().Equal(expectedTreatment, convertedResp) } @@ -141,7 +143,7 @@ func (suite *TreatmentSelectionSuite) TestSingleAbExperiment() { } experiment := newTestXPExperiment(1, _pubsub.Experiment_A_B, treatment, suite.dayStart, suite.hourStart) randomizationValue := "" - resp, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp, windowId, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) expectedTreatment := &_pubsub.ExperimentTreatment{ Config: &structpb.Struct{}, @@ -150,6 +152,7 @@ func (suite *TreatmentSelectionSuite) TestSingleAbExperiment() { } suite.Require().NoError(err) + suite.Require().Nil(windowId) suite.Require().Equal(expectedTreatment, resp) } @@ -169,7 +172,7 @@ func (suite *TreatmentSelectionSuite) TestMultipleAbExperiment() { experiment := newTestXPExperiment(1, _pubsub.Experiment_A_B, treatment, suite.dayStart, suite.hourStart) // Should return different treatment based on randomization value randomizationValue := "1234567891" - resp1, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp1, windowId, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) expectedTreatment1 := &_pubsub.ExperimentTreatment{ Config: &structpb.Struct{}, @@ -177,10 +180,11 @@ func (suite *TreatmentSelectionSuite) TestMultipleAbExperiment() { Traffic: 30, } suite.Require().NoError(err) + suite.Require().Nil(windowId) suite.Require().Equal(expectedTreatment1, resp1) randomizationValue = "12341" - resp2, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp2, windowId, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) expectedTreatment2 := &_pubsub.ExperimentTreatment{ Config: &structpb.Struct{}, Name: "ab-exp2-treatment2", @@ -188,6 +192,7 @@ func (suite *TreatmentSelectionSuite) TestMultipleAbExperiment() { } suite.Require().NoError(err) + suite.Require().Nil(windowId) suite.Require().Equal(expectedTreatment2, resp2) } @@ -200,14 +205,19 @@ func (suite *TreatmentSelectionSuite) TestSingleCyclicalSbExperiment() { } experiment := newTestXPExperiment(1, _pubsub.Experiment_Switchback, treatment, suite.hourStart, suite.hourEnd) randomizationValue := "1234" - resp, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp, windowId, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + var expectedWindowId int64 = 0 + if time.Now().Minute() >= 30 { + expectedWindowId = 1 + } expectedTreatment := &_pubsub.ExperimentTreatment{ Config: &structpb.Struct{}, Name: "sb-exp1-treatment1", } suite.Require().NoError(err) + suite.Require().Equal(expectedWindowId, *windowId) suite.Require().Equal(expectedTreatment, resp) } @@ -225,7 +235,7 @@ func (suite *TreatmentSelectionSuite) TestMultiCyclicalSbExperiment() { experiment := newTestXPExperiment(1, _pubsub.Experiment_Switchback, treatment, suite.hourStart, suite.hourEnd) randomizationValue := "1234" - resp, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp, windowId, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) expectedTreatment1 := &_pubsub.ExperimentTreatment{ Config: &structpb.Struct{}, @@ -238,10 +248,12 @@ func (suite *TreatmentSelectionSuite) TestMultiCyclicalSbExperiment() { suite.Require().NoError(err) // Different treatments based on 30min interval - if time.Now().Minute() > 30 { + if time.Now().Minute() >= 30 { suite.Require().Equal(expectedTreatment2, resp) + suite.Require().Equal(int64(1), *windowId) } else { suite.Require().Equal(expectedTreatment1, resp) + suite.Require().Equal(int64(0), *windowId) } } @@ -256,8 +268,12 @@ func (suite *TreatmentSelectionSuite) TestSingleRandomSbExperiment() { experiment := newTestXPExperiment(1, _pubsub.Experiment_Switchback, treatment, suite.hourStart, suite.hourEnd) randomizationValue := "1234" - resp, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp, windowId, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + var expectedWindowId int64 = 0 + if time.Now().Minute() >= 30 { + expectedWindowId = 1 + } expectedTreatment := &_pubsub.ExperimentTreatment{ Config: &structpb.Struct{}, Name: "sb-exp3-treatment1", @@ -265,6 +281,7 @@ func (suite *TreatmentSelectionSuite) TestSingleRandomSbExperiment() { } suite.Require().NoError(err) + suite.Require().Equal(expectedWindowId, *windowId) suite.Require().Equal(expectedTreatment, resp) } @@ -283,6 +300,11 @@ func (suite *TreatmentSelectionSuite) TestMultiRandomSbExperiment() { } experiment := newTestXPExperiment(1, _pubsub.Experiment_Switchback, treatment, suite.hourStart, suite.hourEnd) + var expectedWindowId int64 = 0 + if time.Now().Minute() >= 30 { + expectedWindowId = 1 + } + expectedTreatment1 := &_pubsub.ExperimentTreatment{ Config: &structpb.Struct{}, Name: "sb-exp4-treatment1", @@ -290,13 +312,15 @@ func (suite *TreatmentSelectionSuite) TestMultiRandomSbExperiment() { } randomizationValue := "1234" - resp1, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp1, windowId, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) suite.Require().NoError(err) + suite.Require().Equal(expectedWindowId, *windowId) suite.Require().Equal(expectedTreatment1, resp1) randomizationValue = "12341" - resp2, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) + resp2, windowId, err := suite.treatmentService.GetTreatment(&experiment, &randomizationValue) suite.Require().NoError(err) + suite.Require().Equal(expectedWindowId, *windowId) suite.Require().Equal(expectedTreatment1, resp2) } diff --git a/treatment-service/testhelper/mockmanagement/service/store.go b/treatment-service/testhelper/mockmanagement/service/store.go index ce54bda2..e8fc2061 100644 --- a/treatment-service/testhelper/mockmanagement/service/store.go +++ b/treatment-service/testhelper/mockmanagement/service/store.go @@ -65,6 +65,7 @@ func (i *InMemoryStore) CreateExperiment(experiment schema.Experiment) (schema.E experiment.Id = int64(len(i.Experiments)) + 1 experiment.Status = schema.ExperimentStatusActive experiment.UpdatedAt = time.Now() + experiment.Version = 1 i.Experiments = append(i.Experiments, experiment) err := i.MessageQueue.PublishNewExperiment(experiment, i.SegmentersTypes) @@ -79,8 +80,9 @@ func (i *InMemoryStore) UpdateExperiment(projectId int64, experimentId int64, ex i.Lock() defer i.Unlock() experiment.UpdatedAt = time.Now() - for index, experiment := range i.Experiments { - if experiment.ProjectId == projectId && experiment.Id == experimentId { + for index, e := range i.Experiments { + if experiment.ProjectId == projectId && e.Id == experimentId { + experiment.Version = e.Version + 1 i.Experiments[index] = experiment } } diff --git a/ui/src/components/version_badge/VersionBadge.js b/ui/src/components/version_badge/VersionBadge.js new file mode 100644 index 00000000..cae3c042 --- /dev/null +++ b/ui/src/components/version_badge/VersionBadge.js @@ -0,0 +1,10 @@ +import React from "react"; + +import { EuiBadge } from "@elastic/eui"; + +export const VersionBadge = ({ version }) => ( + + {`v${version}`} + +); diff --git a/ui/src/experiments/details/ExperimentDetailsView.js b/ui/src/experiments/details/ExperimentDetailsView.js index 64dd8275..b2c5697e 100644 --- a/ui/src/experiments/details/ExperimentDetailsView.js +++ b/ui/src/experiments/details/ExperimentDetailsView.js @@ -2,16 +2,19 @@ import React, { Fragment, useCallback, useEffect } from "react"; import { EuiCallOut, + EuiFlexGroup, + EuiFlexItem, EuiLoadingChart, EuiSpacer, EuiTextAlign, - EuiPageTemplate + EuiPageTemplate, } from "@elastic/eui"; import { PageNavigation } from "@gojek/mlp-ui"; import { Redirect, Router } from "@reach/router"; -import { PageTitle } from "components/page/PageTitle"; +import { VersionBadge } from "components/version_badge/VersionBadge"; import { StatusBadge } from "components/status_badge/StatusBadge"; +import { PageTitle } from "components/page/PageTitle"; import EditExperimentView from "experiments/edit/EditExperimentView"; import ListExperimentHistoryView from "experiments/history/ListExperimentHistoryView"; import { useXpApi } from "hooks/useXpApi"; @@ -21,6 +24,17 @@ import { ExperimentConfigView } from "./config/ExperimentConfigView"; import { ExperimentActions } from "./ExperimentActions"; import { useConfig } from "config"; +const ExperimentBadges = ({ version, status }) => ( + + + + + + + + +); + const ExperimentDetailsView = ({ projectId, experimentId, ...props }) => { const { appConfig: { @@ -85,7 +99,7 @@ const ExperimentDetailsView = ({ projectId, experimentId, ...props }) => { + } /> } @@ -106,7 +120,6 @@ const ExperimentDetailsView = ({ projectId, experimentId, ...props }) => { )} - diff --git a/ui/src/experiments/history/details/ExperimentHistoryDetailsView.js b/ui/src/experiments/history/details/ExperimentHistoryDetailsView.js index 0710dcb2..beae47ab 100644 --- a/ui/src/experiments/history/details/ExperimentHistoryDetailsView.js +++ b/ui/src/experiments/history/details/ExperimentHistoryDetailsView.js @@ -20,6 +20,7 @@ import { TreatmentConfigSection } from "experiments/components/configuration/Tre import { useXpApi } from "hooks/useXpApi"; import { SegmenterContextProvider } from "providers/segmenters/context"; import { useConfig } from "config"; +import { VersionBadge } from "components/version_badge/VersionBadge"; const ExperimentHistoryDetailsView = ({ projectId, experimentId, version }) => { const { @@ -93,7 +94,8 @@ const ExperimentHistoryDetailsView = ({ projectId, experimentId, version }) => { bottomBorder={false} pageTitle={ } /> } /> diff --git a/ui/src/settings/components/form/components/segmenter_section/SegmenterCard.js b/ui/src/settings/components/form/components/segmenter_section/SegmenterCard.js index 63c185ac..f8497671 100644 --- a/ui/src/settings/components/form/components/segmenter_section/SegmenterCard.js +++ b/ui/src/settings/components/form/components/segmenter_section/SegmenterCard.js @@ -10,7 +10,7 @@ import { StatusBadge } from "components/status_badge/StatusBadge"; import { getSegmenterScope } from "services/segmenter/SegmenterScope"; import "./SegmenterCard.scss"; -import {SegmenterSettings} from "./SegmenterSettings"; +import { SegmenterSettings } from "./SegmenterSettings"; export const SegmenterCard = ({ id, @@ -63,6 +63,7 @@ export const SegmenterCard = ({ buttonContent={buttonContent} errors={errors} onChangeSelectedVariables={onChangeSelectedVariables} + initialIsOpen={variables.length > 1} /> ) : ( <> diff --git a/ui/src/settings/components/form/components/segmenter_section/SegmenterSettings.js b/ui/src/settings/components/form/components/segmenter_section/SegmenterSettings.js index 10226da9..8e674790 100644 --- a/ui/src/settings/components/form/components/segmenter_section/SegmenterSettings.js +++ b/ui/src/settings/components/form/components/segmenter_section/SegmenterSettings.js @@ -6,7 +6,7 @@ import { EuiPanel, EuiRadioGroup, } from "@elastic/eui"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import isEqual from "lodash/isEqual"; import sortBy from "lodash/sortBy"; @@ -62,8 +62,9 @@ export const SegmenterSettings = ({ buttonContent, errors, onChangeSelectedVariables, + initialIsOpen, }) => { - const [isOpen, setIsOpen] = useState(false) + const [isOpen, setIsOpen] = useState(initialIsOpen) return ( setIsOpen(!isOpen)}/> + setIsOpen(!isOpen)} /> } >