diff --git a/pkg/app/api/grpcapi/web_api.go b/pkg/app/api/grpcapi/web_api.go index 23a9bb70d0..0c92711ebf 100644 --- a/pkg/app/api/grpcapi/web_api.go +++ b/pkg/app/api/grpcapi/web_api.go @@ -1318,16 +1318,10 @@ func (a *WebAPI) GetInsightData(ctx context.Context, req *webservice.GetInsightD return nil, err } - switch req.MetricsKind { - case model.InsightMetricsKind_DEPLOYMENT_FREQUENCY: - return a.getInsightDataForDeployFrequency(ctx, claims.Role.ProjectId, req) - } - return nil, status.Error(codes.Unimplemented, "") + return a.getInsightData(ctx, claims.Role.ProjectId, req) } -// getInsightDataForDeployFrequency returns the accumulated insight data for deploy frequency. -// This function is temporary implementation for front end. -func (a *WebAPI) getInsightDataForDeployFrequency(ctx context.Context, projectID string, req *webservice.GetInsightDataRequest) (*webservice.GetInsightDataResponse, error) { +func (a *WebAPI) getInsightData(ctx context.Context, projectID string, req *webservice.GetInsightDataRequest) (*webservice.GetInsightDataResponse, error) { counts := make([]*model.InsightDataPoint, req.DataPointCount) var movePoint func(time.Time, int) time.Time @@ -1350,48 +1344,24 @@ func (a *WebAPI) getInsightDataForDeployFrequency(ctx context.Context, projectID } for i := 0; i < int(req.DataPointCount); i++ { - target := movePoint(start, i) - - filters := []datastore.ListFilter{ - { - Field: "ProjectId", - Operator: "==", - Value: projectID, - }, - { - Field: "CreatedAt", - Operator: ">=", - Value: target.Unix(), - }, - { - Field: "CreatedAt", - Operator: "<", - Value: movePoint(target, 1).Unix(), // target's finish time on unix time - }, - } - - if req.ApplicationId != "" { - filters = append(filters, datastore.ListFilter{ - Field: "ApplicationId", - Operator: "==", - Value: req.ApplicationId, - }) + targetRangeFrom := movePoint(start, i) + targetRangeTo := movePoint(targetRangeFrom, 1) + + var getInsightDataForEachKind func(context.Context, string, string, time.Time, time.Time) (*model.InsightDataPoint, error) + switch req.MetricsKind { + case model.InsightMetricsKind_DEPLOYMENT_FREQUENCY: + getInsightDataForEachKind = a.getInsightDataForDeployFrequency + case model.InsightMetricsKind_CHANGE_FAILURE_RATE: + getInsightDataForEachKind = a.getInsightDataForChangeFailureRate + default: + return nil, status.Error(codes.Unimplemented, "") } - pageSize := 50 - deployments, err := a.deploymentStore.ListDeployments(ctx, datastore.ListOptions{ - PageSize: pageSize, - Filters: filters, - }) + count, err := getInsightDataForEachKind(ctx, projectID, req.ApplicationId, targetRangeFrom, targetRangeTo) if err != nil { - a.logger.Error("failed to get deployments", zap.Error(err)) - return nil, status.Error(codes.Internal, "Failed to get deployments") - } - - counts[i] = &model.InsightDataPoint{ - Timestamp: target.Unix(), - Value: float32(len(deployments)), + return nil, err } + counts[i] = count } return &webservice.GetInsightDataResponse{ @@ -1399,3 +1369,139 @@ func (a *WebAPI) getInsightDataForDeployFrequency(ctx context.Context, projectID DataPoints: counts, }, nil } + +// getInsightDataForDeployFrequency accumulate insight data in target range for deploy frequency. +// This function is temporary implementation for front end. +func (a *WebAPI) getInsightDataForDeployFrequency( + ctx context.Context, + projectID string, + applicationID string, + targetRangeFrom time.Time, + targetRangeTo time.Time) (*model.InsightDataPoint, error) { + filters := []datastore.ListFilter{ + { + Field: "ProjectId", + Operator: "==", + Value: projectID, + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: targetRangeFrom.Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: targetRangeTo.Unix(), // target's finish time on unix time + }, + } + + if applicationID != "" { + filters = append(filters, datastore.ListFilter{ + Field: "ApplicationId", + Operator: "==", + Value: applicationID, + }) + } + + pageSize := 50 + deployments, err := a.deploymentStore.ListDeployments(ctx, datastore.ListOptions{ + PageSize: pageSize, + Filters: filters, + }) + if err != nil { + a.logger.Error("failed to get deployments", zap.Error(err)) + return nil, status.Error(codes.Internal, "Failed to get deployments") + } + + return &model.InsightDataPoint{ + Timestamp: targetRangeFrom.Unix(), + Value: float32(len(deployments)), + }, nil +} + +// getInsightDataForChangeFailureRate accumulate insight data in target range for change failure rate +// This function is temporary implementation for front end. +func (a *WebAPI) getInsightDataForChangeFailureRate( + ctx context.Context, + projectID string, + applicationID string, + targetRangeFrom time.Time, + targetRangeTo time.Time) (*model.InsightDataPoint, error) { + + commonFilters := []datastore.ListFilter{ + { + Field: "ProjectId", + Operator: "==", + Value: projectID, + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: targetRangeFrom.Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: targetRangeTo.Unix(), // target's finish time on unix time + }, + } + + if applicationID != "" { + commonFilters = append(commonFilters, datastore.ListFilter{ + Field: "ApplicationId", + Operator: "==", + Value: applicationID, + }) + } + + filterForSuccessDeploy := []datastore.ListFilter{ + { + Field: "Status", + Operator: "==", + Value: model.DeploymentStatus_DEPLOYMENT_SUCCESS, + }, + } + + filterForFailureDeploy := []datastore.ListFilter{ + { + Field: "Status", + Operator: "==", + Value: model.DeploymentStatus_DEPLOYMENT_FAILURE, + }, + } + + pageSize := 50 + successDeployments, err := a.deploymentStore.ListDeployments(ctx, datastore.ListOptions{ + PageSize: pageSize, + Filters: append(filterForSuccessDeploy, commonFilters...), + }) + if err != nil { + a.logger.Error("failed to get deployments", zap.Error(err)) + return nil, status.Error(codes.Internal, "Failed to get deployments") + } + + failureDeployments, err := a.deploymentStore.ListDeployments(ctx, datastore.ListOptions{ + PageSize: pageSize, + Filters: append(filterForFailureDeploy, commonFilters...), + }) + if err != nil { + a.logger.Error("failed to get deployments", zap.Error(err)) + return nil, status.Error(codes.Internal, "Failed to get deployments") + } + + successCount := len(successDeployments) + failureCount := len(failureDeployments) + + var changeFailureRate float32 + if successCount+failureCount != 0 { + changeFailureRate = float32(failureCount) / float32(successCount+failureCount) + } else { + changeFailureRate = 0 + } + + return &model.InsightDataPoint{ + Timestamp: targetRangeFrom.Unix(), + Value: changeFailureRate, + }, nil +} diff --git a/pkg/app/api/grpcapi/web_api_test.go b/pkg/app/api/grpcapi/web_api_test.go index 60be953e35..1530be175c 100644 --- a/pkg/app/api/grpcapi/web_api_test.go +++ b/pkg/app/api/grpcapi/web_api_test.go @@ -391,7 +391,7 @@ func TestValidatePipedBelongsToProject(t *testing.T) { } } -func TestGetInsightDataForDeployFrequency(t *testing.T) { +func TestCalculateInsightData(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -514,13 +514,49 @@ func TestGetInsightDataForDeployFrequency(t *testing.T) { }, wantErr: false, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + api := &WebAPI{ + pipedProjectCache: tt.pipedProjectCache, + deploymentStore: tt.deploymentStore, + logger: zap.NewNop(), + } + res, err := api.getInsightData(ctx, tt.projectID, tt.req) + assert.Equal(t, tt.wantErr, err != nil) + if err == nil { + assert.Equal(t, tt.res.DataPoints, res.DataPoints) + } + }) + } +} + +func TestGetInsightDataForDeployFrequency(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + PageSizeForListDeployments := 50 + tests := []struct { + name string + projectID string + applicationID string + targetRangeFrom time.Time + targetRangeTo time.Time + deploymentStore datastore.DeploymentStore + dataPoints *model.InsightDataPoint + wantErr bool + }{ { - name: "return error when something wrong happen on ListDeployments", - pipedID: "pipedID", - projectID: "projectID", + name: "valid with InsightStep_DAILY", + projectID: "projectID", + applicationID: "ApplicationId", + targetRangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + targetRangeTo: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC), deploymentStore: func() datastore.DeploymentStore { - target := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - targetNextYear := target.AddDate(0, 0, 1) s := datastoretest.NewMockDeploymentStore(ctrl) s.EXPECT(). ListDeployments(gomock.Any(), datastore.ListOptions{ @@ -534,12 +570,64 @@ func TestGetInsightDataForDeployFrequency(t *testing.T) { { Field: "CreatedAt", Operator: ">=", - Value: target.Unix(), + Value: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Field: "ApplicationId", + Operator: "==", + Value: "ApplicationId", + }, + }, + }).Return([]*model.Deployment{ + { + Id: "id1", + }, + { + Id: "id2", + }, + { + Id: "id3", + }, + }, nil) + return s + }(), + dataPoints: &model.InsightDataPoint{ + Value: 3, + Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + }, + wantErr: false, + }, + { + name: "return error when something wrong happen on ListDeployments", + projectID: "projectID", + applicationID: "ApplicationId", + targetRangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + targetRangeTo: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC), + deploymentStore: func() datastore.DeploymentStore { + s := datastoretest.NewMockDeploymentStore(ctrl) + s.EXPECT(). + ListDeployments(gomock.Any(), datastore.ListOptions{ + PageSize: PageSizeForListDeployments, + Filters: []datastore.ListFilter{ + { + Field: "ProjectId", + Operator: "==", + Value: "projectID", + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), }, { Field: "CreatedAt", Operator: "<", - Value: targetNextYear.Unix(), + Value: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).Unix(), }, { Field: "ApplicationId", @@ -550,14 +638,176 @@ func TestGetInsightDataForDeployFrequency(t *testing.T) { }).Return([]*model.Deployment{}, fmt.Errorf("something wrong happens in ListDeployments")) return s }(), - req: &webservice.GetInsightDataRequest{ - MetricsKind: model.InsightMetricsKind_DEPLOYMENT_FREQUENCY, - Step: model.InsightStep_DAILY, - RangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), - DataPointCount: 2, - ApplicationId: "ApplicationId", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + api := &WebAPI{ + deploymentStore: tt.deploymentStore, + logger: zap.NewNop(), + } + value, err := api.getInsightDataForDeployFrequency(ctx, tt.projectID, tt.applicationID, tt.targetRangeFrom, tt.targetRangeTo) + assert.Equal(t, tt.wantErr, err != nil) + if err == nil { + assert.Equal(t, tt.dataPoints, value) + } + }) + } +} +func TestGetInsightDataForChangeFailureRate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + PageSizeForListDeployments := 50 + tests := []struct { + name string + projectID string + applicationID string + targetRangeFrom time.Time + targetRangeTo time.Time + deploymentStore datastore.DeploymentStore + dataPoints *model.InsightDataPoint + wantErr bool + }{ + { + name: "valid with InsightStep_DAILY", + projectID: "projectID", + applicationID: "ApplicationId", + targetRangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + targetRangeTo: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC), + deploymentStore: func() datastore.DeploymentStore { + s := datastoretest.NewMockDeploymentStore(ctrl) + s.EXPECT(). + ListDeployments(gomock.Any(), datastore.ListOptions{ + PageSize: PageSizeForListDeployments, + Filters: []datastore.ListFilter{ + { + Field: "Status", + Operator: "==", + Value: model.DeploymentStatus_DEPLOYMENT_SUCCESS, + }, + { + Field: "ProjectId", + Operator: "==", + Value: "projectID", + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Field: "ApplicationId", + Operator: "==", + Value: "ApplicationId", + }, + }, + }).Return([]*model.Deployment{ + { + Id: "id1", + }, + { + Id: "id2", + }, + { + Id: "id3", + }, + }, nil) + + s.EXPECT(). + ListDeployments(gomock.Any(), datastore.ListOptions{ + PageSize: PageSizeForListDeployments, + Filters: []datastore.ListFilter{ + { + Field: "Status", + Operator: "==", + Value: model.DeploymentStatus_DEPLOYMENT_FAILURE, + }, + { + Field: "ProjectId", + Operator: "==", + Value: "projectID", + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Field: "ApplicationId", + Operator: "==", + Value: "ApplicationId", + }, + }, + }).Return([]*model.Deployment{ + { + Id: "id1", + }, + }, nil) + return s + }(), + dataPoints: &model.InsightDataPoint{ + Value: 0.25, + Timestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), }, - res: nil, + wantErr: false, + }, + { + name: "return error when something wrong happen on ListDeployments", + projectID: "projectID", + applicationID: "ApplicationId", + targetRangeFrom: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + targetRangeTo: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC), + deploymentStore: func() datastore.DeploymentStore { + s := datastoretest.NewMockDeploymentStore(ctrl) + s.EXPECT(). + ListDeployments(gomock.Any(), datastore.ListOptions{ + PageSize: PageSizeForListDeployments, + Filters: []datastore.ListFilter{ + { + Field: "Status", + Operator: "==", + Value: model.DeploymentStatus_DEPLOYMENT_SUCCESS, + }, + { + Field: "ProjectId", + Operator: "==", + Value: "projectID", + }, + { + Field: "CreatedAt", + Operator: ">=", + Value: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Field: "CreatedAt", + Operator: "<", + Value: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).Unix(), + }, + { + Field: "ApplicationId", + Operator: "==", + Value: "ApplicationId", + }, + }, + }).Return([]*model.Deployment{}, fmt.Errorf("something wrong happens in ListDeployments")) + return s + }(), wantErr: true, }, } @@ -565,14 +815,13 @@ func TestGetInsightDataForDeployFrequency(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { api := &WebAPI{ - pipedProjectCache: tt.pipedProjectCache, - deploymentStore: tt.deploymentStore, - logger: zap.NewNop(), + deploymentStore: tt.deploymentStore, + logger: zap.NewNop(), } - res, err := api.getInsightDataForDeployFrequency(ctx, tt.projectID, tt.req) + value, err := api.getInsightDataForChangeFailureRate(ctx, tt.projectID, tt.applicationID, tt.targetRangeFrom, tt.targetRangeTo) assert.Equal(t, tt.wantErr, err != nil) if err == nil { - assert.Equal(t, tt.res.DataPoints, res.DataPoints) + assert.Equal(t, tt.dataPoints, value) } }) }