Skip to content

Commit b99a46e

Browse files
authored
Merge pull request #37 from korotin/master
Plugin & alert rules APIs
2 parents c034314 + 56022f7 commit b99a46e

File tree

8 files changed

+352
-4
lines changed

8 files changed

+352
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
vendor/
22
coverage.out
3+
.env

alert_rule.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package sentry
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"time"
7+
)
8+
9+
type alertRuleMatchPolicy string
10+
11+
var (
12+
AlertRuleMatchAll = alertRuleMatchPolicy("all")
13+
AlertRuleMatchAny = alertRuleMatchPolicy("any")
14+
AlertRuleMatchNone = alertRuleMatchPolicy("none")
15+
)
16+
17+
// AlertRuleCondition represents alert rule condition.
18+
// Refer to https://github.com/getsentry/sentry/tree/master/src/sentry/rules/conditions or GUI
19+
// to get detailed information.
20+
type AlertRuleCondition struct {
21+
ID string `json:"id,omitempty"`
22+
Name string `json:"name,omitempty"`
23+
Interval string `json:"interval,omitempty"` // 1m, 1w, 30d etc
24+
Value interface{} `json:"value,omitempty"`
25+
Attribute string `json:"attribute,omitempty"`
26+
Key string `json:"key,omitempty"`
27+
}
28+
29+
// AlertRuleAction represents alert rule action.
30+
// Refer to https://github.com/getsentry/sentry/tree/master/src/sentry/rules/actions or GUI
31+
// to get detailed information.
32+
type AlertRuleAction struct {
33+
ID string `json:"id,omitempty"`
34+
Name string `json:"name,omitempty"`
35+
Service string `json:"service,omitempty"`
36+
}
37+
38+
type AlertRule struct {
39+
ID string `json:"id,omitempty"`
40+
Name string `json:"name,omitempty"`
41+
DateCreated *time.Time `json:"dateCreated,omitempty"`
42+
Environment *string `json:"environment,omitempty"`
43+
ActionMatch alertRuleMatchPolicy `json:"actionMatch,omitempty"`
44+
Frequency uint `json:"frequency,omitempty"` // run actions at most once every Frequency minutes
45+
Conditions []AlertRuleCondition `json:"conditions,omitempty"`
46+
Actions []AlertRuleAction `json:"actions,omitempty"`
47+
}
48+
49+
func (c *Client) GetAlertRules(o Organization, p Project) ([]AlertRule, *Link, error) {
50+
var rules []AlertRule
51+
link, err := c.doWithPagination(http.MethodGet, fmt.Sprintf("projects/%s/%s/rules/", *o.Slug, *p.Slug), &rules, nil)
52+
53+
return rules, link, err
54+
}
55+
56+
func (c *Client) AddAlertRule(o Organization, p Project, r AlertRule) (AlertRule, error) {
57+
err := c.do(http.MethodPost, fmt.Sprintf("projects/%s/%s/rules/", *o.Slug, *p.Slug), &r, r)
58+
59+
return r, err
60+
}
61+
62+
func (c *Client) UpdateAlertRule(o Organization, p Project, r AlertRule) (AlertRule, error) {
63+
err := c.do(http.MethodPut, fmt.Sprintf("projects/%s/%s/rules/%s/", *o.Slug, *p.Slug, r.ID), &r, r)
64+
65+
return r, err
66+
}
67+
68+
func (c *Client) DeleteAlertRule(o Organization, p Project, r AlertRule) error {
69+
return c.do(http.MethodDelete, fmt.Sprintf("projects/%s/%s/rules/%s/", *o.Slug, *p.Slug, r.ID), nil, nil)
70+
}

alert_rule_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package sentry
2+
3+
import (
4+
"github.com/stretchr/testify/require"
5+
"testing"
6+
)
7+
8+
func TestClient_GetAlertRules(t *testing.T) {
9+
client := newTestClient(t)
10+
org, err := client.GetOrganization(getDefaultOrg())
11+
if err != nil {
12+
t.Fatal(err)
13+
}
14+
15+
team, cleanup := createTeamHelper(t)
16+
defer cleanup()
17+
18+
project, cleanupproj := createProjectHelper(t, team)
19+
defer cleanupproj()
20+
21+
rules, _, err := client.GetAlertRules(org, project)
22+
require.NoError(t, err, "unable to get alert rules")
23+
24+
require.Greater(t, len(rules), 0, "no alert rules defined")
25+
require.Greater(t, len(rules[0].Conditions), 0, "no conditions for rule")
26+
require.Greater(t, len(rules[0].Actions), 0, "no actions for rule")
27+
}
28+
29+
func TestClient_AddAlertRule(t *testing.T) {
30+
client := newTestClient(t)
31+
org, err := client.GetOrganization(getDefaultOrg())
32+
if err != nil {
33+
t.Fatal(err)
34+
}
35+
36+
team, cleanup := createTeamHelper(t)
37+
defer cleanup()
38+
39+
project, cleanupproj := createProjectHelper(t, team)
40+
defer cleanupproj()
41+
42+
rule, err := client.AddAlertRule(org, project, AlertRule{
43+
Name: "Test alert rule",
44+
ActionMatch: AlertRuleMatchAll,
45+
Frequency: 30,
46+
Conditions: []AlertRuleCondition{
47+
{ID: "sentry.rules.conditions.regression_event.RegressionEventCondition"},
48+
},
49+
Actions: []AlertRuleAction{
50+
{ID: "sentry.rules.actions.notify_event_service.NotifyEventServiceAction", Service: "mail"},
51+
},
52+
})
53+
require.NoError(t, err, "unable to create rule")
54+
require.NotEqual(t, 0, rule.ID, "missing rule ID")
55+
require.Equal(t, 1, len(rule.Conditions), "wrong condition count")
56+
require.Equal(t, 1, len(rule.Actions), "wrong action count")
57+
58+
rules, _, err := client.GetAlertRules(org, project)
59+
require.NoError(t, err, "unable to get alert rules")
60+
require.Equal(t, len(rules), 2, "rule wasn't created")
61+
for _, r := range rules {
62+
if r.ID == rule.ID {
63+
require.Equal(t, rule, r, "rules do not match")
64+
break
65+
}
66+
}
67+
}
68+
69+
func TestClient_UpdateAlertRule(t *testing.T) {
70+
client := newTestClient(t)
71+
org, err := client.GetOrganization(getDefaultOrg())
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
76+
team, cleanup := createTeamHelper(t)
77+
defer cleanup()
78+
79+
project, cleanupproj := createProjectHelper(t, team)
80+
defer cleanupproj()
81+
82+
rules, _, err := client.GetAlertRules(org, project)
83+
require.NoError(t, err, "unable to get alert rules")
84+
85+
require.Greater(t, len(rules), 0, "no alert rules defined")
86+
87+
rule := rules[0]
88+
require.Equal(t, 1, len(rule.Conditions), "unexpected condition count for rule")
89+
90+
rule.Conditions = append(
91+
rule.Conditions,
92+
AlertRuleCondition{ID: "sentry.rules.conditions.regression_event.RegressionEventCondition"},
93+
)
94+
95+
rule, err = client.UpdateAlertRule(org, project, rule)
96+
require.NoError(t, err, "unable to get update rule")
97+
require.Equal(t, 2, len(rule.Conditions), "conditions weren't updated")
98+
}
99+
100+
func TestClient_DeleteAlertRule(t *testing.T) {
101+
client := newTestClient(t)
102+
org, err := client.GetOrganization(getDefaultOrg())
103+
if err != nil {
104+
t.Fatal(err)
105+
}
106+
107+
team, cleanup := createTeamHelper(t)
108+
defer cleanup()
109+
110+
project, cleanupproj := createProjectHelper(t, team)
111+
defer cleanupproj()
112+
113+
rules, _, err := client.GetAlertRules(org, project)
114+
require.NoError(t, err, "unable to get alert rules")
115+
116+
require.Greater(t, len(rules), 0, "no alert rules defined")
117+
118+
err = client.DeleteAlertRule(org, project, rules[0])
119+
require.NoError(t, err, "unable to get delete rule")
120+
121+
rules, _, err = client.GetAlertRules(org, project)
122+
require.NoError(t, err, "unable to get alert rules")
123+
require.Equal(t, 0, len(rules), "rule wasn't deleted")
124+
125+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/go-errors/errors v1.1.1 // indirect
88
github.com/google/go-cmp v0.5.4 // indirect
99
github.com/pkg/errors v0.9.1 // indirect
10+
github.com/stretchr/testify v1.7.0
1011
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
1112
)
1213

go.sum

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz
1313
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
1414
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
1515
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1617
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1718
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
1819
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@@ -29,7 +30,6 @@ github.com/getsentry/sentry-go v0.9.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wB
2930
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
3031
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
3132
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
32-
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
3333
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
3434
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
3535
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -104,6 +104,7 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw
104104
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
105105
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
106106
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
107+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
107108
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
108109
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
109110
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -122,6 +123,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
122123
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
123124
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
124125
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
126+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
127+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
125128
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
126129
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
127130
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@@ -176,10 +179,10 @@ golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGm
176179
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
177180
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
178181
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
179-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
180182
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
181183
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
182184
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
185+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
183186
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
184187
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
185188
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
@@ -190,3 +193,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
190193
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
191194
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
192195
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
196+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
197+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

plugin.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package sentry
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
)
7+
8+
func (c *Client) touchPlugin(o Organization, p Project, pluginID string, enabled bool) error {
9+
method := http.MethodDelete
10+
if enabled {
11+
method = http.MethodPost
12+
}
13+
14+
return c.do(method, fmt.Sprintf("projects/%s/%s/plugins/%s/", *o.Slug, *p.Slug, pluginID), nil, nil)
15+
}
16+
17+
func (c *Client) EnablePlugin(o Organization, p Project, pluginID string) error {
18+
return c.touchPlugin(o, p, pluginID, true)
19+
}
20+
21+
func (c *Client) DisablePlugin(o Organization, p Project, pluginID string) error {
22+
return c.touchPlugin(o, p, pluginID, false)
23+
}
24+
25+
func (c *Client) GetPlugin(o Organization, p Project, pluginID string) (plugin Plugin, err error) {
26+
err = c.do(http.MethodGet, fmt.Sprintf("projects/%s/%s/plugins/%s/", *o.Slug, *p.Slug, pluginID), &plugin, nil)
27+
28+
return
29+
}
30+
31+
func (c *Client) SetPluginConfig(o Organization, p Project, pluginID string, config map[string]interface{}) (plugin Plugin, err error) {
32+
err = c.do(http.MethodPut, fmt.Sprintf("projects/%s/%s/plugins/%s/", *o.Slug, *p.Slug, pluginID), &plugin, config)
33+
34+
return
35+
}

plugin_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package sentry
2+
3+
import (
4+
"github.com/stretchr/testify/require"
5+
"testing"
6+
)
7+
8+
const testPlugin = "webhooks"
9+
10+
func pluginEnabled(t *testing.T, client *Client, org Organization, project Project, pluginID string) bool {
11+
project, err := client.GetProject(org, *project.Slug)
12+
require.NoError(t, err, "failed to get project details")
13+
14+
require.NotNil(t, project.Plugins, "project details missing plugins section")
15+
require.Greater(t, len(*project.Plugins), 0, "no plugins installed")
16+
17+
for _, p := range *project.Plugins {
18+
if p.ID == pluginID {
19+
return p.Enabled
20+
}
21+
}
22+
require.Fail(t, "plugin not found")
23+
24+
return false
25+
}
26+
27+
func TestClient_EnablePlugin(t *testing.T) {
28+
client := newTestClient(t)
29+
org, err := client.GetOrganization(getDefaultOrg())
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
team, cleanup := createTeamHelper(t)
35+
defer cleanup()
36+
37+
project, cleanupproj := createProjectHelper(t, team)
38+
defer cleanupproj()
39+
40+
require.False(t, pluginEnabled(t, client, org, project, testPlugin), "plugin already enabled")
41+
42+
err = client.EnablePlugin(org, project, testPlugin)
43+
require.NoError(t, err, "unable to enable plugin")
44+
45+
require.True(t, pluginEnabled(t, client, org, project, testPlugin), "plugin wasn't enabled")
46+
47+
err = client.EnablePlugin(org, project, testPlugin)
48+
require.NoError(t, err, "unable to enable plugin")
49+
50+
require.True(t, pluginEnabled(t, client, org, project, testPlugin), "plugin wasn't enabled")
51+
52+
err = client.DisablePlugin(org, project, testPlugin)
53+
require.NoError(t, err, "unable to disable plugin")
54+
55+
require.False(t, pluginEnabled(t, client, org, project, testPlugin), "plugin wasn't disabled")
56+
}
57+
58+
func TestClient_GetPlugin(t *testing.T) {
59+
client := newTestClient(t)
60+
org, err := client.GetOrganization(getDefaultOrg())
61+
if err != nil {
62+
t.Fatal(err)
63+
}
64+
65+
team, cleanup := createTeamHelper(t)
66+
defer cleanup()
67+
68+
project, cleanupproj := createProjectHelper(t, team)
69+
defer cleanupproj()
70+
71+
plugin, err := client.GetPlugin(org, project, testPlugin)
72+
require.NoError(t, err, "unable to get plugin")
73+
74+
require.Greater(t, len(plugin.Config), 0, "plugin config is missing")
75+
}
76+
77+
func TestClient_SetPluginConfig(t *testing.T) {
78+
client := newTestClient(t)
79+
org, err := client.GetOrganization(getDefaultOrg())
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
84+
team, cleanup := createTeamHelper(t)
85+
defer cleanup()
86+
87+
project, cleanupproj := createProjectHelper(t, team)
88+
defer cleanupproj()
89+
90+
plugin, err := client.SetPluginConfig(org, project, testPlugin, map[string]interface{}{
91+
"urls": "https://test.com/",
92+
})
93+
require.NoError(t, err, "unable to get plugin")
94+
require.NotEmpty(t, plugin.Config, "unable to get plugin config")
95+
require.Equal(t, "https://test.com/", plugin.Config[0].Value)
96+
}

0 commit comments

Comments
 (0)