blob: 4b5d6d932d6d2d90d48315c900de42eac74eefa9 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"strings"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/timestamp"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
ds "go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/grpc/grpcutil"
tricium "infra/tricium/api/v1"
"infra/tricium/appengine/common/track"
)
// Feedback processes one feedback request to Tricium.
func (r *TriciumServer) Feedback(c context.Context, req *tricium.FeedbackRequest) (res *tricium.FeedbackResponse, err error) {
defer func() {
err = grpcutil.GRPCifyAndLogErr(c, err)
}()
logging.Fields{
"project": req.Project,
"category": req.Category,
"startTime": req.StartTime,
"endTime": req.EndTime,
}.Infof(c, "Request received.")
if req.Category == "" {
return nil, errors.New("missing category", grpcutil.InvalidArgumentTag)
}
stime, etime, err := parseTimeRange(c, req.StartTime, req.EndTime)
if err != nil {
return nil, errors.Annotate(err, "invalid time range").Tag(grpcutil.InvalidArgumentTag).Err()
}
count, reports, err := feedback(c, req.Category, stime, etime)
if err != nil {
return nil, err
}
return &tricium.FeedbackResponse{Comments: int32(count), NotUsefulReports: int32(reports)}, nil
}
// parseTimeRange returns the parsed time range and checks for validity.
//
// Start and end time can be nil, in which case the intent is to have
// an open-ended range. No start or end is meant to mean "all time".
func parseTimeRange(c context.Context, startTimestamp, endTimestamp *timestamp.Timestamp) (time.Time, time.Time, error) {
startTime := time.Unix(0, 0).UTC()
endTime := clock.Now(c).UTC()
var err error
if startTimestamp != nil {
startTime, err = ptypes.Timestamp(startTimestamp)
if err != nil {
return startTime, endTime, errors.Annotate(err, "failed to convert start_time").Err()
}
}
if endTimestamp != nil {
endTime, err = ptypes.Timestamp(endTimestamp)
if err != nil {
return startTime, endTime, errors.Annotate(err, "failed to convert end_time").Err()
}
}
if endTime.Before(startTime) {
return startTime, endTime, errors.New("start_time/end_time out of order")
}
return startTime, endTime, nil
}
func feedback(c context.Context, category string, stime, etime time.Time) (int, int, error) {
// Extract analyzer name from category.
analyzer := strings.SplitN(category, "/", 2)[0]
// Getting all comments for the provided category.
// Note that this includes potentially not selected comments, but these comments are not
// exposed for feedback which means they have no feedback data to contribute.
var comments []*track.Comment
if err := ds.GetAll(c, ds.NewQuery("Comment").Eq("Analyzer", analyzer).Gte("CreationTime", stime).Lte("CreationTime", etime), &comments); err != nil {
return 0, 0, errors.Annotate(err, "failed to retrieve Comment entities").Err()
}
if len(comments) == 0 {
logging.Fields{
"category": category,
"startTime": stime.String(),
"endTime": etime.String(),
}.Infof(c, "Found no comments.")
}
var entities []interface{}
for _, comment := range comments {
// Only include comments with the same prefix as the requested category.
// This enables narrowing of summaries to subcategories.
if strings.HasPrefix(comment.Category, category) {
commentKey := ds.KeyForObj(c, comment)
entities = append(entities, &track.CommentFeedback{ID: 1, Parent: commentKey})
entities = append(entities, &track.CommentSelection{ID: 1, Parent: commentKey})
}
}
if err := ds.Get(c, entities); err != nil {
return 0, 0, errors.Annotate(err, "failed to retrieve CommentFeedback entities").Err()
}
count := 0
reports := 0
for _, entity := range entities {
switch t := entity.(type) {
case *track.CommentFeedback:
reports += t.NotUsefulReports
case *track.CommentSelection:
if t.Included {
count++
}
}
}
return count, reports, nil
}