blob: f9e04c4073bc7788a28c450c2417b75d4a6e1b92 [file] [log] [blame]
package notify
import (
"bytes"
"context"
"net/url"
"strconv"
"strings"
"text/template"
"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/ui/frame"
)
const (
defaultRegressionMarkdownSubject = `{{ .Alert.DisplayName }} - Regression found for {{ .Commit.Subject }}`
defaultRegressionMarkdown = `A Perf Regression ({{.Cluster.StepFit.Status}}) has been found at:
{{.URL}}/g/t/{{.Commit.GitHash}}
For:
Commit {{.CommitURL}}
With:
- {{.Cluster.Num}} matching traces.
- Direction {{.Cluster.StepFit.Status}}.
From Alert [{{ .Alert.DisplayName }}]({{.URL}}/a/?{{ .Alert.IDAsString }})
`
defaultRegressionMissingMarkdownSubject = `{{ .Alert.DisplayName }} - Regression no longer found for {{ .Commit.Subject }}`
defaultRegressionMissingMarkdown = `The Perf Regression can no longer be detected. This issue is being automatically closed.
`
)
// MarkdownFormatter implement Formatter.
type MarkdownFormatter struct {
commitRangeURITemplate string
markdownTemplateNewRegression *template.Template
markdownTemplateNewRegressionSubject *template.Template
markdownTemplateRegressionMissing *template.Template
markdownTemplateRegressionMissingSubject *template.Template
}
// buildIDFromSubject is a template func for notify templates.
//
// Note that this is very specific to the android-x git repo where each commit
// subject is formatted as a single URL, for example:
//
// https://android-build.googleplex.com/builds/jump-to-build/10768702
//
// And the template func will extract "10768702" from the above subject.
//
// The implementation is robust and if the subject isn't in the right format
// then the empty string is returned.
func buildIDFromSubject(subject string) string {
parts := strings.Split(strings.TrimSpace(subject), "/")
n := len(parts)
if n == 0 {
return ""
}
return parts[n-1]
}
// NewMarkdownFormatter return a new MarkdownFormatter.
func NewMarkdownFormatter(commitRangeURITemplate string, notifyConfig *config.NotifyConfig) (MarkdownFormatter, 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{
"buildIDFromSubject": buildIDFromSubject,
}
markdownTemplateNewRegression, err := template.New("newRegressionMarkdown").Funcs(funcMap).Parse(body)
if err != nil {
return MarkdownFormatter{}, skerr.Wrapf(err, "compiling markdownTemplateNewRegression")
}
markdownTemplateNewRegressionSubject, err := template.New("newRegressionMarkdown").Funcs(funcMap).Parse(subject)
if err != nil {
return MarkdownFormatter{}, skerr.Wrapf(err, "compiling markdownTemplateNewRegressionSubject")
}
markdownTemplateRegressionMissing, err := template.New("regressionMissingMarkdown").Funcs(funcMap).Parse(missingBody)
if err != nil {
return MarkdownFormatter{}, skerr.Wrapf(err, "compiling markdownTemplateRegressionMissing")
}
markdownTemplateRegressionMissingSubject, err := template.New("regressionMissingMarkdown").Funcs(funcMap).Parse(missingSubject)
if err != nil {
return MarkdownFormatter{}, skerr.Wrapf(err, "compiling markdownTemplateRegressionMissingSubject")
}
return MarkdownFormatter{
commitRangeURITemplate: commitRangeURITemplate,
markdownTemplateNewRegression: markdownTemplateNewRegression,
markdownTemplateNewRegressionSubject: markdownTemplateNewRegressionSubject,
markdownTemplateRegressionMissing: markdownTemplateRegressionMissing,
markdownTemplateRegressionMissingSubject: markdownTemplateRegressionMissingSubject,
}, nil
}
// wiewOnDashboard is the URL to view the regressing traces on the explore page.
func viewOnDashboard(cl *clustering2.ClusterSummary, URL string, frame *frame.FrameResponse) string {
u, err := url.Parse(URL)
if err != nil {
// Fallback to a relative URL if the base URL is invalid.
u = &url.URL{}
}
end := ""
if frame != nil && frame.DataFrame != nil && len(frame.DataFrame.Header) > 0 {
n := len(frame.DataFrame.Header)
// Expand the time range by 1s to ensure inclusion of the last commit.
end = strconv.Itoa(int(frame.DataFrame.Header[n-1].Timestamp + 1))
}
q := url.Values{
"keys": []string{cl.Shortcut},
"xbaroffset": []string{strconv.Itoa(int(cl.StepPoint.Offset))},
"num_commits": []string{"250"},
"request_type": []string{"1"},
}
if end != "" {
q.Set("end", end)
}
u.Path = "/e/"
u.RawQuery = q.Encode()
return u.String()
}
// FormatNewRegression implements Formatter.
func (h MarkdownFormatter) FormatNewRegression(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, URL string, frame *frame.FrameResponse) (string, string, error) {
templateContext := &TemplateContext{
URL: URL,
ViewOnDashboard: viewOnDashboard(cl, URL, frame),
PreviousCommit: previousCommit,
Commit: commit,
CommitURL: URLFromCommitRange(commit, previousCommit, h.commitRangeURITemplate),
Alert: alert,
Cluster: cl,
ParamSet: frame.DataFrame.ParamSet,
}
var body bytes.Buffer
if err := h.markdownTemplateNewRegression.Execute(&body, templateContext); err != nil {
return "", "", skerr.Wrapf(err, "format Markdown body for a new regression")
}
var subject bytes.Buffer
if err := h.markdownTemplateNewRegressionSubject.Execute(&subject, templateContext); err != nil {
return "", "", skerr.Wrapf(err, "format Markdown subject for a new regression")
}
return body.String(), subject.String(), nil
}
// FormatRegressionMissing implements Formatter.
func (h MarkdownFormatter) FormatRegressionMissing(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, URL string, frame *frame.FrameResponse) (string, string, error) {
templateContext := &TemplateContext{
URL: URL,
ViewOnDashboard: viewOnDashboard(cl, URL, frame),
PreviousCommit: previousCommit,
Commit: commit,
CommitURL: URLFromCommitRange(commit, previousCommit, h.commitRangeURITemplate),
Alert: alert,
Cluster: cl,
ParamSet: frame.DataFrame.ParamSet,
}
var body bytes.Buffer
if err := h.markdownTemplateRegressionMissing.Execute(&body, templateContext); err != nil {
return "", "", skerr.Wrapf(err, "format Markdown body for a regression that has gone missing")
}
var subject bytes.Buffer
if err := h.markdownTemplateRegressionMissingSubject.Execute(&subject, templateContext); err != nil {
return "", "", skerr.Wrapf(err, "format Markdown subject for regression that has gone missing")
}
return body.String(), subject.String(), nil
}
var _ Formatter = MarkdownFormatter{}