blob: d1a363d66c557342e7a300cc4bf958428a424e77 [file] [log] [blame]
package notify
import (
"context"
"fmt"
"sort"
"strings"
"text/template"
"go.skia.org/infra/go/paramtools"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/perf/go/alerts"
"go.skia.org/infra/perf/go/clustering2"
"go.skia.org/infra/perf/go/config"
"go.skia.org/infra/perf/go/git/provider"
"go.skia.org/infra/perf/go/notify/common"
)
// AndroidBugTemplateContext provides a struct containing data necessary
// for generating body and subject for android instance bugs.
type AndroidBugTemplateContext struct {
// URL is the root URL of the Perf instance.
URL string
// DashboardUrl is the URL to view the regressing traces on the explore
// page.
DashboardUrl string
// PreviousCommit is the previous commit the regression was found at.
//
// All commits that might be blamed for causing the regression
// are in the range `(PreviousCommit, Commit]`, that is inclusive of
// Commit but exclusive of PreviousCommit.
PreviousCommit provider.Commit
// RegressionCommit is the commit the regression was found at.
RegressionCommit provider.Commit
// CommitURL is a URL that points to the above Commit. The value of this URL
// can be controlled via the `--commit_range_url` flag.
CommitURL string
// Alert is the configuration for the alert that found the regression.
Alert *alerts.Alert
// Cluster is all the information found about the regression.
Cluster *clustering2.ClusterSummary
// ParamSet for all the matching traces in the query.
ParamSet paramtools.ReadOnlyParamSet
// TraceID for the trace where regression is detected. Only available when the
// detection is done individually and not KMeans.
TraceID string
// RegressionCommitLinks contain the links for the regression commit data point for the trace.
RegressionCommitLinks map[string]string
// PreviousCommitLinks contain the links for the commit data point before the regression for the trace.
PreviousCommitLinks map[string]string
// Contains formatted names of affected test(s). Formatted as
// "{{test_class}}#{{test_method}} ({{device_name}} {{os_version}})"
Tests []string
}
// GetBuildIdUrlDiff returns a url that contains the diff between build ids for androidx.
func (context AndroidBugTemplateContext) GetBuildIdUrlDiff() string {
const buildIdKey = "Build ID"
var fromBuildId, toBuildId string
var ok bool
if fromBuildId, ok = context.RegressionCommitLinks[buildIdKey]; !ok {
return "No build id in current commit"
}
if toBuildId, ok = context.PreviousCommitLinks[buildIdKey]; !ok {
return "No build id in previous commit"
}
return buildIdsToUrlDiff(fromBuildId, toBuildId)
}
// androidNotificationProvider provides functionality to generate data for android bugs.
type androidNotificationProvider struct {
formatter *MarkdownFormatter
}
// NewAndroidNotificationDataProvider returns a new instance of the androidNotificationProvider.
func NewAndroidNotificationDataProvider(commitRangeURITemplate string, notifyConfig *config.NotifyConfig) (*androidNotificationProvider, error) {
body := strings.Join(notifyConfig.Body, "\n")
if body == "" {
body = defaultRegressionMarkdown
}
subject := notifyConfig.Subject
if subject == "" {
subject = defaultRegressionMarkdownSubject
}
missingBody := strings.Join(notifyConfig.MissingBody, "\n")
if missingBody == "" {
missingBody = defaultRegressionMissingMarkdown
}
missingSubject := notifyConfig.MissingSubject
if missingSubject == "" {
missingSubject = defaultRegressionMissingMarkdownSubject
}
funcMap := template.FuncMap{
"buildIdsToUrlDiff": buildIdsToUrlDiff,
}
markdownTemplateNewRegression, err := template.New("newRegressionMarkdown").Funcs(funcMap).Parse(body)
if err != nil {
return nil, skerr.Wrapf(err, "compiling markdownTemplateNewRegression")
}
markdownTemplateNewRegressionSubject, err := template.New("newRegressionMarkdown").Funcs(funcMap).Parse(subject)
if err != nil {
return nil, skerr.Wrapf(err, "compiling markdownTemplateNewRegressionSubject")
}
markdownTemplateRegressionMissing, err := template.New("regressionMissingMarkdown").Funcs(funcMap).Parse(missingBody)
if err != nil {
return nil, skerr.Wrapf(err, "compiling markdownTemplateRegressionMissing")
}
markdownTemplateRegressionMissingSubject, err := template.New("regressionMissingMarkdown").Funcs(funcMap).Parse(missingSubject)
if err != nil {
return nil, skerr.Wrapf(err, "compiling markdownTemplateRegressionMissingSubject")
}
formatter := MarkdownFormatter{
commitRangeURITemplate: commitRangeURITemplate,
markdownTemplateNewRegression: markdownTemplateNewRegression,
markdownTemplateNewRegressionSubject: markdownTemplateNewRegressionSubject,
markdownTemplateRegressionMissing: markdownTemplateRegressionMissing,
markdownTemplateRegressionMissingSubject: markdownTemplateRegressionMissingSubject,
}
return &androidNotificationProvider{
formatter: &formatter,
}, nil
}
// GetNotificationDataRegressionFound returns the notification data for a new regression.
func (prov *androidNotificationProvider) GetNotificationDataRegressionFound(ctx context.Context, metadata common.RegressionMetadata) (*common.NotificationData, error) {
if prov.formatter != nil {
templateContext := prov.getTemplateContext(metadata)
body, subject, err := prov.formatter.FormatNewRegressionWithContext(templateContext)
if err != nil {
return nil, err
}
return &common.NotificationData{
Body: body,
Subject: subject,
}, nil
}
return &common.NotificationData{
Body: "",
Subject: "",
}, nil
}
// GetNotificationDataRegressionMissing returns the notification data for a missing regression.
func (prov *androidNotificationProvider) GetNotificationDataRegressionMissing(ctx context.Context, metadata common.RegressionMetadata) (*common.NotificationData, error) {
if prov.formatter != nil {
templateContext := prov.getTemplateContext(metadata)
body, subject, err := prov.formatter.FormatRegressionMissingWithContext(templateContext)
if err != nil {
return nil, err
}
return &common.NotificationData{
Body: body,
Subject: subject,
}, nil
}
return &common.NotificationData{
Body: "",
Subject: "",
}, nil
}
func formatTests(metadata common.RegressionMetadata) []string {
formattedTests := make([]string, 0)
if metadata.Frame != nil && metadata.Frame.DataFrame != nil && metadata.Frame.DataFrame.TraceSet != nil {
if len(metadata.Frame.DataFrame.TraceSet) > 0 {
for traceKey := range metadata.Frame.DataFrame.TraceSet {
params := paramtools.NewParams(traceKey)
testClass := params["test_class"]
testMethod := params["test_method"]
deviceName := params["device_name"]
osVersion := params["os_version"]
formattedString := fmt.Sprintf("%s#%s (%s %s)", testClass, testMethod, deviceName, osVersion)
formattedTests = append(formattedTests, formattedString)
}
}
}
sort.Strings(formattedTests)
return formattedTests
}
// getTemplateContext returns a template context object to be used for the bug data formatting.
func (prov *androidNotificationProvider) getTemplateContext(metadata common.RegressionMetadata) AndroidBugTemplateContext {
return AndroidBugTemplateContext{
URL: metadata.InstanceUrl,
DashboardUrl: viewOnDashboard(metadata.Cl, metadata.InstanceUrl, metadata.Frame),
PreviousCommit: metadata.PreviousCommit,
RegressionCommit: metadata.RegressionCommit,
CommitURL: URLFromCommitRange(metadata.RegressionCommit, metadata.PreviousCommit, prov.formatter.commitRangeURITemplate),
Alert: metadata.AlertConfig,
Cluster: metadata.Cl,
ParamSet: metadata.Frame.DataFrame.ParamSet,
TraceID: metadata.TraceID,
RegressionCommitLinks: metadata.RegressionCommitLinks,
PreviousCommitLinks: metadata.PreviousCommitLinks,
Tests: formatTests(metadata),
}
}
func buildIdsToUrlDiff(fromBuildId string, toBuildId string) string {
const urlFormat = "https://android-build.corp.google.com/range_search/cls/from_id/%s/to_id/%s/?s=menu&includeTo=0&includeFrom=1"
return fmt.Sprintf(urlFormat, fromBuildId, toBuildId)
}