From ea2d74476beb1eee66b1f43b7d391ac47666c6c2 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Tue, 19 Jul 2022 23:38:35 +0100 Subject: [PATCH 01/41] Export sentry.ParseRate. Remove client-wide rate settings --- sentry/sentry.go | 43 ++++--------------------------------------- sentry/sentry_test.go | 40 +--------------------------------------- 2 files changed, 5 insertions(+), 78 deletions(-) diff --git a/sentry/sentry.go b/sentry/sentry.go index 42c71a5..5ccf886 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -43,9 +43,6 @@ type Client struct { // User agent used when communicating with Sentry. UserAgent string - // Latest rate limit - rate Rate - // Common struct used by all services. common service @@ -194,7 +191,7 @@ type Response struct { func newResponse(r *http.Response) *Response { response := &Response{Response: r} - response.Rate = parseRate(r) + response.Rate = ParseRate(r) response.populatePaginationCursor() return response } @@ -206,8 +203,8 @@ func (r *Response) populatePaginationCursor() { } } -// parseRate parses the rate limit headers. -func parseRate(r *http.Response) Rate { +// ParseRate parses the rate limit headers. +func ParseRate(r *http.Response) Rate { var rate Rate if limit := r.Header.Get(headerRateLimit); limit != "" { rate.Limit, _ = strconv.Atoi(limit) @@ -226,7 +223,6 @@ func parseRate(r *http.Response) Rate { if concurrentRemaining := r.Header.Get(headerRateConcurrentRemaining); concurrentRemaining != "" { rate.ConcurrentRemaining, _ = strconv.Atoi(concurrentRemaining) } - return rate } @@ -235,14 +231,6 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro return nil, errNonNilContext } - // Check rate limit - if err := c.checkRateLimit(req); err != nil { - return &Response{ - Response: err.Response, - Rate: err.Rate, - }, err - } - resp, err := c.client.Do(req) if err != nil { // If we got an error, and the context has been canceled, @@ -256,33 +244,10 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro } response := newResponse(resp) - - c.rate = response.Rate - err = CheckResponse(resp) - return response, err } -func (c *Client) checkRateLimit(req *http.Request) *RateLimitError { - if !c.rate.Reset.IsZero() && c.rate.Remaining == 0 && time.Now().Before(c.rate.Reset) { - resp := &http.Response{ - Status: http.StatusText(http.StatusTooManyRequests), - StatusCode: http.StatusTooManyRequests, - Request: req, - Header: http.Header{}, - Body: ioutil.NopCloser(strings.NewReader("")), - } - return &RateLimitError{ - Rate: c.rate, - Response: resp, - Detail: fmt.Sprintf("API rate limit of %v and concurrent limit of %v still exceeded until %v, not making remote request.", - c.rate.Limit, c.rate.ConcurrentLimit, c.rate.Reset), - } - } - return nil -} - func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { resp, err := c.BareDo(ctx, req) if err != nil { @@ -389,7 +354,7 @@ func CheckResponse(r *http.Response) error { case r.StatusCode == http.StatusTooManyRequests && (r.Header.Get(headerRateRemaining) == "0" || r.Header.Get(headerRateConcurrentRemaining) == "0"): return &RateLimitError{ - Rate: parseRate(r), + Rate: ParseRate(r), Response: errorResponse.Response, Detail: errorResponse.Detail, } diff --git a/sentry/sentry_test.go b/sentry/sentry_test.go index bd0fd46..a0b0312 100644 --- a/sentry/sentry_test.go +++ b/sentry/sentry_test.go @@ -184,44 +184,6 @@ func TestDo_rateLimit(t *testing.T) { assert.Equal(t, resp.Rate.ConcurrentRemaining, 24) } -func TestDo_rateLimit_noNetworkCall(t *testing.T) { - client, mux, _, teardown := setup() - defer teardown() - - reset := time.Now().UTC().Add(time.Minute).Round(time.Second) - - mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(headerRateLimit, "40") - w.Header().Set(headerRateRemaining, "0") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix())) - w.WriteHeader(http.StatusTooManyRequests) - fmt.Fprint(w, `{"detail": "Rate limit exceeded"}`) - }) - - madeNetworkCall := false - mux.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) { - madeNetworkCall = true - }) - - // First request to determine the rate limit. - req, _ := client.NewRequest("GET", "/first", nil) - ctx := context.Background() - client.Do(ctx, req, nil) - - // Second request should not make a network call. - req, _ = client.NewRequest("GET", "/second", nil) - _, err := client.Do(ctx, req, nil) - - assert.False(t, madeNetworkCall) - assert.Error(t, err) - - if rateLimitErr, ok := err.(*RateLimitError); assert.True(t, ok) { - assert.Equal(t, 40, rateLimitErr.Rate.Limit) - assert.Equal(t, 0, rateLimitErr.Rate.Remaining) - assert.Equal(t, reset, rateLimitErr.Rate.Reset) - } -} - func TestDo_nilContext(t *testing.T) { client, _, _, teardown := setup() defer teardown() @@ -358,7 +320,7 @@ func TestCheckResponse_rateLimit(t *testing.T) { err := CheckResponse(res) expected := &RateLimitError{ - Rate: parseRate(res), + Rate: ParseRate(res), Response: res, Detail: "Rate limit exceeded", } From 7f04ca724533fa6f512bf9ae61243e4a233738c0 Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Thu, 21 Jul 2022 04:02:46 +0930 Subject: [PATCH 02/41] Add in Project Filter Service (#62) * [ODC-441] Extra API fields * [ODC-441] Implement Sentry Filter * Change go.mod to canva * Fix go.mod for using local * Improve replacement * Fix canva in module path * Remove extra comments * Remove extra comments * Change path instead of replace * Remove unused structs * fix: don't use string literals for HTTP methods * chore: use original module name * fix: typo of LegacyBrowserParams Co-authored-by: Foad Nosrati Habibi Co-authored-by: Foad Nosrati Habibi --- sentry/project_filter.go | 105 ++++++++++++++++++++++ sentry/project_filter_test.go | 163 ++++++++++++++++++++++++++++++++++ sentry/projects.go | 20 +++-- sentry/projects_test.go | 16 ++-- sentry/sentry.go | 2 + 5 files changed, 292 insertions(+), 14 deletions(-) create mode 100644 sentry/project_filter.go create mode 100644 sentry/project_filter_test.go diff --git a/sentry/project_filter.go b/sentry/project_filter.go new file mode 100644 index 0000000..9b642f0 --- /dev/null +++ b/sentry/project_filter.go @@ -0,0 +1,105 @@ +package sentry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// ProjectFilter represents inbounding filters applied to a project. +type ProjectFilter struct { + ID string `json:"id"` + Active json.RawMessage `json:"active"` +} + +// ProjectOwnershipService provides methods for accessing Sentry project +// filters API endpoints. +type ProjectFilterService service + +// Get the filters. +func (s *ProjectFilterService) Get(ctx context.Context, organizationSlug string, projectSlug string) ([]*ProjectFilter, *Response, error) { + url := fmt.Sprintf("0/projects/%v/%v/filters/", organizationSlug, projectSlug) + req, err := s.client.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, nil, err + } + + var filters []*ProjectFilter + resp, err := s.client.Do(ctx, req, &filters) + if err != nil { + return nil, resp, err + } + + return filters, resp, nil +} + +// FilterConfig represents configuration for project filter +type FilterConfig struct { + BrowserExtension bool + LegacyBrowsers []string +} + +// GetFilterConfig retrieves filter configuration. +func (s *ProjectFilterService) GetFilterConfig(ctx context.Context, organizationSlug string, projectSlug string) (*FilterConfig, *Response, error) { + filters, resp, err := s.Get(ctx, organizationSlug, projectSlug) + if err != nil { + return nil, resp, err + } + + var filterConfig FilterConfig + + for _, filter := range filters { + switch filter.ID { + case "browser-extensions": + if string(filter.Active) == "true" { + filterConfig.BrowserExtension = true + } + + case "legacy-browsers": + if string(filter.Active) != "false" { + err = json.Unmarshal(filter.Active, &filterConfig.LegacyBrowsers) + if err != nil { + return nil, resp, err + } + } + } + } + + return &filterConfig, resp, err +} + +// BrowserExtensionParams defines parameters for browser extension request +type BrowserExtensionParams struct { + Active bool `json:"active"` +} + +// UpdateBrowserExtensions updates configuration for browser extension filter +func (s *ProjectFilterService) UpdateBrowserExtensions(ctx context.Context, organizationSlug string, projectSlug string, active bool) (*Response, error) { + url := fmt.Sprintf("0/projects/%v/%v/filters/browser-extensions/", organizationSlug, projectSlug) + params := BrowserExtensionParams{active} + req, err := s.client.NewRequest(http.MethodPut, url, params) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// LegacyBrowserParams defines parameters for legacy browser request +type LegacyBrowserParams struct { + Browsers []string `json:"subfilters"` +} + +// UpdateLegacyBrowser updates configuration for legacy browser filters +func (s *ProjectFilterService) UpdateLegacyBrowser(ctx context.Context, organizationSlug string, projectSlug string, browsers []string) (*Response, error) { + url := fmt.Sprintf("0/projects/%v/%v/filters/legacy-browsers/", organizationSlug, projectSlug) + params := LegacyBrowserParams{browsers} + + req, err := s.client.NewRequest(http.MethodPut, url, params) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/sentry/project_filter_test.go b/sentry/project_filter_test.go new file mode 100644 index 0000000..709f5b1 --- /dev/null +++ b/sentry/project_filter_test.go @@ -0,0 +1,163 @@ +package sentry + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProjectFilterService_GetWithLegacyExtension(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/powerful-abolitionist/filters/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, getWithLegacyExtensionHeader) + }) + + ctx := context.Background() + filterConfig, _, err := client.ProjectFilter.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") + assert.NoError(t, err) + + expected := FilterConfig{ + LegacyBrowsers: []string{"ie_pre_9"}, + BrowserExtension: false, + } + assert.Equal(t, &expected, filterConfig) +} + +func TestProjectFilterService_GetWithoutLegacyExtension(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/powerful-abolitionist/filters/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, getWithoutLegacyExtensionHeader) + }) + + ctx := context.Background() + filterConfig, _, err := client.ProjectFilter.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") + assert.NoError(t, err) + + expected := FilterConfig{ + LegacyBrowsers: nil, + BrowserExtension: true, + } + assert.Equal(t, &expected, filterConfig) +} + +func readRequestBody(r *http.Request) string { + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + + if err != nil { + panic(err) + } + str := string(b) + str = strings.TrimSuffix(str, "\n") + return str +} + +func TestBrowserExtensionFilter(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/test_org/test_project/filters/browser-extensions/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + body := readRequestBody(r) + assert.Equal(t, body, `{"active":true}`) + w.Header().Set("Content-Type", "application/json") + }) + + ctx := context.Background() + _, err := client.ProjectFilter.UpdateBrowserExtensions(ctx, "test_org", "test_project", true) + assert.NoError(t, err) +} + +func TestLegacyBrowserFilter(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/test_org/test_project/filters/legacy-browsers/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + body := readRequestBody(r) + assert.Equal(t, body, `{"subfilters":["ie_pre_9","ie10"]}`) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, "") + }) + + ctx := context.Background() + browsers := []string{"ie_pre_9", "ie10"} + _, err := client.ProjectFilter.UpdateLegacyBrowser(ctx, "test_org", "test_project", browsers) + assert.NoError(t, err) +} + +var ( + getWithLegacyExtensionHeader = `[ + { + "id":"browser-extensions", + "active":false, + "description":"description_1", + "name":"name_1", + "hello":"hello_1" + }, + { + "id":"localhost", + "active":false, + "description":"description_2", + "name":"name_2", + "hello":"hello_2" + }, + { + "id":"legacy-browsers", + "active":["ie_pre_9"], + "description":"description_3", + "name":"name_3", + "hello":"hello_3" + }, + { + "id":"web-crawlers", + "active":true, + "description":"description_4", + "name":"name_4", + "hello":"hello_4" + } + ]` + getWithoutLegacyExtensionHeader = `[ + { + "id":"browser-extensions", + "active":true, + "description":"description_1", + "name":"name_1", + "hello":"hello_1" + }, + { + "id":"localhost", + "active":false, + "description":"description_2", + "name":"name_2", + "hello":"hello_2" + }, + { + "id":"legacy-browsers", + "active":false, + "description":"description_3", + "name":"name_3", + "hello":"hello_3" + }, + { + "id":"web-crawlers", + "active":true, + "description":"description_4", + "name":"name_4", + "hello":"hello_4" + } + ]` +) diff --git a/sentry/projects.go b/sentry/projects.go index e4c196c..afdc1df 100644 --- a/sentry/projects.go +++ b/sentry/projects.go @@ -40,6 +40,7 @@ type Project struct { ResolveAge int `json:"resolveAge"` DataScrubber bool `json:"dataScrubber"` DataScrubberDefaults bool `json:"dataScrubberDefaults"` + GroupingEnhancements string `json:"groupingEnhancements"` SafeFields []string `json:"safeFields"` SensitiveFields []string `json:"sensitiveFields"` SubjectTemplate string `json:"subjectTemplate"` @@ -149,15 +150,16 @@ func (s *ProjectsService) Create(ctx context.Context, organizationSlug string, t // UpdateProjectParams are the parameters for ProjectService.Update. type UpdateProjectParams struct { - Name string `json:"name,omitempty"` - Slug string `json:"slug,omitempty"` - Platform string `json:"platform,omitempty"` - IsBookmarked *bool `json:"isBookmarked,omitempty"` - DigestsMinDelay *int `json:"digestsMinDelay,omitempty"` - DigestsMaxDelay *int `json:"digestsMaxDelay,omitempty"` - ResolveAge *int `json:"resolveAge,omitempty"` - Options map[string]interface{} `json:"options,omitempty"` - AllowedDomains []string `json:"allowedDomains,omitempty"` + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Platform string `json:"platform,omitempty"` + IsBookmarked *bool `json:"isBookmarked,omitempty"` + DigestsMinDelay *int `json:"digestsMinDelay,omitempty"` + DigestsMaxDelay *int `json:"digestsMaxDelay,omitempty"` + ResolveAge *int `json:"resolveAge,omitempty"` + Options map[string]interface{} `json:"options,omitempty"` + AllowedDomains []string `json:"allowedDomains,omitempty"` + GroupingEnhancements string `json:"groupingEnhancements,omitempty"` } // Update various attributes and configurable settings for a given project. diff --git a/sentry/projects_test.go b/sentry/projects_test.go index 8be3c6f..d938df4 100644 --- a/sentry/projects_test.go +++ b/sentry/projects_test.go @@ -134,6 +134,7 @@ func TestProjectsService_List(t *testing.T) { }, "platform": null, "slug": "pump-station", + "groupingEnhancements": "pump station grouping enhancement rule", "status": "active" } ]`) @@ -214,7 +215,8 @@ func TestProjectsService_List(t *testing.T) { Avatar: Avatar{ Type: "letter_avatar", }, - Organization: expectedOrganization, + Organization: expectedOrganization, + GroupingEnhancements: "pump station grouping enhancement rule", }, } assert.Equal(t, expected, projects) @@ -359,6 +361,7 @@ func TestProjectsService_Get(t *testing.T) { "name": "Powerful Abolitionist", "slug": "powerful-abolitionist" }], + "groupingEnhancements": "pump-station grouping enhancement rule", "verifySSL": false }`) }) @@ -414,6 +417,7 @@ func TestProjectsService_Get(t *testing.T) { AllowedDomains: []string{"*"}, DataScrubber: true, DataScrubberDefaults: true, + GroupingEnhancements: "pump-station grouping enhancement rule", SafeFields: []string{}, SensitiveFields: []string{}, SubjectTemplate: "$shortID - $title", @@ -534,6 +538,7 @@ func TestProjectsService_Update(t *testing.T) { "callSignReviewed": false, "id": "5", "subjectTemplate": "[$project] ${tag:level}: $title", + "groupingEnhancements": "Plane Proxy grouping enhancement rule", "name": "Plane Proxy" }`) }) @@ -566,10 +571,11 @@ func TestProjectsService_Update(t *testing.T) { "sentry:origins": "http://example.com\nhttp://example.invalid", "sentry:resolve_age": json.Number("720"), }, - DigestsMinDelay: 300, - DigestsMaxDelay: 1800, - ResolveAge: 720, - SubjectTemplate: "[$project] ${tag:level}: $title", + DigestsMinDelay: 300, + DigestsMaxDelay: 1800, + ResolveAge: 720, + SubjectTemplate: "[$project] ${tag:level}: $title", + GroupingEnhancements: "Plane Proxy grouping enhancement rule", } assert.Equal(t, expected, project) } diff --git a/sentry/sentry.go b/sentry/sentry.go index 5ccf886..a3ad2d9 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -57,6 +57,7 @@ type Client struct { ProjectOwnerships *ProjectOwnershipsService ProjectPlugins *ProjectPluginsService Projects *ProjectsService + ProjectFilter *ProjectFilterService Teams *TeamsService } @@ -84,6 +85,7 @@ func NewClient(httpClient *http.Client) *Client { c.MetricAlerts = (*MetricAlertsService)(&c.common) c.OrganizationMembers = (*OrganizationMembersService)(&c.common) c.Organizations = (*OrganizationsService)(&c.common) + c.ProjectFilter = (*ProjectFilterService)(&c.common) c.ProjectKeys = (*ProjectKeysService)(&c.common) c.ProjectOwnerships = (*ProjectOwnershipsService)(&c.common) c.ProjectPlugins = (*ProjectPluginsService)(&c.common) From ddbf69c864ba02cedf5b3f099e64e0d0950a57dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Aug 2022 22:29:07 +0100 Subject: [PATCH 03/41] chore(deps): update module go to 1.19 (#63) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 756b26b..c3dd590 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/jianyuan/go-sentry/v2 -go 1.18 +go 1.19 require ( github.com/google/go-querystring v1.1.0 From 3710411d189843e5279e26d93348a7d84173b8b1 Mon Sep 17 00:00:00 2001 From: Daniel Poon Date: Wed, 24 Aug 2022 06:23:54 +0800 Subject: [PATCH 04/41] Feature: Organization repository code mapping (#64) * Implement OrganizationIntegrationsService with List method * Implement OrganizationRepositoriesService with List method * Add Create and Delete to OrganizationRepositoriesService * Implement OrganizationCodeMappingsService with List method * Add Create, Update and Delete to OrganizationCodeMappingsService --- sentry/organization_code_mappings.go | 106 +++++++++ sentry/organization_code_mappings_test.go | 249 ++++++++++++++++++++++ sentry/organization_integrations.go | 68 ++++++ sentry/organization_integrations_test.go | 90 ++++++++ sentry/organization_repositories.go | 94 ++++++++ sentry/organization_repositories_test.go | 152 +++++++++++++ sentry/sentry.go | 30 +-- 7 files changed, 777 insertions(+), 12 deletions(-) create mode 100644 sentry/organization_code_mappings.go create mode 100644 sentry/organization_code_mappings_test.go create mode 100644 sentry/organization_integrations.go create mode 100644 sentry/organization_integrations_test.go create mode 100644 sentry/organization_repositories.go create mode 100644 sentry/organization_repositories_test.go diff --git a/sentry/organization_code_mappings.go b/sentry/organization_code_mappings.go new file mode 100644 index 0000000..b103dc0 --- /dev/null +++ b/sentry/organization_code_mappings.go @@ -0,0 +1,106 @@ +package sentry + +import ( + "context" + "fmt" +) + +// OrganizationCodeMapping represents a code mapping added for the organization. +// https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/repository_project_path_config.py +type OrganizationCodeMapping struct { + ID string `json:"id"` + ProjectId string `json:"projectId"` + ProjectSlug string `json:"projectSlug"` + RepoId string `json:"repoId"` + RepoName string `json:"repoName"` + IntegrationId string `json:"integrationId"` + Provider *OrganizationIntegrationProvider `json:"provider"` + StackRoot string `json:"stackRoot"` + SourceRoot string `json:"sourceRoot"` + DefaultBranch string `json:"defaultBranch"` +} + +// OrganizationCodeMappingsService provides methods for accessing Sentry organization code mappings API endpoints. +// Paths: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/urls.py#L929-L938 +// Endpoints: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/organization_code_mappings.py +// Endpoints: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/organization_code_mapping_details.py +type OrganizationCodeMappingsService service + +type ListOrganizationCodeMappingsParams struct { + ListCursorParams + IntegrationId string `url:"integrationId,omitempty"` +} + +// List organization integrations. +func (s *OrganizationCodeMappingsService) List(ctx context.Context, organizationSlug string, params *ListOrganizationCodeMappingsParams) ([]*OrganizationCodeMapping, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/code-mappings/", organizationSlug) + u, err := addQuery(u, params) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + integrations := []*OrganizationCodeMapping{} + resp, err := s.client.Do(ctx, req, &integrations) + if err != nil { + return nil, resp, err + } + return integrations, resp, nil +} + +// https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/organization_code_mappings.py#L26-L35 +type CreateOrganizationCodeMappingParams struct { + DefaultBranch string `json:"defaultBranch"` + StackRoot string `json:"stackRoot"` + SourceRoot string `json:"sourceRoot"` + RepositoryId string `json:"repositoryId"` + IntegrationId string `json:"integrationId"` + ProjectId string `json:"projectId"` +} + +func (s *OrganizationCodeMappingsService) Create(ctx context.Context, organizationSlug string, params CreateOrganizationCodeMappingParams) (*OrganizationCodeMapping, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/code-mappings/", organizationSlug) + req, err := s.client.NewRequest("POST", u, params) + if err != nil { + return nil, nil, err + } + + repo := new(OrganizationCodeMapping) + resp, err := s.client.Do(ctx, req, repo) + if err != nil { + return nil, resp, err + } + return repo, resp, nil +} + +// https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/organization_code_mappings.py#L26-L35 +type UpdateOrganizationCodeMappingParams CreateOrganizationCodeMappingParams + +func (s *OrganizationCodeMappingsService) Update(ctx context.Context, organizationSlug string, codeMappingId string, params UpdateOrganizationCodeMappingParams) (*OrganizationCodeMapping, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/code-mappings/%v/", organizationSlug, codeMappingId) + req, err := s.client.NewRequest("PUT", u, params) + if err != nil { + return nil, nil, err + } + + repo := new(OrganizationCodeMapping) + resp, err := s.client.Do(ctx, req, repo) + if err != nil { + return nil, resp, err + } + return repo, resp, nil +} + +func (s *OrganizationCodeMappingsService) Delete(ctx context.Context, organizationSlug string, codeMappingId string) (*Response, error) { + u := fmt.Sprintf("0/organizations/%v/code-mappings/%v/", organizationSlug, codeMappingId) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/sentry/organization_code_mappings_test.go b/sentry/organization_code_mappings_test.go new file mode 100644 index 0000000..ce451e7 --- /dev/null +++ b/sentry/organization_code_mappings_test.go @@ -0,0 +1,249 @@ +package sentry + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOrganizationCodeMappingsService_List(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/code-mappings/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + assertQuery(t, map[string]string{"cursor": "100:-1:1", "integrationId": "123456"}, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[ + { + "id": "54321", + "projectId": "7654321", + "projectSlug": "spoon-knife", + "repoId": "456123", + "repoName": "octocat/Spoon-Knife", + "integrationId": "123456", + "provider": { + "key": "github", + "slug": "github", + "name": "GitHub", + "canAdd": true, + "canDisable": false, + "features": [ + "codeowners", + "commits", + "issue-basic", + "stacktrace-link" + ], + "aspects": {} + }, + "stackRoot": "/", + "sourceRoot": "src/", + "defaultBranch": "main" + } + ]`) + }) + + ctx := context.Background() + integrations, _, err := client.OrganizationCodeMappings.List(ctx, "the-interstellar-jurisdiction", &ListOrganizationCodeMappingsParams{ + ListCursorParams: ListCursorParams{ + Cursor: "100:-1:1", + }, + IntegrationId: "123456", + }) + assert.NoError(t, err) + expected := []*OrganizationCodeMapping{ + { + ID: "54321", + ProjectId: "7654321", + ProjectSlug: "spoon-knife", + RepoId: "456123", + RepoName: "octocat/Spoon-Knife", + IntegrationId: "123456", + Provider: &OrganizationIntegrationProvider{ + Key: "github", + Slug: "github", + Name: "GitHub", + CanAdd: true, + CanDisable: false, + Features: []string{ + "codeowners", + "commits", + "issue-basic", + "stacktrace-link", + }, + }, + StackRoot: "/", + SourceRoot: "src/", + DefaultBranch: "main", + }, + } + assert.Equal(t, expected, integrations) +} + +func TestOrganizationCodeMappingsService_Create(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/code-mappings/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + w.WriteHeader(http.StatusCreated) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "id": "54321", + "projectId": "7654321", + "projectSlug": "spoon-knife", + "repoId": "456123", + "repoName": "octocat/Spoon-Knife", + "integrationId": "123456", + "provider": { + "key": "github", + "slug": "github", + "name": "GitHub", + "canAdd": true, + "canDisable": false, + "features": [ + "codeowners", + "commits", + "issue-basic", + "stacktrace-link" + ], + "aspects": {} + }, + "stackRoot": "/", + "sourceRoot": "src/", + "defaultBranch": "main" + }`) + }) + + ctx := context.Background() + createOrganizationCodeMappingParams := CreateOrganizationCodeMappingParams{ + DefaultBranch: "main", + StackRoot: "/", + SourceRoot: "src/", + RepositoryId: "456123", + IntegrationId: "123456", + ProjectId: "7654321", + } + codeMapping, _, err := client.OrganizationCodeMappings.Create(ctx, "the-interstellar-jurisdiction", createOrganizationCodeMappingParams) + assert.NoError(t, err) + expected := &OrganizationCodeMapping{ + ID: "54321", + ProjectId: "7654321", + ProjectSlug: "spoon-knife", + RepoId: "456123", + RepoName: "octocat/Spoon-Knife", + IntegrationId: "123456", + Provider: &OrganizationIntegrationProvider{ + Key: "github", + Slug: "github", + Name: "GitHub", + CanAdd: true, + CanDisable: false, + Features: []string{ + "codeowners", + "commits", + "issue-basic", + "stacktrace-link", + }, + }, + StackRoot: "/", + SourceRoot: "src/", + DefaultBranch: "main", + } + assert.Equal(t, expected, codeMapping) +} + +func TestOrganizationCodeMappingsService_Update(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + codeMappingId := "54321" + + mux.HandleFunc(fmt.Sprintf("/api/0/organizations/the-interstellar-jurisdiction/code-mappings/%s/", codeMappingId), func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "PUT", r) + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "id": "%s", + "projectId": "7654321", + "projectSlug": "spoon-knife", + "repoId": "456123", + "repoName": "octocat/Spoon-Knife", + "integrationId": "123456", + "provider": { + "key": "github", + "slug": "github", + "name": "GitHub", + "canAdd": true, + "canDisable": false, + "features": [ + "codeowners", + "commits", + "issue-basic", + "stacktrace-link" + ], + "aspects": {} + }, + "stackRoot": "/", + "sourceRoot": "src/", + "defaultBranch": "main" + }`, codeMappingId) + }) + + ctx := context.Background() + updateOrganizationCodeMappingParams := UpdateOrganizationCodeMappingParams{ + DefaultBranch: "main", + StackRoot: "/", + SourceRoot: "src/", + RepositoryId: "456123", + IntegrationId: "123456", + ProjectId: "7654321", + } + codeMapping, _, err := client.OrganizationCodeMappings.Update(ctx, "the-interstellar-jurisdiction", codeMappingId, updateOrganizationCodeMappingParams) + assert.NoError(t, err) + expected := &OrganizationCodeMapping{ + ID: codeMappingId, + ProjectId: "7654321", + ProjectSlug: "spoon-knife", + RepoId: "456123", + RepoName: "octocat/Spoon-Knife", + IntegrationId: "123456", + Provider: &OrganizationIntegrationProvider{ + Key: "github", + Slug: "github", + Name: "GitHub", + CanAdd: true, + CanDisable: false, + Features: []string{ + "codeowners", + "commits", + "issue-basic", + "stacktrace-link", + }, + }, + StackRoot: "/", + SourceRoot: "src/", + DefaultBranch: "main", + } + assert.Equal(t, expected, codeMapping) +} + +func TestOrganizationCodeMappingsService_Delete(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + codeMappingId := "54321" + + mux.HandleFunc(fmt.Sprintf("/api/0/organizations/the-interstellar-jurisdiction/code-mappings/%s/", codeMappingId), func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "DELETE", r) + w.WriteHeader(http.StatusNoContent) + w.Header().Set("Content-Type", "application/json") + }) + + ctx := context.Background() + _, err := client.OrganizationCodeMappings.Delete(ctx, "the-interstellar-jurisdiction", codeMappingId) + assert.NoError(t, err) +} diff --git a/sentry/organization_integrations.go b/sentry/organization_integrations.go new file mode 100644 index 0000000..cb52c68 --- /dev/null +++ b/sentry/organization_integrations.go @@ -0,0 +1,68 @@ +package sentry + +import ( + "context" + "fmt" + "time" +) + +// https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/integration.py#L22 +type OrganizationIntegrationProvider struct { + Key string `json:"key"` + Slug string `json:"slug"` + Name string `json:"name"` + CanAdd bool `json:"canAdd"` + CanDisable bool `json:"canDisable"` + Features []string `json:"features"` +} + +// OrganizationIntegration represents an integration added for the organization. +// https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/integration.py#L93 +type OrganizationIntegration struct { + // https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/integration.py#L35 + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + DomainName string `json:"domainName"` + AccountType string `json:"accountType"` + Scopes *string `json:"scopes"` + Status string `json:"status"` + Provider OrganizationIntegrationProvider `json:"provider"` + + // https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/integration.py#L138 + ExternalId string `json:"externalId"` + OrganizationId int `json:"organizationId"` + OrganizationIntegrationStatus string `json:"organizationIntegrationStatus"` + GracePeriodEnd *time.Time `json:"gracePeriodEnd"` +} + +// OrganizationIntegrationsService provides methods for accessing Sentry organization integrations API endpoints. +// Paths: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/urls.py#L1236-L1240 +// Endpoints: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/integrations/organization_integrations/index.py +type OrganizationIntegrationsService service + +type ListOrganizationIntegrationsParams struct { + ListCursorParams + ProviderKey string `url:"provider_key,omitempty"` +} + +// List organization integrations. +func (s *OrganizationIntegrationsService) List(ctx context.Context, organizationSlug string, params *ListOrganizationIntegrationsParams) ([]*OrganizationIntegration, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/integrations/", organizationSlug) + u, err := addQuery(u, params) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + integrations := []*OrganizationIntegration{} + resp, err := s.client.Do(ctx, req, &integrations) + if err != nil { + return nil, resp, err + } + return integrations, resp, nil +} diff --git a/sentry/organization_integrations_test.go b/sentry/organization_integrations_test.go new file mode 100644 index 0000000..edca9f9 --- /dev/null +++ b/sentry/organization_integrations_test.go @@ -0,0 +1,90 @@ +package sentry + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOrganizationIntegrationsService_List(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/integrations/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + assertQuery(t, map[string]string{"cursor": "100:-1:1", "provider_key": "github"}, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[ + { + "id": "123456", + "name": "octocat", + "icon": "https://avatars.githubusercontent.com/u/583231?v=4", + "domainName": "github.com/octocat", + "accountType": "Organization", + "scopes": null, + "status": "active", + "provider": { + "key": "github", + "slug": "github", + "name": "GitHub", + "canAdd": true, + "canDisable": false, + "features": [ + "codeowners", + "commits", + "issue-basic", + "stacktrace-link" + ], + "aspects": {} + }, + "configOrganization": [], + "configData": {}, + "externalId": "87654321", + "organizationId": 2, + "organizationIntegrationStatus": "active", + "gracePeriodEnd": null + } + ]`) + }) + + ctx := context.Background() + integrations, _, err := client.OrganizationIntegrations.List(ctx, "the-interstellar-jurisdiction", &ListOrganizationIntegrationsParams{ + ListCursorParams: ListCursorParams{ + Cursor: "100:-1:1", + }, + ProviderKey: "github", + }) + assert.NoError(t, err) + expected := []*OrganizationIntegration{ + { + ID: "123456", + Name: "octocat", + Icon: "https://avatars.githubusercontent.com/u/583231?v=4", + DomainName: "github.com/octocat", + AccountType: "Organization", + Scopes: nil, + Status: "active", + Provider: OrganizationIntegrationProvider{ + Key: "github", + Slug: "github", + Name: "GitHub", + CanAdd: true, + CanDisable: false, + Features: []string{ + "codeowners", + "commits", + "issue-basic", + "stacktrace-link", + }, + }, + ExternalId: "87654321", + OrganizationId: 2, + OrganizationIntegrationStatus: "active", + GracePeriodEnd: nil, + }, + } + assert.Equal(t, expected, integrations) +} diff --git a/sentry/organization_repositories.go b/sentry/organization_repositories.go new file mode 100644 index 0000000..f935d90 --- /dev/null +++ b/sentry/organization_repositories.go @@ -0,0 +1,94 @@ +package sentry + +import ( + "context" + "fmt" + "time" +) + +// https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/repository.py#L12-L17 +type OrganizationRepositoryProvider struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// OrganizationRepositories represents +// https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/repository.py +type OrganizationRepository struct { + ID string `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + Provider OrganizationRepositoryProvider `json:"provider"` + Status string `json:"status"` + DateCreated time.Time `json:"dateCreated"` + IntegrationId string `json:"integrationId"` + ExternalSlug string `json:"externalSlug"` +} + +// OrganizationRepositoriesService provides methods for accessing Sentry organization repositories API endpoints. +// Paths: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/urls.py#L1385-L1394 +// Endpoints: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/organization_repositories.py +// Endpoints: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/organization_repository_details.py +type OrganizationRepositoriesService service + +type ListOrganizationRepositoriesParams struct { + ListCursorParams + // omitting status defaults to only active. + // sending empty string shows everything, which is a more reasonable default. + Status string `url:"status"` + Query string `url:"query,omitempty"` +} + +// List organization integrations. +func (s *OrganizationRepositoriesService) List(ctx context.Context, organizationSlug string, params *ListOrganizationRepositoriesParams) ([]*OrganizationRepository, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/repos/", organizationSlug) + u, err := addQuery(u, params) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + repos := []*OrganizationRepository{} + resp, err := s.client.Do(ctx, req, &repos) + if err != nil { + return nil, resp, err + } + return repos, resp, nil +} + +// Fields are different for different providers +type CreateOrganizationRepositoryParams map[string]interface{} + +func (s *OrganizationRepositoriesService) Create(ctx context.Context, organizationSlug string, params CreateOrganizationRepositoryParams) (*OrganizationRepository, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/repos/", organizationSlug) + req, err := s.client.NewRequest("POST", u, params) + if err != nil { + return nil, nil, err + } + + repo := new(OrganizationRepository) + resp, err := s.client.Do(ctx, req, repo) + if err != nil { + return nil, resp, err + } + return repo, resp, nil +} + +func (s *OrganizationRepositoriesService) Delete(ctx context.Context, organizationSlug string, repoID string) (*OrganizationRepository, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/repos/%v/", organizationSlug, repoID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, nil, err + } + + repo := new(OrganizationRepository) + resp, err := s.client.Do(ctx, req, repo) + if err != nil { + return nil, resp, err + } + return repo, resp, nil +} diff --git a/sentry/organization_repositories_test.go b/sentry/organization_repositories_test.go new file mode 100644 index 0000000..b4ebca6 --- /dev/null +++ b/sentry/organization_repositories_test.go @@ -0,0 +1,152 @@ +package sentry + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOrganizationRepositoriesService_List(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/repos/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + assertQuery(t, map[string]string{"cursor": "100:-1:1", "status": "", "query": "foo"}, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[ + { + "id": "456123", + "name": "octocat/Spoon-Knife", + "url": "https://github.com/octocat/Spoon-Knife", + "provider": { + "id": "integrations:github", + "name": "GitHub" + }, + "status": "active", + "dateCreated": "2022-08-15T06:31:49.817916Z", + "integrationId": "123456", + "externalSlug": "aht4davchml6srhh6mvthluoscl2lzmi" + } + ]`) + }) + + ctx := context.Background() + repos, _, err := client.OrganizationRepositories.List(ctx, "the-interstellar-jurisdiction", &ListOrganizationRepositoriesParams{ + ListCursorParams: ListCursorParams{ + Cursor: "100:-1:1", + }, + Query: "foo", + }) + assert.NoError(t, err) + expected := []*OrganizationRepository{ + { + ID: "456123", + Name: "octocat/Spoon-Knife", + Url: "https://github.com/octocat/Spoon-Knife", + Provider: OrganizationRepositoryProvider{ + ID: "integrations:github", + Name: "GitHub", + }, + Status: "active", + DateCreated: mustParseTime("2022-08-15T06:31:49.817916Z"), + IntegrationId: "123456", + ExternalSlug: "aht4davchml6srhh6mvthluoscl2lzmi", + }, + } + assert.Equal(t, expected, repos) +} + +func TestOrganizationRepositoriesService_Create(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/repos/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + w.WriteHeader(http.StatusCreated) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "id": "456123", + "name": "octocat/Spoon-Knife", + "url": "https://github.com/octocat/Spoon-Knife", + "provider": { + "id": "integrations:github", + "name": "GitHub" + }, + "status": "active", + "dateCreated": "2022-08-15T06:31:49.817916Z", + "integrationId": "123456", + "externalSlug": "aht4davchml6srhh6mvthluoscl2lzmi" + }`) + }) + + ctx := context.Background() + createOrganizationRepositoryParams := CreateOrganizationRepositoryParams{ + "installation": "123456", + "identifier": "octocat/Spoon-Knife", + "provider": "integrations:github", + } + repo, _, err := client.OrganizationRepositories.Create(ctx, "the-interstellar-jurisdiction", createOrganizationRepositoryParams) + assert.NoError(t, err) + expected := &OrganizationRepository{ + ID: "456123", + Name: "octocat/Spoon-Knife", + Url: "https://github.com/octocat/Spoon-Knife", + Provider: OrganizationRepositoryProvider{ + ID: "integrations:github", + Name: "GitHub", + }, + Status: "active", + DateCreated: mustParseTime("2022-08-15T06:31:49.817916Z"), + IntegrationId: "123456", + ExternalSlug: "aht4davchml6srhh6mvthluoscl2lzmi", + } + assert.Equal(t, expected, repo) +} + +func TestOrganizationRepositoriesService_Delete(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + repoId := "456123" + + mux.HandleFunc(fmt.Sprintf("/api/0/organizations/the-interstellar-jurisdiction/repos/%s/", repoId), func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "DELETE", r) + w.WriteHeader(http.StatusAccepted) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "id": "456123", + "name": "octocat/Spoon-Knife", + "url": "https://github.com/octocat/Spoon-Knife", + "provider": { + "id": "integrations:github", + "name": "GitHub" + }, + "status": "pending_deletion", + "dateCreated": "2022-08-15T06:31:49.817916Z", + "integrationId": "123456", + "externalSlug": "aht4davchml6srhh6mvthluoscl2lzmi" + }`) + }) + + ctx := context.Background() + repo, _, err := client.OrganizationRepositories.Delete(ctx, "the-interstellar-jurisdiction", repoId) + assert.NoError(t, err) + expected := &OrganizationRepository{ + ID: "456123", + Name: "octocat/Spoon-Knife", + Url: "https://github.com/octocat/Spoon-Knife", + Provider: OrganizationRepositoryProvider{ + ID: "integrations:github", + Name: "GitHub", + }, + Status: "pending_deletion", + DateCreated: mustParseTime("2022-08-15T06:31:49.817916Z"), + IntegrationId: "123456", + ExternalSlug: "aht4davchml6srhh6mvthluoscl2lzmi", + } + assert.Equal(t, expected, repo) +} diff --git a/sentry/sentry.go b/sentry/sentry.go index a3ad2d9..1f39421 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -47,18 +47,21 @@ type Client struct { common service // Services - DashboardWidgets *DashboardWidgetsService - Dashboards *DashboardsService - IssueAlerts *IssueAlertsService - MetricAlerts *MetricAlertsService - OrganizationMembers *OrganizationMembersService - Organizations *OrganizationsService - ProjectKeys *ProjectKeysService - ProjectOwnerships *ProjectOwnershipsService - ProjectPlugins *ProjectPluginsService - Projects *ProjectsService - ProjectFilter *ProjectFilterService - Teams *TeamsService + DashboardWidgets *DashboardWidgetsService + Dashboards *DashboardsService + IssueAlerts *IssueAlertsService + MetricAlerts *MetricAlertsService + OrganizationCodeMappings *OrganizationCodeMappingsService + OrganizationIntegrations *OrganizationIntegrationsService + OrganizationMembers *OrganizationMembersService + OrganizationRepositories *OrganizationRepositoriesService + Organizations *OrganizationsService + ProjectKeys *ProjectKeysService + ProjectOwnerships *ProjectOwnershipsService + ProjectPlugins *ProjectPluginsService + Projects *ProjectsService + ProjectFilter *ProjectFilterService + Teams *TeamsService } type service struct { @@ -83,7 +86,10 @@ func NewClient(httpClient *http.Client) *Client { c.Dashboards = (*DashboardsService)(&c.common) c.IssueAlerts = (*IssueAlertsService)(&c.common) c.MetricAlerts = (*MetricAlertsService)(&c.common) + c.OrganizationCodeMappings = (*OrganizationCodeMappingsService)(&c.common) + c.OrganizationIntegrations = (*OrganizationIntegrationsService)(&c.common) c.OrganizationMembers = (*OrganizationMembersService)(&c.common) + c.OrganizationRepositories = (*OrganizationRepositoriesService)(&c.common) c.Organizations = (*OrganizationsService)(&c.common) c.ProjectFilter = (*ProjectFilterService)(&c.common) c.ProjectKeys = (*ProjectKeysService)(&c.common) From 250257c38acc64e68a4bb8448303e281b63d2060 Mon Sep 17 00:00:00 2001 From: Taj Pereira Date: Thu, 6 Oct 2022 06:28:36 +1030 Subject: [PATCH 05/41] fix: use string array for scopes type (#66) * fix: use string array for scopes type * Apply suggestions from code review --- sentry/organization_integrations.go | 2 +- sentry/organization_integrations_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry/organization_integrations.go b/sentry/organization_integrations.go index cb52c68..2653889 100644 --- a/sentry/organization_integrations.go +++ b/sentry/organization_integrations.go @@ -25,7 +25,7 @@ type OrganizationIntegration struct { Icon string `json:"icon"` DomainName string `json:"domainName"` AccountType string `json:"accountType"` - Scopes *string `json:"scopes"` + Scopes []string `json:"scopes"` Status string `json:"status"` Provider OrganizationIntegrationProvider `json:"provider"` diff --git a/sentry/organization_integrations_test.go b/sentry/organization_integrations_test.go index edca9f9..028afcd 100644 --- a/sentry/organization_integrations_test.go +++ b/sentry/organization_integrations_test.go @@ -24,7 +24,7 @@ func TestOrganizationIntegrationsService_List(t *testing.T) { "icon": "https://avatars.githubusercontent.com/u/583231?v=4", "domainName": "github.com/octocat", "accountType": "Organization", - "scopes": null, + "scopes": ["read", "write"], "status": "active", "provider": { "key": "github", @@ -65,7 +65,7 @@ func TestOrganizationIntegrationsService_List(t *testing.T) { Icon: "https://avatars.githubusercontent.com/u/583231?v=4", DomainName: "github.com/octocat", AccountType: "Organization", - Scopes: nil, + Scopes: []string{"read", "write"}, Status: "active", Provider: OrganizationIntegrationProvider{ Key: "github", From ce7d9ba54f05098154236b6016fe9015a027d006 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Nov 2022 11:57:55 +0000 Subject: [PATCH 06/41] fix(deps): update module github.com/peterhellberg/link to v1.2.0 (#68) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c3dd590..c1c4ee0 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/google/go-querystring v1.1.0 - github.com/peterhellberg/link v1.1.0 + github.com/peterhellberg/link v1.2.0 github.com/stretchr/testify v1.8.0 ) diff --git a/go.sum b/go.sum index 987b949..777ea81 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/peterhellberg/link v1.1.0 h1:s2+RH8EGuI/mI4QwrWGSYQCRz7uNgip9BaM04HKu5kc= github.com/peterhellberg/link v1.1.0/go.mod h1:gtSlOT4jmkY8P47hbTc8PTgiDDWpdPbFYl75keYyBB8= +github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= +github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 42e3d516a8e3d9c35c99da1c63edfb7af9baf5bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Nov 2022 12:00:03 +0000 Subject: [PATCH 07/41] fix(deps): update module github.com/stretchr/testify to v1.8.1 (#67) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c1c4ee0..1c19955 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/google/go-querystring v1.1.0 github.com/peterhellberg/link v1.2.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 ) require ( diff --git a/go.sum b/go.sum index 777ea81..2d481db 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= @@ -22,6 +23,8 @@ github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324 github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 54108e438df8d7fe73c14b8c9d497abb5b451c5d Mon Sep 17 00:00:00 2001 From: eugeniykurasov <92024390+eugeniykurasov@users.noreply.github.com> Date: Tue, 29 Nov 2022 21:29:48 +0200 Subject: [PATCH 08/41] feat: Available set eventTypes when create alerts. (#69) When you create dataset: events. By default he create eventTypes:['errors']. So you can't create alerts with event_type: default or both. --- sentry/metric_alerts.go | 1 + sentry/metric_alerts_test.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/sentry/metric_alerts.go b/sentry/metric_alerts.go index 9d8f77a..e5cb74b 100644 --- a/sentry/metric_alerts.go +++ b/sentry/metric_alerts.go @@ -13,6 +13,7 @@ type MetricAlert struct { Name *string `json:"name,omitempty"` Environment *string `json:"environment,omitempty"` DataSet *string `json:"dataset,omitempty"` + EventTypes []string `json:"eventTypes,omitempty"` Query *string `json:"query,omitempty"` Aggregate *string `json:"aggregate,omitempty"` TimeWindow *float64 `json:"timeWindow,omitempty"` diff --git a/sentry/metric_alerts_test.go b/sentry/metric_alerts_test.go index f113c12..d98da34 100644 --- a/sentry/metric_alerts_test.go +++ b/sentry/metric_alerts_test.go @@ -24,6 +24,7 @@ func TestMetricAlertService_List(t *testing.T) { "name": "pump-station-alert", "environment": "production", "dataset": "transactions", + "eventTypes": ["transaction"], "query": "http.url:http://service/unreadmessages", "aggregate": "p50(transaction.duration)", "thresholdType": 0, @@ -75,6 +76,7 @@ func TestMetricAlertService_List(t *testing.T) { DataSet: String("transactions"), Query: String("http.url:http://service/unreadmessages"), Aggregate: String("p50(transaction.duration)"), + EventTypes: []string{"transaction"}, ThresholdType: Int(0), ResolveThreshold: Float64(100.0), TimeWindow: Float64(5.0), @@ -123,6 +125,7 @@ func TestMetricAlertService_Get(t *testing.T) { "name": "pump-station-alert", "environment": "production", "dataset": "transactions", + "eventTypes": ["transaction"], "query": "http.url:http://service/unreadmessages", "aggregate": "p50(transaction.duration)", "timeWindow": 10, @@ -171,6 +174,7 @@ func TestMetricAlertService_Get(t *testing.T) { Name: String("pump-station-alert"), Environment: String("production"), DataSet: String("transactions"), + EventTypes: []string{"transaction"}, Query: String("http.url:http://service/unreadmessages"), Aggregate: String("p50(transaction.duration)"), TimeWindow: Float64(10), @@ -220,6 +224,7 @@ func TestMetricAlertService_Create(t *testing.T) { "name": "pump-station-alert", "environment": "production", "dataset": "transactions", + "eventTypes": ["transaction"], "query": "http.url:http://service/unreadmessages", "aggregate": "p50(transaction.duration)", "timeWindow": 10, @@ -304,6 +309,7 @@ func TestMetricAlertService_Create(t *testing.T) { Name: String("pump-station-alert"), Environment: String("production"), DataSet: String("transactions"), + EventTypes: []string{"transaction"}, Query: String("http.url:http://service/unreadmessages"), Aggregate: String("p50(transaction.duration)"), ThresholdType: Int(0), @@ -405,6 +411,7 @@ func TestMetricAlertService_Update(t *testing.T) { "name": "pump-station-alert", "environment": "production", "dataset": "transactions", + "eventTypes": ["transaction"], "query": "http.url:http://service/unreadmessages", "aggregate": "p50(transaction.duration)", "timeWindow": 10, @@ -453,6 +460,7 @@ func TestMetricAlertService_Update(t *testing.T) { Name: String("pump-station-alert"), Environment: String("production"), DataSet: String("transactions"), + EventTypes: []string{"transaction"}, Query: String("http.url:http://service/unreadmessages"), Aggregate: String("p50(transaction.duration)"), ThresholdType: Int(0), From 805b3a926e7530ad56e3096e0388e6296a1f77c8 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Mon, 12 Dec 2022 13:30:35 +0000 Subject: [PATCH 09/41] Switch endpoint (#71) --- sentry/metric_alerts.go | 3 ++- sentry/metric_alerts_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sentry/metric_alerts.go b/sentry/metric_alerts.go index e5cb74b..9362849 100644 --- a/sentry/metric_alerts.go +++ b/sentry/metric_alerts.go @@ -76,7 +76,8 @@ func (s *MetricAlertsService) List(ctx context.Context, organizationSlug string, // Get details on an issue alert. func (s *MetricAlertsService) Get(ctx context.Context, organizationSlug string, projectSlug string, id string) (*MetricAlert, *Response, error) { - u := fmt.Sprintf("0/projects/%v/%v/alert-rules/%v/", organizationSlug, projectSlug, id) + // TODO: Remove projectSlug argument + u := fmt.Sprintf("0/organizations/%v/alert-rules/%v/", organizationSlug, id) req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, nil, err diff --git a/sentry/metric_alerts_test.go b/sentry/metric_alerts_test.go index d98da34..09c8f6b 100644 --- a/sentry/metric_alerts_test.go +++ b/sentry/metric_alerts_test.go @@ -116,7 +116,7 @@ func TestMetricAlertService_Get(t *testing.T) { client, mux, _, teardown := setup() defer teardown() - mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/alert-rules/12345/", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/alert-rules/12345/", func(w http.ResponseWriter, r *http.Request) { assertMethod(t, "GET", r) w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, ` From 8f147c7d7e2fda2420a41cb5543776493c1d7cda Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Mon, 12 Dec 2022 21:06:23 +0000 Subject: [PATCH 10/41] Run go fmt --- sentry/errors.go | 7 +++---- sentry/organization_integrations.go | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sentry/errors.go b/sentry/errors.go index bb6ad50..dac7627 100644 --- a/sentry/errors.go +++ b/sentry/errors.go @@ -8,10 +8,9 @@ import ( // APIError represents a Sentry API Error response. // Should look like: // -// type apiError struct { -// Detail string `json:"detail"` -// } -// +// type apiError struct { +// Detail string `json:"detail"` +// } type APIError struct { f interface{} // unknown } diff --git a/sentry/organization_integrations.go b/sentry/organization_integrations.go index 2653889..6ddfbe5 100644 --- a/sentry/organization_integrations.go +++ b/sentry/organization_integrations.go @@ -25,7 +25,7 @@ type OrganizationIntegration struct { Icon string `json:"icon"` DomainName string `json:"domainName"` AccountType string `json:"accountType"` - Scopes []string `json:"scopes"` + Scopes []string `json:"scopes"` Status string `json:"status"` Provider OrganizationIntegrationProvider `json:"provider"` From 9a37263e941fa308e5923dc165470cf9589d214c Mon Sep 17 00:00:00 2001 From: eugeniykurasov <92024390+eugeniykurasov@users.noreply.github.com> Date: Tue, 3 Jan 2023 20:08:59 +0200 Subject: [PATCH 11/41] fix(#72): Async metric create. (#73) --- sentry/metric_alerts.go | 63 ++++++++++++ sentry/metric_alerts_test.go | 187 +++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) diff --git a/sentry/metric_alerts.go b/sentry/metric_alerts.go index 9362849..68428b3 100644 --- a/sentry/metric_alerts.go +++ b/sentry/metric_alerts.go @@ -2,6 +2,7 @@ package sentry import ( "context" + "errors" "fmt" "time" ) @@ -23,6 +24,15 @@ type MetricAlert struct { Projects []string `json:"projects,omitempty"` Owner *string `json:"owner,omitempty"` DateCreated *time.Time `json:"dateCreated,omitempty"` + TaskUUID *string `json:"uuid,omitempty"` // This is actually the UUID of the async task that can be spawned to create the metric +} + +// MetricAlertTaskDetail represents the inline struct Sentry defines for task details +// https://github.com/getsentry/sentry/blob/22.12.0/src/sentry/incidents/endpoints/project_alert_rule_task_details.py#L31 +type MetricAlertTaskDetail struct { + Status *string `json:"status,omitempty"` + AlertRule *MetricAlert `json:"alertRule,omitempty"` + Error *string `json:"error,omitempty"` } // MetricAlertTrigger represents a metric alert trigger. @@ -104,6 +114,15 @@ func (s *MetricAlertsService) Create(ctx context.Context, organizationSlug strin if err != nil { return nil, resp, err } + + if resp.StatusCode == 202 { + if alert.TaskUUID == nil { + return nil, resp, errors.New("missing task uuid") + } + // We just received a reference to an async task, we need to check another endpoint to retrieve the metric alert we created + return s.getMetricAlertFromMetricAlertTaskDetail(ctx, organizationSlug, projectSlug, *alert.TaskUUID) + } + return alert, resp, nil } @@ -120,9 +139,53 @@ func (s *MetricAlertsService) Update(ctx context.Context, organizationSlug strin if err != nil { return nil, resp, err } + + if resp.StatusCode == 202 { + if alert.TaskUUID == nil { + return nil, resp, errors.New("missing task uuid") + } + // We just received a reference to an async task, we need to check another endpoint to retrieve the metric alert we created + return s.getMetricAlertFromMetricAlertTaskDetail(ctx, organizationSlug, projectSlug, *alert.TaskUUID) + } + return alert, resp, nil } +func (s *MetricAlertsService) getMetricAlertFromMetricAlertTaskDetail(ctx context.Context, organizationSlug string, projectSlug string, taskUUID string) (*MetricAlert, *Response, error) { + u := fmt.Sprintf("0/projects/%v/%v/alert-rule-task/%v/", organizationSlug, projectSlug, taskUUID) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var resp *Response + for i := 0; i < 5; i++ { + time.Sleep(5 * time.Second) + + taskDetail := new(MetricAlertTaskDetail) + resp, err := s.client.Do(ctx, req, taskDetail) + if err != nil { + return nil, resp, err + } + + if resp.StatusCode == 404 { + return nil, resp, fmt.Errorf("cannot find metric alert creation task with UUID %v", taskUUID) + } + if taskDetail.Status != nil && taskDetail.AlertRule != nil { + if *taskDetail.Status == "success" { + return taskDetail.AlertRule, resp, err + } else if *taskDetail.Status == "failed" { + if taskDetail != nil { + return taskDetail.AlertRule, resp, errors.New(*taskDetail.Error) + } + + return taskDetail.AlertRule, resp, errors.New("error while running the metric alert creation task") + } + } + } + return nil, resp, errors.New("getting the status of the metric alert creation from Sentry took too long") +} + // Delete an Alert Rule. func (s *MetricAlertsService) Delete(ctx context.Context, organizationSlug string, projectSlug string, alertRuleID string) (*Response, error) { u := fmt.Sprintf("0/projects/%v/%v/alert-rules/%v/", organizationSlug, projectSlug, alertRuleID) diff --git a/sentry/metric_alerts_test.go b/sentry/metric_alerts_test.go index 09c8f6b..e4c40a6 100644 --- a/sentry/metric_alerts_test.go +++ b/sentry/metric_alerts_test.go @@ -211,6 +211,193 @@ func TestMetricAlertService_Get(t *testing.T) { require.Equal(t, expected, alert) } +func TestMetricAlertsService_CreateWithAsyncTask(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/alert-rule-task/fakeuuid/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, ` + { + "status": "success", + "error": null, + "alertRule": { + "id": "12345", + "name": "pump-station-alert", + "environment": "production", + "dataset": "transactions", + "eventTypes": ["transaction"], + "query": "http.url:http://service/unreadmessages", + "aggregate": "p50(transaction.duration)", + "timeWindow": 10, + "thresholdType": 0, + "resolveThreshold": 0, + "triggers": [ + { + "actions": [ + { + "alertRuleTriggerId": "56789", + "dateCreated": "2022-04-15T15:06:01.087054Z", + "desc": "Send a Slack notification to #alert-rule-alerts", + "id": "12389", + "inputChannelId": "C0XXXFKLXXX", + "integrationId": 111, + "sentryAppId": null, + "targetIdentifier": "#alert-rule-alerts", + "targetType": "specific", + "type": "slack" + } + ], + "alertRuleId": "12345", + "alertThreshold": 10000, + "dateCreated": "2022-04-15T15:06:01.079598Z", + "id": "56789", + "label": "critical", + "resolveThreshold": 0, + "thresholdType": 0 + } + ], + "projects": [ + "pump-station" + ], + "owner": "pump-station:12345", + "dateCreated": "2022-04-15T15:06:01.05618Z" + } + } + `) + }) + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/pump-station/alert-rules/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertPostJSONValue(t, map[string]interface{}{ + "id": "12345", + "name": "pump-station-alert", + "environment": "production", + "dataset": "transactions", + "eventTypes": []string{"transaction"}, + "query": "http.url:http://service/unreadmessages", + "aggregate": "p50(transaction.duration)", + "timeWindow": 10, + "thresholdType": 0, + "resolveThreshold": 0, + "triggers": []map[string]interface{}{ + { + "actions": []map[string]interface{}{ + { + "alertRuleTriggerId": "56789", + "dateCreated": "2022-04-15T15:06:01.087054Z", + "desc": "Send a Slack notification to #alert-rule-alerts", + "id": "12389", + "inputChannelId": "C0XXXFKLXXX", + "integrationId": 111, + "sentryAppId": nil, + "targetIdentifier": "#alert-rule-alerts", + "targetType": "specific", + "type": "slack", + }, + }, + "alertRuleId": "12345", + "alertThreshold": 10000, + "dateCreated": "2022-04-15T15:06:01.079598Z", + "id": "56789", + "label": "critical", + "resolveThreshold": 0, + "thresholdType": 0, + }, + }, + "projects": []string{"pump-station"}, + "owner": "pump-station:12345", + "dateCreated": "2022-04-15T15:06:01.05618Z", + }, r) + + w.WriteHeader(http.StatusAccepted) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"uuid": "fakeuuid"}`) + }) + + params := &MetricAlert{ + Name: String("pump-station-alert"), + Environment: String("production"), + DataSet: String("transactions"), + Query: String("http.url:http://service/unreadmessages"), + Aggregate: String("p50(transaction.duration)"), + TimeWindow: Float64(10.0), + ThresholdType: Int(0), + ResolveThreshold: Float64(0), + Triggers: []*MetricAlertTrigger{ + { + ID: String("56789"), + AlertRuleID: String("12345"), + Label: String("critical"), + ThresholdType: Int(0), + AlertThreshold: Float64(55501.0), + ResolveThreshold: Float64(100.0), + DateCreated: Time(mustParseTime("2022-04-15T15:06:01.079598Z")), + Actions: []*MetricAlertTriggerAction{ + { + ID: String("12389"), + AlertRuleTriggerID: String("56789"), + Type: String("slack"), + TargetType: String("specific"), + TargetIdentifier: String("#alert-rule-alerts"), + InputChannelID: String("C0XXXFKLXXX"), + IntegrationID: Int(123), + DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), + Description: String("Send a Slack notification to #alert-rule-alerts"), + }, + }, + }, + }, + Projects: []string{"pump-station"}, + Owner: String("pump-station:12345"), + } + ctx := context.Background() + alertRule, _, err := client.MetricAlerts.Create(ctx, "the-interstellar-jurisdiction", "pump-station", params) + require.NoError(t, err) + + expected := &MetricAlert{ + ID: String("12345"), + Name: String("pump-station-alert"), + Environment: String("production"), + DataSet: String("transactions"), + EventTypes: []string{"transaction"}, + Query: String("http.url:http://service/unreadmessages"), + Aggregate: String("p50(transaction.duration)"), + ThresholdType: Int(0), + ResolveThreshold: Float64(0), + TimeWindow: Float64(10.0), + Triggers: []*MetricAlertTrigger{ + { + ID: String("56789"), + AlertRuleID: String("12345"), + Label: String("critical"), + ThresholdType: Int(0), + AlertThreshold: Float64(10000.0), + ResolveThreshold: Float64(0.0), + DateCreated: Time(mustParseTime("2022-04-15T15:06:01.079598Z")), + Actions: []*MetricAlertTriggerAction{ + { + ID: String("12389"), + AlertRuleTriggerID: String("56789"), + Type: String("slack"), + TargetType: String("specific"), + TargetIdentifier: String("#alert-rule-alerts"), + InputChannelID: String("C0XXXFKLXXX"), + IntegrationID: Int(111), + DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), + Description: String("Send a Slack notification to #alert-rule-alerts"), + }, + }, + }, + }, + Projects: []string{"pump-station"}, + Owner: String("pump-station:12345"), + DateCreated: Time(mustParseTime("2022-04-15T15:06:01.05618Z")), + } + + require.Equal(t, expected, alertRule) +} + func TestMetricAlertService_Create(t *testing.T) { client, mux, _, teardown := setup() defer teardown() From 966077d83ab1a4d5c6bff632eab00a073bb7ff76 Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Fri, 20 Jan 2023 11:09:43 +0100 Subject: [PATCH 12/41] Add support for creating new release deployments (#74) The release deployments in Sentry only support create and list so these are the only two endpoints added --- sentry/release_deployment.go | 80 ++++++++++++++++++++++++++++++++++++ sentry/sentry.go | 2 + 2 files changed, 82 insertions(+) create mode 100644 sentry/release_deployment.go diff --git a/sentry/release_deployment.go b/sentry/release_deployment.go new file mode 100644 index 0000000..104230f --- /dev/null +++ b/sentry/release_deployment.go @@ -0,0 +1,80 @@ +package sentry + +import ( + "context" + "fmt" + "time" +) + +type ReleaseDeploymentsService service + +type ReleaseDeployment struct { + ID string `json:"id"` + Name *string `json:"name,omitempty"` + Environment string `json:"environment,omitempty"` + URL *string `json:"url,omitempty"` + Projects []string `json:"projects,omitempty"` + DateStarted *time.Time `json:"dateStarted,omitempty"` + DateFinished *time.Time `json:"dateFinished,omitempty"` +} + +// Get a Release Deploy for a project. +func (s *ReleaseDeploymentsService) Get(ctx context.Context, organizationSlug string, version string, deployID string) (*ReleaseDeployment, *Response, error) { + + lastCursor := "" + + // Search for the deployment ID by using the list endpoint. When we have + // found the first match return immediately + for { + params := ListCursorParams{ + Cursor: lastCursor, + } + + u := fmt.Sprintf("0/organizations/%v/releases/%s/deploys/", organizationSlug, version) + u, err := addQuery(u, params) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + deployments := new([]ReleaseDeployment) + resp, err := s.client.Do(ctx, req, deployments) + if err != nil { + return nil, resp, err + } + + for i := range *deployments { + d := (*deployments)[i] + if d.ID == deployID { + return &d, resp, nil + } + } + + // No matches in the current page and no further pages to check + if resp.Cursor == "" { + return nil, resp, nil + } + lastCursor = resp.Cursor + } +} + +// Create a new Release Deploy to a project. +func (s *ReleaseDeploymentsService) Create(ctx context.Context, organizationSlug string, version string, params *ReleaseDeployment) (*ReleaseDeployment, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/releases/%s/deploys/", organizationSlug, version) + req, err := s.client.NewRequest("POST", u, params) + if err != nil { + return nil, nil, err + } + + deploy := new(ReleaseDeployment) + resp, err := s.client.Do(ctx, req, deploy) + if err != nil { + return nil, resp, err + } + + return deploy, resp, nil +} diff --git a/sentry/sentry.go b/sentry/sentry.go index 1f39421..e4dff50 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -61,6 +61,7 @@ type Client struct { ProjectPlugins *ProjectPluginsService Projects *ProjectsService ProjectFilter *ProjectFilterService + ReleaseDeployments *ReleaseDeploymentsService Teams *TeamsService } @@ -96,6 +97,7 @@ func NewClient(httpClient *http.Client) *Client { c.ProjectOwnerships = (*ProjectOwnershipsService)(&c.common) c.ProjectPlugins = (*ProjectPluginsService)(&c.common) c.Projects = (*ProjectsService)(&c.common) + c.ReleaseDeployments = (*ReleaseDeploymentsService)(&c.common) c.Teams = (*TeamsService)(&c.common) return c } From 0932fda5fef41f81da3fd413547728a35ad40c36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Feb 2023 18:08:10 +0000 Subject: [PATCH 13/41] fix(deps): update module github.com/stretchr/testify to v1.8.2 (#75) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 1c19955..3a2f0d0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/google/go-querystring v1.1.0 github.com/peterhellberg/link v1.2.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 ) require ( diff --git a/go.sum b/go.sum index 2d481db..d1de032 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 3850087c9c37b611f74607a5e2a168b805b699cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 19:08:06 +0000 Subject: [PATCH 14/41] chore(deps): update actions/setup-go action to v4 (#77) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/go.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 1af295c..dfdd582 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} From 97df7c9e8f971f0567dbd262562b655ca8684655 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 22 Mar 2023 06:27:14 +0900 Subject: [PATCH 15/41] add fingerprinting rules (#76) --- sentry/projects.go | 2 ++ sentry/projects_test.go | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/sentry/projects.go b/sentry/projects.go index afdc1df..c2a688e 100644 --- a/sentry/projects.go +++ b/sentry/projects.go @@ -40,6 +40,7 @@ type Project struct { ResolveAge int `json:"resolveAge"` DataScrubber bool `json:"dataScrubber"` DataScrubberDefaults bool `json:"dataScrubberDefaults"` + FingerprintingRules string `json:"fingerprintingRules"` GroupingEnhancements string `json:"groupingEnhancements"` SafeFields []string `json:"safeFields"` SensitiveFields []string `json:"sensitiveFields"` @@ -159,6 +160,7 @@ type UpdateProjectParams struct { ResolveAge *int `json:"resolveAge,omitempty"` Options map[string]interface{} `json:"options,omitempty"` AllowedDomains []string `json:"allowedDomains,omitempty"` + FingerprintingRules string `json:"fingerprintingRules,omitempty"` GroupingEnhancements string `json:"groupingEnhancements,omitempty"` } diff --git a/sentry/projects_test.go b/sentry/projects_test.go index d938df4..f5761e6 100644 --- a/sentry/projects_test.go +++ b/sentry/projects_test.go @@ -134,6 +134,7 @@ func TestProjectsService_List(t *testing.T) { }, "platform": null, "slug": "pump-station", + "fingerprintingRules": "fingerprinting rule", "groupingEnhancements": "pump station grouping enhancement rule", "status": "active" } @@ -216,6 +217,7 @@ func TestProjectsService_List(t *testing.T) { Type: "letter_avatar", }, Organization: expectedOrganization, + FingerprintingRules: "fingerprinting rule", GroupingEnhancements: "pump station grouping enhancement rule", }, } @@ -361,6 +363,7 @@ func TestProjectsService_Get(t *testing.T) { "name": "Powerful Abolitionist", "slug": "powerful-abolitionist" }], + "fingerprintingRules": "fingerprinting rule", "groupingEnhancements": "pump-station grouping enhancement rule", "verifySSL": false }`) @@ -417,6 +420,7 @@ func TestProjectsService_Get(t *testing.T) { AllowedDomains: []string{"*"}, DataScrubber: true, DataScrubberDefaults: true, + FingerprintingRules: "fingerprinting rule", GroupingEnhancements: "pump-station grouping enhancement rule", SafeFields: []string{}, SensitiveFields: []string{}, @@ -538,6 +542,7 @@ func TestProjectsService_Update(t *testing.T) { "callSignReviewed": false, "id": "5", "subjectTemplate": "[$project] ${tag:level}: $title", + "fingerprintingRules": "fingerprinting rule", "groupingEnhancements": "Plane Proxy grouping enhancement rule", "name": "Plane Proxy" }`) @@ -575,6 +580,7 @@ func TestProjectsService_Update(t *testing.T) { DigestsMaxDelay: 1800, ResolveAge: 720, SubjectTemplate: "[$project] ${tag:level}: $title", + FingerprintingRules: "fingerprinting rule", GroupingEnhancements: "Plane Proxy grouping enhancement rule", } assert.Equal(t, expected, project) From 7a0b01c5de4d94c375de254bf7f464e82e344224 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Fri, 14 Apr 2023 12:40:12 -0500 Subject: [PATCH 16/41] Add methods to get and update organization integration configData (#80) * Extend OrganizationIntegrationsService to support GET org integration details endpoint * Support variable shape configData for org integrations * Implement OrganizationIntegrationsService UpdateConfig --- sentry/organization_integrations.go | 49 ++++++-- sentry/organization_integrations_test.go | 142 ++++++++++++++++++++++- 2 files changed, 182 insertions(+), 9 deletions(-) diff --git a/sentry/organization_integrations.go b/sentry/organization_integrations.go index 6ddfbe5..687296e 100644 --- a/sentry/organization_integrations.go +++ b/sentry/organization_integrations.go @@ -16,29 +16,34 @@ type OrganizationIntegrationProvider struct { Features []string `json:"features"` } +// IntegrationConfigData for defining integration-specific configuration data. +type IntegrationConfigData map[string]interface{} + // OrganizationIntegration represents an integration added for the organization. // https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/integration.py#L93 type OrganizationIntegration struct { // https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/integration.py#L35 ID string `json:"id"` Name string `json:"name"` - Icon string `json:"icon"` + Icon *string `json:"icon"` DomainName string `json:"domainName"` - AccountType string `json:"accountType"` + AccountType *string `json:"accountType"` Scopes []string `json:"scopes"` Status string `json:"status"` Provider OrganizationIntegrationProvider `json:"provider"` // https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/serializers/models/integration.py#L138 - ExternalId string `json:"externalId"` - OrganizationId int `json:"organizationId"` - OrganizationIntegrationStatus string `json:"organizationIntegrationStatus"` - GracePeriodEnd *time.Time `json:"gracePeriodEnd"` + ConfigData *IntegrationConfigData `json:"configData"` + ExternalId string `json:"externalId"` + OrganizationId int `json:"organizationId"` + OrganizationIntegrationStatus string `json:"organizationIntegrationStatus"` + GracePeriodEnd *time.Time `json:"gracePeriodEnd"` } // OrganizationIntegrationsService provides methods for accessing Sentry organization integrations API endpoints. -// Paths: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/urls.py#L1236-L1240 +// Paths: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/urls.py#L1236-L1245 // Endpoints: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/integrations/organization_integrations/index.py +// Endpoints: https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/integrations/organization_integrations/details.py type OrganizationIntegrationsService service type ListOrganizationIntegrationsParams struct { @@ -66,3 +71,33 @@ func (s *OrganizationIntegrationsService) List(ctx context.Context, organization } return integrations, resp, nil } + +// Get organization integration details. +func (s *OrganizationIntegrationsService) Get(ctx context.Context, organizationSlug string, integrationID string) (*OrganizationIntegration, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/integrations/%v/", organizationSlug, integrationID) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + integration := new(OrganizationIntegration) + resp, err := s.client.Do(ctx, req, integration) + if err != nil { + return nil, resp, err + } + return integration, resp, nil +} + +type UpdateConfigOrganizationIntegrationsParams = IntegrationConfigData + +// UpdateConfig - update configData for organization integration. +// https://github.com/getsentry/sentry/blob/22.7.0/src/sentry/api/endpoints/integrations/organization_integrations/details.py#L94-L102 +func (s *OrganizationIntegrationsService) UpdateConfig(ctx context.Context, organizationSlug string, integrationID string, params *UpdateConfigOrganizationIntegrationsParams) (*Response, error) { + u := fmt.Sprintf("0/organizations/%v/integrations/%v/", organizationSlug, integrationID) + req, err := s.client.NewRequest("POST", u, params) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/sentry/organization_integrations_test.go b/sentry/organization_integrations_test.go index 028afcd..63ab695 100644 --- a/sentry/organization_integrations_test.go +++ b/sentry/organization_integrations_test.go @@ -2,6 +2,7 @@ package sentry import ( "context" + "encoding/json" "fmt" "net/http" "testing" @@ -62,9 +63,9 @@ func TestOrganizationIntegrationsService_List(t *testing.T) { { ID: "123456", Name: "octocat", - Icon: "https://avatars.githubusercontent.com/u/583231?v=4", + Icon: String("https://avatars.githubusercontent.com/u/583231?v=4"), DomainName: "github.com/octocat", - AccountType: "Organization", + AccountType: String("Organization"), Scopes: []string{"read", "write"}, Status: "active", Provider: OrganizationIntegrationProvider{ @@ -80,6 +81,7 @@ func TestOrganizationIntegrationsService_List(t *testing.T) { "stacktrace-link", }, }, + ConfigData: &IntegrationConfigData{}, ExternalId: "87654321", OrganizationId: 2, OrganizationIntegrationStatus: "active", @@ -88,3 +90,139 @@ func TestOrganizationIntegrationsService_List(t *testing.T) { } assert.Equal(t, expected, integrations) } + +func TestOrganizationIntegrationsService_Get(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/integrations/456789/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "GET", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "id": "456789", + "name": "Interstellar PagerDuty", + "icon": null, + "domainName": "the-interstellar-jurisdiction", + "accountType": null, + "scopes": null, + "status": "active", + "provider": { + "key": "pagerduty", + "slug": "pagerduty", + "name": "PagerDuty", + "canAdd": true, + "canDisable": false, + "features": [ + "alert-rule", + "incident-management" + ], + "aspects": { + "alerts": [ + { + "type": "info", + "text": "The PagerDuty integration adds a new Alert Rule action to all projects. To enable automatic notifications sent to PagerDuty you must create a rule using the PagerDuty action in your project settings." + } + ] + } + }, + "configOrganization": [ + { + "name": "service_table", + "type": "table", + "label": "PagerDuty services with the Sentry integration enabled", + "help": "If services need to be updated, deleted, or added manually please do so here. Alert rules will need to be individually updated for any additions or deletions of services.", + "addButtonText": "", + "columnLabels": { + "service": "Service", + "integration_key": "Integration Key" + }, + "columnKeys": [ + "service", + "integration_key" + ], + "confirmDeleteMessage": "Any alert rules associated with this service will stop working. The rules will still exist but will show a removed service." + } + ], + "configData": { + "service_table": [ + { + "service": "testing123", + "integration_key": "abc123xyz", + "id": 22222 + } + ] + }, + "externalId": "999999", + "organizationId": 2, + "organizationIntegrationStatus": "active", + "gracePeriodEnd": null + }`) + }) + + ctx := context.Background() + integration, _, err := client.OrganizationIntegrations.Get(ctx, "the-interstellar-jurisdiction", "456789") + assert.NoError(t, err) + expected := OrganizationIntegration{ + ID: "456789", + Name: "Interstellar PagerDuty", + Icon: nil, + DomainName: "the-interstellar-jurisdiction", + AccountType: nil, + Scopes: nil, + Status: "active", + Provider: OrganizationIntegrationProvider{ + Key: "pagerduty", + Slug: "pagerduty", + Name: "PagerDuty", + CanAdd: true, + CanDisable: false, + Features: []string{ + "alert-rule", + "incident-management", + }, + }, + ConfigData: &IntegrationConfigData{ + "service_table": []interface{}{ + map[string]interface{}{ + "service": "testing123", + "integration_key": "abc123xyz", + "id": json.Number("22222"), + }, + }, + }, + ExternalId: "999999", + OrganizationId: 2, + OrganizationIntegrationStatus: "active", + GracePeriodEnd: nil, + } + assert.Equal(t, &expected, integration) +} + +func TestOrganizationIntegrationsService_UpdateConfig(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/the-interstellar-jurisdiction/integrations/456789/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + w.Header().Set("Content-Type", "application/json") + }) + + updateConfigOrganizationIntegrationsParams := UpdateConfigOrganizationIntegrationsParams{ + "service_table": []interface{}{ + map[string]interface{}{ + "service": "testing123", + "integration_key": "abc123xyz", + "id": json.Number("22222"), + }, + map[string]interface{}{ + "service": "testing456", + "integration_key": "efg456lmn", + "id": "", + }, + }, + } + ctx := context.Background() + resp, err := client.OrganizationIntegrations.UpdateConfig(ctx, "the-interstellar-jurisdiction", "456789", &updateConfigOrganizationIntegrationsParams) + assert.NoError(t, err) + assert.Equal(t, int64(0), resp.ContentLength) +} From 861f4c0509953c22100db4e1e6051266c34d0359 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:02:56 +0000 Subject: [PATCH 17/41] chore(deps): update actions/checkout action to v4 (#82) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/go.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index dfdd582..9a3bd47 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -14,7 +14,7 @@ jobs: - "1.17" - "1.18" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 From adc51b156720d66b78be6e4c229925fceb648d31 Mon Sep 17 00:00:00 2001 From: Mike Errington Date: Thu, 30 Nov 2023 15:11:27 -0500 Subject: [PATCH 18/41] fix: add support for default_rules to create project (#85) --- sentry/projects.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sentry/projects.go b/sentry/projects.go index c2a688e..c9465f7 100644 --- a/sentry/projects.go +++ b/sentry/projects.go @@ -128,9 +128,10 @@ func (s *ProjectsService) Get(ctx context.Context, organizationSlug string, slug // CreateProjectParams are the parameters for ProjectService.Create. type CreateProjectParams struct { - Name string `json:"name,omitempty"` - Slug string `json:"slug,omitempty"` - Platform string `json:"platform,omitempty"` + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Platform string `json:"platform,omitempty"` + DefaultRules *bool `json:"default_rules,omitempty"` } // Create a new project bound to a team. From 5beaa3fed3876d93e094e8ce04a6b1f8e7b81a8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:43:59 +0000 Subject: [PATCH 19/41] chore(deps): update actions/setup-go action to v5 (#86) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/go.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 9a3bd47..c41f41e 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} From c7b30d00cf60c8c606e8c6b89db29aeb6dc4cc3a Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Wed, 6 Dec 2023 23:44:48 +0000 Subject: [PATCH 20/41] feat: add/remove organization member to/from a team (#87) --- sentry/sentry.go | 2 + sentry/team_members.go | 55 +++++++++++++ sentry/team_members_test.go | 154 ++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 sentry/team_members.go create mode 100644 sentry/team_members_test.go diff --git a/sentry/sentry.go b/sentry/sentry.go index e4dff50..9be1b68 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -63,6 +63,7 @@ type Client struct { ProjectFilter *ProjectFilterService ReleaseDeployments *ReleaseDeploymentsService Teams *TeamsService + TeamMembers *TeamMembersService } type service struct { @@ -99,6 +100,7 @@ func NewClient(httpClient *http.Client) *Client { c.Projects = (*ProjectsService)(&c.common) c.ReleaseDeployments = (*ReleaseDeploymentsService)(&c.common) c.Teams = (*TeamsService)(&c.common) + c.TeamMembers = (*TeamMembersService)(&c.common) return c } diff --git a/sentry/team_members.go b/sentry/team_members.go new file mode 100644 index 0000000..9b5148e --- /dev/null +++ b/sentry/team_members.go @@ -0,0 +1,55 @@ +package sentry + +import ( + "context" + "fmt" + "time" +) + +type TeamMember struct { + ID *string `json:"id"` + Slug *string `json:"slug"` + Name *string `json:"name"` + DateCreated *time.Time `json:"dateCreated"` + IsMember *bool `json:"isMember"` + TeamRole *string `json:"teamRole"` + Flags map[string]bool `json:"flags"` + Access []string `json:"access"` + HasAccess *bool `json:"hasAccess"` + IsPending *bool `json:"isPending"` + MemberCount *int `json:"memberCount"` + Avatar *Avatar `json:"avatar"` +} + +// TeamMember provides methods for accessing Sentry team member API endpoints. +type TeamMembersService service + +func (s *TeamMembersService) Create(ctx context.Context, organizationSlug string, memberId string, teamSlug string) (*TeamMember, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberId, teamSlug) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, nil, err + } + + member := new(TeamMember) + resp, err := s.client.Do(ctx, req, member) + if err != nil { + return nil, resp, err + } + return member, resp, nil +} + +func (s *TeamMembersService) Delete(ctx context.Context, organizationSlug string, memberId string, teamSlug string) (*TeamMember, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberId, teamSlug) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, nil, err + } + + member := new(TeamMember) + resp, err := s.client.Do(ctx, req, member) + if err != nil { + return nil, resp, err + } + return member, resp, nil +} diff --git a/sentry/team_members_test.go b/sentry/team_members_test.go new file mode 100644 index 0000000..08186c7 --- /dev/null +++ b/sentry/team_members_test.go @@ -0,0 +1,154 @@ +package sentry + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTeamMembersService_Create(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/organization_slug/members/member_id/teams/team_slug/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "id": "4502349234123", + "slug": "ancient-gabelers", + "name": "Ancient Gabelers", + "dateCreated": "2023-05-31T19:47:53.621181Z", + "isMember": true, + "teamRole": "contributor", + "flags": { + "idp:provisioned": false + }, + "access": [ + "alerts:read", + "event:write", + "project:read", + "team:read", + "member:read", + "project:releases", + "event:read", + "org:read" + ], + "hasAccess": true, + "isPending": false, + "memberCount": 3, + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + } + }`) + }) + + ctx := context.Background() + team, _, err := client.TeamMembers.Create(ctx, "organization_slug", "member_id", "team_slug") + assert.NoError(t, err) + + expected := &TeamMember{ + ID: String("4502349234123"), + Slug: String("ancient-gabelers"), + Name: String("Ancient Gabelers"), + DateCreated: Time(mustParseTime("2023-05-31T19:47:53.621181Z")), + IsMember: Bool(true), + TeamRole: String("contributor"), + Flags: map[string]bool{ + "idp:provisioned": false, + }, + Access: []string{ + "alerts:read", + "event:write", + "project:read", + "team:read", + "member:read", + "project:releases", + "event:read", + "org:read", + }, + HasAccess: Bool(true), + IsPending: Bool(false), + MemberCount: Int(3), + Avatar: &Avatar{ + UUID: nil, + Type: "letter_avatar", + }, + } + assert.Equal(t, expected, team) +} + +func TestTeamMembersService_Delete(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/organization_slug/members/member_id/teams/team_slug/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "DELETE", r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "id": "4502349234123", + "slug": "ancient-gabelers", + "name": "Ancient Gabelers", + "dateCreated": "2023-05-31T19:47:53.621181Z", + "isMember": false, + "teamRole": null, + "flags": { + "idp:provisioned": false + }, + "access": [ + "alerts:read", + "event:write", + "project:read", + "team:read", + "member:read", + "project:releases", + "event:read", + "org:read" + ], + "hasAccess": true, + "isPending": false, + "memberCount": 3, + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + } + }`) + }) + + ctx := context.Background() + team, _, err := client.TeamMembers.Delete(ctx, "organization_slug", "member_id", "team_slug") + assert.NoError(t, err) + + expected := &TeamMember{ + ID: String("4502349234123"), + Slug: String("ancient-gabelers"), + Name: String("Ancient Gabelers"), + DateCreated: Time(mustParseTime("2023-05-31T19:47:53.621181Z")), + IsMember: Bool(false), + TeamRole: nil, + Flags: map[string]bool{ + "idp:provisioned": false, + }, + Access: []string{ + "alerts:read", + "event:write", + "project:read", + "team:read", + "member:read", + "project:releases", + "event:read", + "org:read", + }, + HasAccess: Bool(true), + IsPending: Bool(false), + MemberCount: Int(3), + Avatar: &Avatar{ + UUID: nil, + Type: "letter_avatar", + }, + } + assert.Equal(t, expected, team) +} From 24b9e99ec73cab041b509fdef38e0eafd1eaa788 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:10:42 +0000 Subject: [PATCH 21/41] fix(deps): update module github.com/stretchr/testify to v1.8.4 (#81) * fix(deps): update module github.com/stretchr/testify to v1.8.4 * Update latest Golang versions used in tests --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jian Yuan Lee --- .github/workflows/go.yaml | 26 ++++++++++++-------------- go.mod | 2 +- go.sum | 2 ++ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index c41f41e..9692341 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -3,26 +3,24 @@ name: Go on: push jobs: - build: runs-on: ubuntu-latest strategy: matrix: go: - - "1.15" - - "1.16" - - "1.17" - - "1.18" + - "1.19" + - "1.20" + - "1.21" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go }} + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: make test + - name: Test + run: make test diff --git a/go.mod b/go.mod index 3a2f0d0..46ec5f7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/google/go-querystring v1.1.0 github.com/peterhellberg/link v1.2.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 ) require ( diff --git a/go.sum b/go.sum index d1de032..148666f 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From d6f394e83cc3690d0aecd290af06ec44e4ea0d53 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Thu, 7 Dec 2023 00:14:07 +0000 Subject: [PATCH 22/41] ref: fix casing in memberID --- sentry/team_members.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry/team_members.go b/sentry/team_members.go index 9b5148e..711801c 100644 --- a/sentry/team_members.go +++ b/sentry/team_members.go @@ -24,8 +24,8 @@ type TeamMember struct { // TeamMember provides methods for accessing Sentry team member API endpoints. type TeamMembersService service -func (s *TeamMembersService) Create(ctx context.Context, organizationSlug string, memberId string, teamSlug string) (*TeamMember, *Response, error) { - u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberId, teamSlug) +func (s *TeamMembersService) Create(ctx context.Context, organizationSlug string, memberID string, teamSlug string) (*TeamMember, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberID, teamSlug) req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, nil, err @@ -39,8 +39,8 @@ func (s *TeamMembersService) Create(ctx context.Context, organizationSlug string return member, resp, nil } -func (s *TeamMembersService) Delete(ctx context.Context, organizationSlug string, memberId string, teamSlug string) (*TeamMember, *Response, error) { - u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberId, teamSlug) +func (s *TeamMembersService) Delete(ctx context.Context, organizationSlug string, memberID string, teamSlug string) (*TeamMember, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberID, teamSlug) req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return nil, nil, err From d91bffadf30f63e2ff22959e82300d80c6208fe4 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Thu, 7 Dec 2023 00:43:29 +0000 Subject: [PATCH 23/41] feat: Add/delete/change roles to organization members- Including Team level roles (#88) --- sentry/organization_members.go | 49 +++-- sentry/organization_members_test.go | 317 +++++++++++++++++++++++++--- 2 files changed, 322 insertions(+), 44 deletions(-) diff --git a/sentry/organization_members.go b/sentry/organization_members.go index e954788..3275472 100644 --- a/sentry/organization_members.go +++ b/sentry/organization_members.go @@ -9,27 +9,31 @@ import ( // OrganizationMember represents a User's membership to the organization. // https://github.com/getsentry/sentry/blob/22.5.0/src/sentry/api/serializers/models/organization_member/response.py#L57-L69 type OrganizationMember struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - User User `json:"user"` - Role string `json:"role"` - RoleName string `json:"roleName"` - Pending bool `json:"pending"` - Expired bool `json:"expired"` - Flags map[string]bool `json:"flags"` - DateCreated time.Time `json:"dateCreated"` - InviteStatus string `json:"inviteStatus"` - InviterName *string `json:"inviterName"` - Teams []string `json:"teams"` + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + User User `json:"user"` + Role string `json:"role"` // Deprecated + RoleName string `json:"roleName"` // Deprecated + OrganizationRole string `json:"orgRole"` + Pending bool `json:"pending"` + Expired bool `json:"expired"` + Flags map[string]bool `json:"flags"` + DateCreated time.Time `json:"dateCreated"` + InviteStatus string `json:"inviteStatus"` + InviterName *string `json:"inviterName"` + Teams []string `json:"teams"` + TeamRoles []TeamRole `json:"teamRoles"` } const ( - RoleMember string = "member" - RoleBilling string = "billing" - RoleAdmin string = "admin" - RoleOwner string = "owner" - RoleManager string = "manager" + OrganizationRoleBilling string = "billing" + OrganizationRoleMember string = "member" + OrganizationRoleManager string = "manager" + OrganizationRoleOwner string = "owner" + + TeamRoleContributor string = "contributor" + TeamRoleAdmin string = "admin" ) // OrganizationMembersService provides methods for accessing Sentry membership API endpoints. @@ -92,9 +96,14 @@ func (s *OrganizationMembersService) Create(ctx context.Context, organizationSlu return member, resp, nil } +type TeamRole struct { + TeamSlug string `json:"teamSlug"` + Role string `json:"role"` +} + type UpdateOrganizationMemberParams struct { - Role string `json:"role"` - Teams []string `json:"teams,omitempty"` + OrganizationRole string `json:"role"` + TeamRoles []TeamRole `json:"teamRoles"` } func (s *OrganizationMembersService) Update(ctx context.Context, organizationSlug string, memberID string, params *UpdateOrganizationMemberParams) (*OrganizationMember, *Response, error) { diff --git a/sentry/organization_members_test.go b/sentry/organization_members_test.go index b4bc7fe..b2e6932 100644 --- a/sentry/organization_members_test.go +++ b/sentry/organization_members_test.go @@ -273,7 +273,7 @@ func TestOrganizationMembersService_Create(t *testing.T) { createOrganizationMemberParams := CreateOrganizationMemberParams{ Email: "test@example.com", - Role: RoleMember, + Role: OrganizationRoleMember, } ctx := context.Background() member, _, err := client.OrganizationMembers.Create(ctx, "the-interstellar-jurisdiction", &createOrganizationMemberParams) @@ -311,52 +311,321 @@ func TestOrganizationMembersService_Update(t *testing.T) { assertMethod(t, "PUT", r) w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, `{ - "id": "1", - "email": "test@example.com", - "name": "test@example.com", - "user": null, - "role": "manager", - "roleName": "Manager", - "pending": true, + "id": "57377908164", + "email": "sirpenguin@antarcticarocks.com", + "name": "Sir Penguin", + "user": { + "id": "280094367316", + "name": "Sir Penguin", + "username": "sirpenguin@antarcticarocks.com", + "email": "sirpenguin@antarcticarocks.com", + "avatarUrl": "https://secure.gravatar.com/avatar/16aeb26c5fdba335c7078e9e9ddb5149?s=32&d=mm", + "isActive": true, + "hasPasswordAuth": true, + "isManaged": false, + "dateJoined": "2021-07-06T21:13:58.375239Z", + "lastLogin": "2021-08-02T18:25:00.051182Z", + "has2fa": false, + "lastActive": "2021-08-02T21:32:18.836829Z", + "isSuperuser": false, + "isStaff": false, + "experiments": {}, + "emails": [ + { + "id": "2153450836", + "email": "sirpenguin@antarcticarocks.com", + "is_verified": true + } + ], + "avatar": { + "avatarType": "letter_avatar", + "avatarUuid": null + }, + "authenticators": [], + "canReset2fa": true + }, + "role": "member", + "orgRole": "member", + "roleName": "Member", + "pending": false, "expired": false, "flags": { + "idp:provisioned": false, + "idp:role-restricted": false, "sso:linked": false, "sso:invalid": false, - "member-limit:restricted": false + "member-limit:restricted": false, + "partnership:restricted": false }, - "teams": [], - "dateCreated": "2020-01-01T00:00:00.000000Z", + "dateCreated": "2021-07-06T21:13:01.120263Z", "inviteStatus": "approved", - "inviterName": "John Doe" + "inviterName": "maininviter@antarcticarocks.com", + "teams": [ + "cool-team", + "ancient-gabelers" + ], + "teamRoles": [ + { + "teamSlug": "ancient-gabelers", + "role": "admin" + }, + { + "teamSlug": "powerful-abolitionist", + "role": "contributor" + } + ], + "invite_link": null, + "isOnlyOwner": false, + "orgRoleList": [ + { + "id": "billing", + "name": "Billing", + "desc": "Can manage subscription and billing details.", + "scopes": [ + "org:billing" + ], + "allowed": true, + "isAllowed": true, + "isRetired": false, + "is_global": false, + "isGlobal": false, + "minimumTeamRole": "contributor" + }, + { + "id": "member", + "name": "Member", + "desc": "Members can view and act on events, as well as view most other data within the organization.", + "scopes": [ + "team:read", + "project:releases", + "org:read", + "event:read", + "alerts:write", + "member:read", + "alerts:read", + "event:admin", + "project:read", + "event:write" + ], + "allowed": true, + "isAllowed": true, + "isRetired": false, + "is_global": false, + "isGlobal": false, + "minimumTeamRole": "contributor" + }, + { + "id": "admin", + "name": "Admin", + "desc": "Admin privileges on any teams of which they're a member. They can create new teams and projects, as well as remove teams and projects on which they already hold membership (or all teams, if open membership is enabled). Additionally, they can manage memberships of teams that they are members of. They cannot invite members to the organization.", + "scopes": [ + "team:admin", + "org:integrations", + "project:admin", + "team:read", + "project:releases", + "org:read", + "team:write", + "event:read", + "alerts:write", + "member:read", + "alerts:read", + "event:admin", + "project:read", + "event:write", + "project:write" + ], + "allowed": true, + "isAllowed": true, + "isRetired": true, + "is_global": false, + "isGlobal": false, + "minimumTeamRole": "admin" + }, + { + "id": "manager", + "name": "Manager", + "desc": "Gains admin access on all teams as well as the ability to add and remove members.", + "scopes": [ + "team:admin", + "org:integrations", + "project:releases", + "team:write", + "member:read", + "org:write", + "project:write", + "project:admin", + "team:read", + "org:read", + "event:read", + "member:write", + "alerts:write", + "alerts:read", + "event:admin", + "project:read", + "event:write", + "member:admin" + ], + "allowed": true, + "isAllowed": true, + "isRetired": false, + "is_global": true, + "isGlobal": true, + "minimumTeamRole": "admin" + }, + { + "id": "owner", + "name": "Owner", + "desc": "Unrestricted access to the organization, its data, and its settings. Can add, modify, and delete projects and members, as well as make billing and plan changes.", + "scopes": [ + "team:admin", + "org:integrations", + "project:releases", + "org:admin", + "team:write", + "member:read", + "org:write", + "project:write", + "project:admin", + "team:read", + "org:read", + "event:read", + "member:write", + "alerts:write", + "org:billing", + "alerts:read", + "event:admin", + "project:read", + "event:write", + "member:admin" + ], + "allowed": true, + "isAllowed": true, + "isRetired": false, + "is_global": true, + "isGlobal": true, + "minimumTeamRole": "admin" + } + ], + "teamRoleList": [ + { + "id": "contributor", + "name": "Contributor", + "desc": "Contributors can view and act on events, as well as view most other data within the team's projects.", + "scopes": [ + "team:read", + "project:releases", + "org:read", + "event:read", + "member:read", + "alerts:read", + "project:read", + "event:write" + ], + "allowed": false, + "isAllowed": false, + "isRetired": false, + "isMinimumRoleFor": null + }, + { + "id": "admin", + "name": "Team Admin", + "desc": "Admin privileges on the team. They can create and remove projects, and can manage the team's memberships. They cannot invite members to the organization.", + "scopes": [ + "team:admin", + "org:integrations", + "project:admin", + "team:read", + "project:releases", + "org:read", + "team:write", + "event:read", + "alerts:write", + "member:read", + "alerts:read", + "event:admin", + "project:read", + "event:write", + "project:write" + ], + "allowed": false, + "isAllowed": false, + "isRetired": false, + "isMinimumRoleFor": "admin" + } + ] }`) }) updateOrganizationMemberParams := UpdateOrganizationMemberParams{ - Role: RoleMember, + OrganizationRole: OrganizationRoleMember, } ctx := context.Background() member, _, err := client.OrganizationMembers.Update(ctx, "the-interstellar-jurisdiction", "1", &updateOrganizationMemberParams) assert.NoError(t, err) - inviterName := "John Doe" + inviterName := "maininviter@antarcticarocks.com" expected := OrganizationMember{ - ID: "1", - Email: "test@example.com", - Name: "test@example.com", - User: User{}, - Role: "manager", - RoleName: "Manager", - Pending: true, - Expired: false, + ID: "57377908164", + Email: "sirpenguin@antarcticarocks.com", + Name: "Sir Penguin", + User: User{ + ID: "280094367316", + Name: "Sir Penguin", + Username: "sirpenguin@antarcticarocks.com", + Email: "sirpenguin@antarcticarocks.com", + AvatarURL: "https://secure.gravatar.com/avatar/16aeb26c5fdba335c7078e9e9ddb5149?s=32&d=mm", + IsActive: true, + HasPasswordAuth: true, + IsManaged: false, + DateJoined: mustParseTime("2021-07-06T21:13:58.375239Z"), + LastLogin: mustParseTime("2021-08-02T18:25:00.051182Z"), + Has2FA: false, + LastActive: mustParseTime("2021-08-02T21:32:18.836829Z"), + IsSuperuser: false, + IsStaff: false, + Avatar: Avatar{ + Type: "letter_avatar", + UUID: nil, + }, + Emails: []UserEmail{ + { + ID: "2153450836", + Email: "sirpenguin@antarcticarocks.com", + IsVerified: true, + }, + }, + }, + Role: "member", + RoleName: "Member", + OrganizationRole: OrganizationRoleMember, + Pending: false, + Expired: false, Flags: map[string]bool{ + "idp:provisioned": false, + "idp:role-restricted": false, "sso:linked": false, "sso:invalid": false, "member-limit:restricted": false, + "partnership:restricted": false, }, - Teams: []string{}, - DateCreated: mustParseTime("2020-01-01T00:00:00.000000Z"), + DateCreated: mustParseTime("2021-07-06T21:13:01.120263Z"), InviteStatus: "approved", InviterName: &inviterName, + Teams: []string{ + "cool-team", + "ancient-gabelers", + }, + TeamRoles: []TeamRole{ + { + TeamSlug: "ancient-gabelers", + Role: TeamRoleAdmin, + }, + { + TeamSlug: "powerful-abolitionist", + Role: TeamRoleContributor, + }, + }, } assert.Equal(t, &expected, member) From 449195c0035b57dceb22e3e0aa132e00e26d1928 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Thu, 7 Dec 2023 01:22:51 +0000 Subject: [PATCH 24/41] feat: Allow projects to update inbound data filters (#89) --- .../{project_filter.go => project_filters.go} | 27 +++++++-- ...filter_test.go => project_filters_test.go} | 58 +++++++++++-------- sentry/sentry.go | 4 +- 3 files changed, 57 insertions(+), 32 deletions(-) rename sentry/{project_filter.go => project_filters.go} (65%) rename sentry/{project_filter_test.go => project_filters_test.go} (67%) diff --git a/sentry/project_filter.go b/sentry/project_filters.go similarity index 65% rename from sentry/project_filter.go rename to sentry/project_filters.go index 9b642f0..4ddccad 100644 --- a/sentry/project_filter.go +++ b/sentry/project_filters.go @@ -13,12 +13,12 @@ type ProjectFilter struct { Active json.RawMessage `json:"active"` } -// ProjectOwnershipService provides methods for accessing Sentry project +// ProjectFiltersService provides methods for accessing Sentry project // filters API endpoints. -type ProjectFilterService service +type ProjectFiltersService service // Get the filters. -func (s *ProjectFilterService) Get(ctx context.Context, organizationSlug string, projectSlug string) ([]*ProjectFilter, *Response, error) { +func (s *ProjectFiltersService) Get(ctx context.Context, organizationSlug string, projectSlug string) ([]*ProjectFilter, *Response, error) { url := fmt.Sprintf("0/projects/%v/%v/filters/", organizationSlug, projectSlug) req, err := s.client.NewRequest(http.MethodGet, url, nil) if err != nil { @@ -41,7 +41,7 @@ type FilterConfig struct { } // GetFilterConfig retrieves filter configuration. -func (s *ProjectFilterService) GetFilterConfig(ctx context.Context, organizationSlug string, projectSlug string) (*FilterConfig, *Response, error) { +func (s *ProjectFiltersService) GetFilterConfig(ctx context.Context, organizationSlug string, projectSlug string) (*FilterConfig, *Response, error) { filters, resp, err := s.Get(ctx, organizationSlug, projectSlug) if err != nil { return nil, resp, err @@ -75,7 +75,7 @@ type BrowserExtensionParams struct { } // UpdateBrowserExtensions updates configuration for browser extension filter -func (s *ProjectFilterService) UpdateBrowserExtensions(ctx context.Context, organizationSlug string, projectSlug string, active bool) (*Response, error) { +func (s *ProjectFiltersService) UpdateBrowserExtensions(ctx context.Context, organizationSlug string, projectSlug string, active bool) (*Response, error) { url := fmt.Sprintf("0/projects/%v/%v/filters/browser-extensions/", organizationSlug, projectSlug) params := BrowserExtensionParams{active} req, err := s.client.NewRequest(http.MethodPut, url, params) @@ -92,7 +92,7 @@ type LegacyBrowserParams struct { } // UpdateLegacyBrowser updates configuration for legacy browser filters -func (s *ProjectFilterService) UpdateLegacyBrowser(ctx context.Context, organizationSlug string, projectSlug string, browsers []string) (*Response, error) { +func (s *ProjectFiltersService) UpdateLegacyBrowser(ctx context.Context, organizationSlug string, projectSlug string, browsers []string) (*Response, error) { url := fmt.Sprintf("0/projects/%v/%v/filters/legacy-browsers/", organizationSlug, projectSlug) params := LegacyBrowserParams{browsers} @@ -103,3 +103,18 @@ func (s *ProjectFilterService) UpdateLegacyBrowser(ctx context.Context, organiza return s.client.Do(ctx, req, nil) } + +type UpdateProjectFilterParams struct { + Active bool `json:"active"` + Subfilters []string `json:"subfilters"` +} + +func (s *ProjectFiltersService) Update(ctx context.Context, organizationSlug string, projectSlug string, filterID string, params *UpdateProjectFilterParams) (*Response, error) { + url := fmt.Sprintf("0/projects/%v/%v/filters/%v/", organizationSlug, projectSlug, filterID) + req, err := s.client.NewRequest(http.MethodPut, url, params) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/sentry/project_filter_test.go b/sentry/project_filters_test.go similarity index 67% rename from sentry/project_filter_test.go rename to sentry/project_filters_test.go index 709f5b1..e2fbb40 100644 --- a/sentry/project_filter_test.go +++ b/sentry/project_filters_test.go @@ -3,15 +3,13 @@ package sentry import ( "context" "fmt" - "io/ioutil" "net/http" - "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestProjectFilterService_GetWithLegacyExtension(t *testing.T) { +func TestProjectFiltersService_GetWithLegacyExtension(t *testing.T) { client, mux, _, teardown := setup() defer teardown() @@ -22,7 +20,7 @@ func TestProjectFilterService_GetWithLegacyExtension(t *testing.T) { }) ctx := context.Background() - filterConfig, _, err := client.ProjectFilter.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") + filterConfig, _, err := client.ProjectFilters.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") assert.NoError(t, err) expected := FilterConfig{ @@ -32,7 +30,7 @@ func TestProjectFilterService_GetWithLegacyExtension(t *testing.T) { assert.Equal(t, &expected, filterConfig) } -func TestProjectFilterService_GetWithoutLegacyExtension(t *testing.T) { +func TestProjectFiltersService_GetWithoutLegacyExtension(t *testing.T) { client, mux, _, teardown := setup() defer teardown() @@ -43,7 +41,7 @@ func TestProjectFilterService_GetWithoutLegacyExtension(t *testing.T) { }) ctx := context.Background() - filterConfig, _, err := client.ProjectFilter.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") + filterConfig, _, err := client.ProjectFilters.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") assert.NoError(t, err) expected := FilterConfig{ @@ -53,31 +51,20 @@ func TestProjectFilterService_GetWithoutLegacyExtension(t *testing.T) { assert.Equal(t, &expected, filterConfig) } -func readRequestBody(r *http.Request) string { - b, err := ioutil.ReadAll(r.Body) - defer r.Body.Close() - - if err != nil { - panic(err) - } - str := string(b) - str = strings.TrimSuffix(str, "\n") - return str -} - func TestBrowserExtensionFilter(t *testing.T) { client, mux, _, teardown := setup() defer teardown() mux.HandleFunc("/api/0/projects/test_org/test_project/filters/browser-extensions/", func(w http.ResponseWriter, r *http.Request) { assertMethod(t, "PUT", r) - body := readRequestBody(r) - assert.Equal(t, body, `{"active":true}`) + assertPostJSON(t, map[string]interface{}{ + "active": true, + }, r) w.Header().Set("Content-Type", "application/json") }) ctx := context.Background() - _, err := client.ProjectFilter.UpdateBrowserExtensions(ctx, "test_org", "test_project", true) + _, err := client.ProjectFilters.UpdateBrowserExtensions(ctx, "test_org", "test_project", true) assert.NoError(t, err) } @@ -87,15 +74,16 @@ func TestLegacyBrowserFilter(t *testing.T) { mux.HandleFunc("/api/0/projects/test_org/test_project/filters/legacy-browsers/", func(w http.ResponseWriter, r *http.Request) { assertMethod(t, "PUT", r) - body := readRequestBody(r) - assert.Equal(t, body, `{"subfilters":["ie_pre_9","ie10"]}`) + assertPostJSON(t, map[string]interface{}{ + "subfilters": []interface{}{"ie_pre_9", "ie10"}, + }, r) w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, "") }) ctx := context.Background() browsers := []string{"ie_pre_9", "ie10"} - _, err := client.ProjectFilter.UpdateLegacyBrowser(ctx, "test_org", "test_project", browsers) + _, err := client.ProjectFilters.UpdateLegacyBrowser(ctx, "test_org", "test_project", browsers) assert.NoError(t, err) } @@ -161,3 +149,25 @@ var ( } ]` ) + +func TestProjectFiltersService_Update(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/powerful-abolitionist/filters/filter-id/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodPut, r) + assertPostJSON(t, map[string]interface{}{ + "active": true, + "subfilters": []interface{}{"ie_pre_9", "ie9"}, + }, r) + }) + + params := &UpdateProjectFilterParams{ + Active: true, + Subfilters: []string{"ie_pre_9", "ie9"}, + } + ctx := context.Background() + _, err := client.ProjectFilters.Update(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist", "filter-id", params) + assert.NoError(t, err) + +} diff --git a/sentry/sentry.go b/sentry/sentry.go index 9be1b68..19bfd63 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -60,7 +60,7 @@ type Client struct { ProjectOwnerships *ProjectOwnershipsService ProjectPlugins *ProjectPluginsService Projects *ProjectsService - ProjectFilter *ProjectFilterService + ProjectFilters *ProjectFiltersService ReleaseDeployments *ReleaseDeploymentsService Teams *TeamsService TeamMembers *TeamMembersService @@ -93,7 +93,7 @@ func NewClient(httpClient *http.Client) *Client { c.OrganizationMembers = (*OrganizationMembersService)(&c.common) c.OrganizationRepositories = (*OrganizationRepositoriesService)(&c.common) c.Organizations = (*OrganizationsService)(&c.common) - c.ProjectFilter = (*ProjectFilterService)(&c.common) + c.ProjectFilters = (*ProjectFiltersService)(&c.common) c.ProjectKeys = (*ProjectKeysService)(&c.common) c.ProjectOwnerships = (*ProjectOwnershipsService)(&c.common) c.ProjectPlugins = (*ProjectPluginsService)(&c.common) From 9f4943bc48d7029b11a5de2e6f34b193bac26df6 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Thu, 7 Dec 2023 01:32:39 +0000 Subject: [PATCH 25/41] feat: Enable/disable spike protection (#90) --- sentry/sentry.go | 12 ++++---- sentry/spike_protections.go | 33 ++++++++++++++++++++++ sentry/spike_protections_test.go | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 sentry/spike_protections.go create mode 100644 sentry/spike_protections_test.go diff --git a/sentry/sentry.go b/sentry/sentry.go index 19bfd63..ca249fb 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -47,8 +47,8 @@ type Client struct { common service // Services - DashboardWidgets *DashboardWidgetsService Dashboards *DashboardsService + DashboardWidgets *DashboardWidgetsService IssueAlerts *IssueAlertsService MetricAlerts *MetricAlertsService OrganizationCodeMappings *OrganizationCodeMappingsService @@ -56,14 +56,15 @@ type Client struct { OrganizationMembers *OrganizationMembersService OrganizationRepositories *OrganizationRepositoriesService Organizations *OrganizationsService + ProjectFilters *ProjectFiltersService ProjectKeys *ProjectKeysService ProjectOwnerships *ProjectOwnershipsService ProjectPlugins *ProjectPluginsService Projects *ProjectsService - ProjectFilters *ProjectFiltersService ReleaseDeployments *ReleaseDeploymentsService - Teams *TeamsService + SpikeProtections *SpikeProtectionsService TeamMembers *TeamMembersService + Teams *TeamsService } type service struct { @@ -84,8 +85,8 @@ func NewClient(httpClient *http.Client) *Client { UserAgent: userAgent, } c.common.client = c - c.DashboardWidgets = (*DashboardWidgetsService)(&c.common) c.Dashboards = (*DashboardsService)(&c.common) + c.DashboardWidgets = (*DashboardWidgetsService)(&c.common) c.IssueAlerts = (*IssueAlertsService)(&c.common) c.MetricAlerts = (*MetricAlertsService)(&c.common) c.OrganizationCodeMappings = (*OrganizationCodeMappingsService)(&c.common) @@ -99,8 +100,9 @@ func NewClient(httpClient *http.Client) *Client { c.ProjectPlugins = (*ProjectPluginsService)(&c.common) c.Projects = (*ProjectsService)(&c.common) c.ReleaseDeployments = (*ReleaseDeploymentsService)(&c.common) - c.Teams = (*TeamsService)(&c.common) + c.SpikeProtections = (*SpikeProtectionsService)(&c.common) c.TeamMembers = (*TeamMembersService)(&c.common) + c.Teams = (*TeamsService)(&c.common) return c } diff --git a/sentry/spike_protections.go b/sentry/spike_protections.go new file mode 100644 index 0000000..20c0d6b --- /dev/null +++ b/sentry/spike_protections.go @@ -0,0 +1,33 @@ +package sentry + +import ( + "context" + "fmt" + "net/http" +) + +type SpikeProtectionsService service + +type SpikeProtectionParams struct { + Projects []string `json:"projects"` +} + +func (s *SpikeProtectionsService) Enable(ctx context.Context, organizationSlug string, params *SpikeProtectionParams) (*Response, error) { + u := fmt.Sprintf("0/organizations/%v/spike-protections/", organizationSlug) + req, err := s.client.NewRequest(http.MethodPost, u, params) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +func (s *SpikeProtectionsService) Disable(ctx context.Context, organizationSlug string, params *SpikeProtectionParams) (*Response, error) { + u := fmt.Sprintf("0/organizations/%v/spike-protections/", organizationSlug) + req, err := s.client.NewRequest(http.MethodDelete, u, params) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/sentry/spike_protections_test.go b/sentry/spike_protections_test.go new file mode 100644 index 0000000..ba9b012 --- /dev/null +++ b/sentry/spike_protections_test.go @@ -0,0 +1,47 @@ +package sentry + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSpikeProtectionsService_Enable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/organization_slug/spike-protections/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodPost, r) + assertPostJSON(t, map[string]interface{}{ + "projects": []interface{}{"$all"}, + }, r) + }) + + params := &SpikeProtectionParams{ + Projects: []string{"$all"}, + } + ctx := context.Background() + _, err := client.SpikeProtections.Enable(ctx, "organization_slug", params) + assert.NoError(t, err) +} + +func TestSpikeProtectionsService_Disable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/organization_slug/spike-protections/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodDelete, r) + assertPostJSON(t, map[string]interface{}{ + "projects": []interface{}{"$all"}, + }, r) + }) + + params := &SpikeProtectionParams{ + Projects: []string{"$all"}, + } + ctx := context.Background() + _, err := client.SpikeProtections.Disable(ctx, "organization_slug", params) + assert.NoError(t, err) +} From e63a506909a68dc89281f90dac46b438b014c0f5 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sat, 9 Dec 2023 11:13:47 +0000 Subject: [PATCH 26/41] feat: Update team member --- sentry/team_members.go | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/sentry/team_members.go b/sentry/team_members.go index 711801c..c9dc1da 100644 --- a/sentry/team_members.go +++ b/sentry/team_members.go @@ -3,6 +3,7 @@ package sentry import ( "context" "fmt" + "net/http" "time" ) @@ -26,7 +27,7 @@ type TeamMembersService service func (s *TeamMembersService) Create(ctx context.Context, organizationSlug string, memberID string, teamSlug string) (*TeamMember, *Response, error) { u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberID, teamSlug) - req, err := s.client.NewRequest("POST", u, nil) + req, err := s.client.NewRequest(http.MethodPost, u, nil) if err != nil { return nil, nil, err } @@ -39,9 +40,33 @@ func (s *TeamMembersService) Create(ctx context.Context, organizationSlug string return member, resp, nil } +type UpdateTeamMemberParams struct { + TeamRole *string `json:"teamRole,omitempty"` +} + +type UpdateTeamMemberResponse struct { + IsActive *bool `json:"isActive,omitempty"` + TeamRole *string `json:"teamRole,omitempty"` +} + +func (s *TeamMembersService) Update(ctx context.Context, organizationSlug string, memberID string, teamSlug string, params *UpdateTeamMemberParams) (*UpdateTeamMemberResponse, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberID, teamSlug) + req, err := s.client.NewRequest(http.MethodPut, u, params) + if err != nil { + return nil, nil, err + } + + member := new(UpdateTeamMemberResponse) + resp, err := s.client.Do(ctx, req, member) + if err != nil { + return nil, resp, err + } + return member, resp, nil +} + func (s *TeamMembersService) Delete(ctx context.Context, organizationSlug string, memberID string, teamSlug string) (*TeamMember, *Response, error) { u := fmt.Sprintf("0/organizations/%v/members/%v/teams/%v/", organizationSlug, memberID, teamSlug) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest(http.MethodDelete, u, nil) if err != nil { return nil, nil, err } From 0c7dd3e56585ad9e2cf04c395413b650c8dc4082 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sat, 9 Dec 2023 11:49:56 +0000 Subject: [PATCH 27/41] ref: org/team role list --- sentry/organization_members.go | 37 ++++++++------ sentry/organization_members_test.go | 77 ++++++++++++++++++++--------- 2 files changed, 76 insertions(+), 38 deletions(-) diff --git a/sentry/organization_members.go b/sentry/organization_members.go index 3275472..5c5868a 100644 --- a/sentry/organization_members.go +++ b/sentry/organization_members.go @@ -6,24 +6,31 @@ import ( "time" ) +// https://github.com/getsentry/sentry/blob/8b683002ec84f9edd338348500937a480e49e61c/src/sentry/api/serializers/models/role.py#L32 +type RoleListItem struct { + ID string `json:"id"` + Name string `json:"name"` + IsAllowed bool `json:"isAllowed"` +} + // OrganizationMember represents a User's membership to the organization. // https://github.com/getsentry/sentry/blob/22.5.0/src/sentry/api/serializers/models/organization_member/response.py#L57-L69 type OrganizationMember struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - User User `json:"user"` - Role string `json:"role"` // Deprecated - RoleName string `json:"roleName"` // Deprecated - OrganizationRole string `json:"orgRole"` - Pending bool `json:"pending"` - Expired bool `json:"expired"` - Flags map[string]bool `json:"flags"` - DateCreated time.Time `json:"dateCreated"` - InviteStatus string `json:"inviteStatus"` - InviterName *string `json:"inviterName"` - Teams []string `json:"teams"` - TeamRoles []TeamRole `json:"teamRoles"` + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + User User `json:"user"` + OrganizationRole string `json:"orgRole"` + OrganizationRoleList []RoleListItem `json:"orgRoleList"` + Pending bool `json:"pending"` + Expired bool `json:"expired"` + Flags map[string]bool `json:"flags"` + DateCreated time.Time `json:"dateCreated"` + InviteStatus string `json:"inviteStatus"` + InviterName *string `json:"inviterName"` + TeamRoleList []RoleListItem `json:"teamRoleList"` + TeamRoles []TeamRole `json:"teamRoles"` + Teams []string `json:"teams"` } const ( diff --git a/sentry/organization_members_test.go b/sentry/organization_members_test.go index b2e6932..828e085 100644 --- a/sentry/organization_members_test.go +++ b/sentry/organization_members_test.go @@ -112,10 +112,8 @@ func TestOrganizationMembersService_List(t *testing.T) { }, }, }, - Role: "owner", - RoleName: "Owner", - Pending: false, - Expired: false, + Pending: false, + Expired: false, Flags: map[string]bool{ "sso:invalid": false, "sso:linked": false, @@ -226,10 +224,8 @@ func TestOrganizationMembersService_Get(t *testing.T) { }, }, }, - Role: "owner", - RoleName: "Owner", - Pending: false, - Expired: false, + Pending: false, + Expired: false, Flags: map[string]bool{ "sso:invalid": false, "sso:linked": false, @@ -281,14 +277,12 @@ func TestOrganizationMembersService_Create(t *testing.T) { inviterName := "John Doe" expected := OrganizationMember{ - ID: "1", - Email: "test@example.com", - Name: "test@example.com", - User: User{}, - Role: "member", - RoleName: "Member", - Pending: true, - Expired: false, + ID: "1", + Email: "test@example.com", + Name: "test@example.com", + User: User{}, + Pending: true, + Expired: false, Flags: map[string]bool{ "sso:linked": false, "sso:invalid": false, @@ -596,11 +590,36 @@ func TestOrganizationMembersService_Update(t *testing.T) { }, }, }, - Role: "member", - RoleName: "Member", OrganizationRole: OrganizationRoleMember, - Pending: false, - Expired: false, + OrganizationRoleList: []RoleListItem{ + { + ID: "billing", + Name: "Billing", + IsAllowed: true, + }, + { + ID: "member", + Name: "Member", + IsAllowed: true, + }, + { + ID: "admin", + Name: "Admin", + IsAllowed: true, + }, + { + ID: "manager", + Name: "Manager", + IsAllowed: true, + }, + { + ID: "owner", + Name: "Owner", + IsAllowed: true, + }, + }, + Pending: false, + Expired: false, Flags: map[string]bool{ "idp:provisioned": false, "idp:role-restricted": false, @@ -612,9 +631,17 @@ func TestOrganizationMembersService_Update(t *testing.T) { DateCreated: mustParseTime("2021-07-06T21:13:01.120263Z"), InviteStatus: "approved", InviterName: &inviterName, - Teams: []string{ - "cool-team", - "ancient-gabelers", + TeamRoleList: []RoleListItem{ + { + ID: "contributor", + Name: "Contributor", + IsAllowed: false, + }, + { + ID: "admin", + Name: "Team Admin", + IsAllowed: false, + }, }, TeamRoles: []TeamRole{ { @@ -626,6 +653,10 @@ func TestOrganizationMembersService_Update(t *testing.T) { Role: TeamRoleContributor, }, }, + Teams: []string{ + "cool-team", + "ancient-gabelers", + }, } assert.Equal(t, &expected, member) From 881b50c9d2988620f5c94a7d60c2e7df6ac990d2 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sat, 9 Dec 2023 21:23:42 +0000 Subject: [PATCH 28/41] feat: project inbound data filters service --- sentry/project_inbound_data_filters.go | 42 ++++++++ sentry/project_inbound_data_filters_test.go | 110 ++++++++++++++++++++ sentry/sentry.go | 38 +++---- sentry/types.go | 38 +++++++ 4 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 sentry/project_inbound_data_filters.go create mode 100644 sentry/project_inbound_data_filters_test.go create mode 100644 sentry/types.go diff --git a/sentry/project_inbound_data_filters.go b/sentry/project_inbound_data_filters.go new file mode 100644 index 0000000..13f7182 --- /dev/null +++ b/sentry/project_inbound_data_filters.go @@ -0,0 +1,42 @@ +package sentry + +import ( + "context" + "net/http" +) + +type ProjectInboundDataFilter struct { + ID string `json:"id"` + Active BoolOrStringSlice `json:"active"` +} + +type ProjectInboundDataFiltersService service + +func (s *ProjectInboundDataFiltersService) List(ctx context.Context, organizationSlug string, projectSlug string) ([]*ProjectInboundDataFilter, *Response, error) { + u := "0/projects/" + organizationSlug + "/" + projectSlug + "/filters/" + req, err := s.client.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, nil, err + } + + filters := []*ProjectInboundDataFilter{} + resp, err := s.client.Do(ctx, req, &filters) + if err != nil { + return nil, resp, err + } + return filters, resp, nil +} + +type UpdateProjectInboundDataFilterParams struct { + Active *bool `json:"active,omitempty"` + Subfilters []string `json:"subfilters,omitempty"` +} + +func (s *ProjectInboundDataFiltersService) Update(ctx context.Context, organizationSlug string, projectSlug string, filterID string, params *UpdateProjectInboundDataFilterParams) (*Response, error) { + u := "0/projects/" + organizationSlug + "/" + projectSlug + "/filters/" + filterID + "/" + req, err := s.client.NewRequest(http.MethodPut, u, params) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} diff --git a/sentry/project_inbound_data_filters_test.go b/sentry/project_inbound_data_filters_test.go new file mode 100644 index 0000000..ed600a5 --- /dev/null +++ b/sentry/project_inbound_data_filters_test.go @@ -0,0 +1,110 @@ +package sentry + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProjectInboundDataFiltersService_List(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/organization_slug/project_slug/filters/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodGet, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[ + { + "id": "browser-extensions", + "active": false + }, + { + "id": "filtered-transaction", + "active": true + }, + { + "id": "legacy-browsers", + "active": [ + "ie_pre_9" + ] + }, + { + "id": "localhost", + "active": false + }, + { + "id": "web-crawlers", + "active": false + } + ]`) + }) + + ctx := context.Background() + filters, _, err := client.ProjectInboundDataFilters.List(ctx, "organization_slug", "project_slug") + assert.NoError(t, err) + + expected := []*ProjectInboundDataFilter{ + { + ID: "browser-extensions", + Active: BoolOrStringSlice{IsBool: true, BoolVal: false}, + }, + { + ID: "filtered-transaction", + Active: BoolOrStringSlice{IsBool: true, BoolVal: true}, + }, + { + ID: "legacy-browsers", + Active: BoolOrStringSlice{IsBool: false, SliceVal: []string{"ie_pre_9"}}, + }, + { + ID: "localhost", + Active: BoolOrStringSlice{IsBool: true, BoolVal: false}, + }, + { + ID: "web-crawlers", + Active: BoolOrStringSlice{IsBool: true, BoolVal: false}, + }, + } + assert.Equal(t, expected, filters) +} + +func TestProjectInboundDataFiltersService_UpdateActive(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/organization_slug/project_slug/filters/filter_id/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodPut, r) + assertPostJSON(t, map[string]interface{}{ + "active": true, + }, r) + }) + + ctx := context.Background() + params := &UpdateProjectInboundDataFilterParams{ + Active: Bool(true), + } + _, err := client.ProjectInboundDataFilters.Update(ctx, "organization_slug", "project_slug", "filter_id", params) + assert.NoError(t, err) +} + +func TestProjectInboundDataFiltersService_UpdateSubfilters(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/organization_slug/project_slug/filters/filter_id/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodPut, r) + assertPostJSON(t, map[string]interface{}{ + "subfilters": []interface{}{"ie_pre_9"}, + }, r) + }) + + ctx := context.Background() + params := &UpdateProjectInboundDataFilterParams{ + Subfilters: []string{"ie_pre_9"}, + } + _, err := client.ProjectInboundDataFilters.Update(ctx, "organization_slug", "project_slug", "filter_id", params) + assert.NoError(t, err) +} diff --git a/sentry/sentry.go b/sentry/sentry.go index ca249fb..50de5e8 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -47,24 +47,25 @@ type Client struct { common service // Services - Dashboards *DashboardsService - DashboardWidgets *DashboardWidgetsService - IssueAlerts *IssueAlertsService - MetricAlerts *MetricAlertsService - OrganizationCodeMappings *OrganizationCodeMappingsService - OrganizationIntegrations *OrganizationIntegrationsService - OrganizationMembers *OrganizationMembersService - OrganizationRepositories *OrganizationRepositoriesService - Organizations *OrganizationsService - ProjectFilters *ProjectFiltersService - ProjectKeys *ProjectKeysService - ProjectOwnerships *ProjectOwnershipsService - ProjectPlugins *ProjectPluginsService - Projects *ProjectsService - ReleaseDeployments *ReleaseDeploymentsService - SpikeProtections *SpikeProtectionsService - TeamMembers *TeamMembersService - Teams *TeamsService + Dashboards *DashboardsService + DashboardWidgets *DashboardWidgetsService + IssueAlerts *IssueAlertsService + MetricAlerts *MetricAlertsService + OrganizationCodeMappings *OrganizationCodeMappingsService + OrganizationIntegrations *OrganizationIntegrationsService + OrganizationMembers *OrganizationMembersService + OrganizationRepositories *OrganizationRepositoriesService + Organizations *OrganizationsService + ProjectFilters *ProjectFiltersService + ProjectInboundDataFilters *ProjectInboundDataFiltersService + ProjectKeys *ProjectKeysService + ProjectOwnerships *ProjectOwnershipsService + ProjectPlugins *ProjectPluginsService + Projects *ProjectsService + ReleaseDeployments *ReleaseDeploymentsService + SpikeProtections *SpikeProtectionsService + TeamMembers *TeamMembersService + Teams *TeamsService } type service struct { @@ -95,6 +96,7 @@ func NewClient(httpClient *http.Client) *Client { c.OrganizationRepositories = (*OrganizationRepositoriesService)(&c.common) c.Organizations = (*OrganizationsService)(&c.common) c.ProjectFilters = (*ProjectFiltersService)(&c.common) + c.ProjectInboundDataFilters = (*ProjectInboundDataFiltersService)(&c.common) c.ProjectKeys = (*ProjectKeysService)(&c.common) c.ProjectOwnerships = (*ProjectOwnershipsService)(&c.common) c.ProjectPlugins = (*ProjectPluginsService)(&c.common) diff --git a/sentry/types.go b/sentry/types.go new file mode 100644 index 0000000..b372ef0 --- /dev/null +++ b/sentry/types.go @@ -0,0 +1,38 @@ +package sentry + +import ( + "encoding/json" + "fmt" +) + +// BoolOrStringSlice is a type that can be unmarshaled from either a bool or a +// string slice. +type BoolOrStringSlice struct { + IsBool bool + BoolVal bool + SliceVal []string +} + +var _ json.Unmarshaler = (*BoolOrStringSlice)(nil) + +// UnmarshalJSON implements json.Unmarshaler. +func (bos *BoolOrStringSlice) UnmarshalJSON(data []byte) error { + // Try to unmarshal as a bool + var boolVal bool + if err := json.Unmarshal(data, &boolVal); err == nil { + bos.IsBool = true + bos.BoolVal = boolVal + return nil + } + + // Try to unmarshal as a string slice + var sliceVal []string + if err := json.Unmarshal(data, &sliceVal); err == nil { + bos.IsBool = false + bos.SliceVal = sliceVal + return nil + } + + // If neither worked, return an error + return fmt.Errorf("unable to unmarshal as bool or string slice: %s", string(data)) +} From 5fe6715dbf45a6a6929278248f60696626b763bc Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sat, 9 Dec 2023 23:37:55 +0000 Subject: [PATCH 29/41] feat: notification actions --- sentry/notification_actions.go | 86 +++++++++++++++++++++ sentry/notification_actions_test.go | 113 ++++++++++++++++++++++++++++ sentry/sentry.go | 14 ++++ 3 files changed, 213 insertions(+) create mode 100644 sentry/notification_actions.go create mode 100644 sentry/notification_actions_test.go diff --git a/sentry/notification_actions.go b/sentry/notification_actions.go new file mode 100644 index 0000000..08a3db3 --- /dev/null +++ b/sentry/notification_actions.go @@ -0,0 +1,86 @@ +package sentry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +type NotificationActionsService service + +type CreateNotificationActionParams struct { + TriggerType *string `json:"triggerType"` + ServiceType *string `json:"serviceType"` + IntegrationId *json.Number `json:"integrationId,omitempty"` + TargetIdentifier interface{} `json:"targetIdentifier,omitempty"` + TargetDisplay *string `json:"targetDisplay,omitempty"` + TargetType *string `json:"targetType,omitempty"` + Projects []string `json:"projects"` +} + +type NotificationAction struct { + ID *json.Number `json:"id"` + TriggerType *string `json:"triggerType"` + ServiceType *string `json:"serviceType"` + IntegrationId *json.Number `json:"integrationId"` + TargetIdentifier interface{} `json:"targetIdentifier"` + TargetDisplay *string `json:"targetDisplay"` + TargetType *string `json:"targetType"` + Projects []json.Number `json:"projects"` +} + +func (s *NotificationActionsService) Get(ctx context.Context, organizationSlug string, actionId string) (*NotificationAction, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/notifications/actions/%v/", organizationSlug, actionId) + req, err := s.client.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, nil, err + } + + action := &NotificationAction{} + resp, err := s.client.Do(ctx, req, action) + if err != nil { + return nil, resp, err + } + return action, resp, nil +} + +func (s *NotificationActionsService) Create(ctx context.Context, organizationSlug string, params *CreateNotificationActionParams) (*NotificationAction, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/notifications/actions/", organizationSlug) + req, err := s.client.NewRequest(http.MethodPost, u, params) + if err != nil { + return nil, nil, err + } + + action := &NotificationAction{} + resp, err := s.client.Do(ctx, req, action) + if err != nil { + return nil, resp, err + } + return action, resp, nil +} + +func (s *NotificationActionsService) Update(ctx context.Context, organizationSlug string, actionId string, params *CreateNotificationActionParams) (*NotificationAction, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/notification/actions/%v/", organizationSlug, actionId) + req, err := s.client.NewRequest(http.MethodPut, u, params) + if err != nil { + return nil, nil, err + } + + action := &NotificationAction{} + resp, err := s.client.Do(ctx, req, action) + if err != nil { + return nil, resp, err + } + return action, resp, nil +} + +func (s *NotificationActionsService) Delete(ctx context.Context, organizationSlug string, actionId string) (*Response, error) { + u := fmt.Sprintf("0/organizations/%v/notifications/actions/%v/", organizationSlug, actionId) + req, err := s.client.NewRequest(http.MethodDelete, u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/sentry/notification_actions_test.go b/sentry/notification_actions_test.go new file mode 100644 index 0000000..180aa2c --- /dev/null +++ b/sentry/notification_actions_test.go @@ -0,0 +1,113 @@ +package sentry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNotificationActionsService_Get(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/organization_slug/notifications/actions/action_id/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodGet, r) + fmt.Fprintf(w, `{ + "id": "836501735", + "organizationId": "62848264", + "serviceType": "sentry_notification", + "targetDisplay": "default", + "targetIdentifier": "default", + "targetType": "specific", + "triggerType": "spike-protection", + "projects": [ + 4505321021243392 + ] + }`) + }) + + ctx := context.Background() + action, _, err := client.NotificationActions.Get(ctx, "organization_slug", "action_id") + assert.NoError(t, err) + + expected := &NotificationAction{ + ID: JsonNumber(json.Number("836501735")), + TriggerType: String("spike-protection"), + ServiceType: String("sentry_notification"), + TargetIdentifier: "default", + TargetDisplay: String("default"), + TargetType: String("specific"), + Projects: []json.Number{"4505321021243392"}, + } + assert.Equal(t, expected, action) +} + +func TestNotificationActionsService_Create(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/organization_slug/notifications/actions/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodPost, r) + assertPostJSON(t, map[string]interface{}{ + "projects": []interface{}{"go"}, + "serviceType": "sentry_notification", + "targetDisplay": "default", + "targetIdentifier": "default", + "targetType": "specific", + "triggerType": "spike-protection", + }, r) + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "id": "836501735", + "organizationId": "62848264", + "serviceType": "sentry_notification", + "targetDisplay": "default", + "targetIdentifier": "default", + "targetType": "specific", + "triggerType": "spike-protection", + "projects": [ + 4505321021243392 + ] + }`) + }) + + params := &CreateNotificationActionParams{ + TriggerType: String("spike-protection"), + ServiceType: String("sentry_notification"), + TargetIdentifier: String("default"), + TargetDisplay: String("default"), + TargetType: String("specific"), + Projects: []string{"go"}, + } + ctx := context.Background() + action, _, err := client.NotificationActions.Create(ctx, "organization_slug", params) + assert.NoError(t, err) + + expected := &NotificationAction{ + ID: JsonNumber(json.Number("836501735")), + TriggerType: String("spike-protection"), + ServiceType: String("sentry_notification"), + TargetIdentifier: "default", + TargetDisplay: String("default"), + TargetType: String("specific"), + Projects: []json.Number{"4505321021243392"}, + } + assert.Equal(t, expected, action) +} + +func TestNotificationActionsService_Delete(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/organizations/organization_slug/notifications/actions/action_id/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodDelete, r) + }) + + ctx := context.Background() + _, err := client.NotificationActions.Delete(ctx, "organization_slug", "action_id") + assert.NoError(t, err) +} diff --git a/sentry/sentry.go b/sentry/sentry.go index 50de5e8..19578e9 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -51,6 +51,7 @@ type Client struct { DashboardWidgets *DashboardWidgetsService IssueAlerts *IssueAlertsService MetricAlerts *MetricAlertsService + NotificationActions *NotificationActionsService OrganizationCodeMappings *OrganizationCodeMappingsService OrganizationIntegrations *OrganizationIntegrationsService OrganizationMembers *OrganizationMembersService @@ -90,6 +91,7 @@ func NewClient(httpClient *http.Client) *Client { c.DashboardWidgets = (*DashboardWidgetsService)(&c.common) c.IssueAlerts = (*IssueAlertsService)(&c.common) c.MetricAlerts = (*MetricAlertsService)(&c.common) + c.NotificationActions = (*NotificationActionsService)(&c.common) c.OrganizationCodeMappings = (*OrganizationCodeMappingsService)(&c.common) c.OrganizationIntegrations = (*OrganizationIntegrationsService)(&c.common) c.OrganizationMembers = (*OrganizationMembersService)(&c.common) @@ -458,3 +460,15 @@ func TimeValue(v *time.Time) time.Time { } return time.Time{} } + +// JsonNumber returns a pointer to the json.Number value passed in. +func JsonNumber(v json.Number) *json.Number { return &v } + +// JsonNumberValue returns the value of the json.Number pointer passed in or +// json.Number("") if the pointer is nil. +func JsonNumberValue(v *json.Number) json.Number { + if v != nil { + return *v + } + return json.Number("") +} From 6da456e3f77d3957b66b8678cc76e0f6ab5d76a6 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Mon, 11 Dec 2023 02:00:28 +0000 Subject: [PATCH 30/41] feat: add list params to list projects endpoint --- sentry/projects.go | 7 ++++++- sentry/projects_test.go | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/sentry/projects.go b/sentry/projects.go index c9465f7..6c6afd9 100644 --- a/sentry/projects.go +++ b/sentry/projects.go @@ -94,8 +94,13 @@ type ProjectsService service // List projects available. // https://docs.sentry.io/api/projects/list-your-projects/ -func (s *ProjectsService) List(ctx context.Context) ([]*Project, *Response, error) { +func (s *ProjectsService) List(ctx context.Context, params *ListCursorParams) ([]*Project, *Response, error) { u := "0/projects/" + u, err := addQuery(u, params) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, nil, err diff --git a/sentry/projects_test.go b/sentry/projects_test.go index f5761e6..c3a7948 100644 --- a/sentry/projects_test.go +++ b/sentry/projects_test.go @@ -142,7 +142,7 @@ func TestProjectsService_List(t *testing.T) { }) ctx := context.Background() - projects, _, err := client.Projects.List(ctx) + projects, _, err := client.Projects.List(ctx, nil) assert.NoError(t, err) expectedOrganization := Organization{ From 621cfd2b7aa3907edf1e5780e273f445e17e6b2a Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Mon, 11 Dec 2023 02:15:10 +0000 Subject: [PATCH 31/41] ref: alias UpdateNotificationActionParams --- sentry/notification_actions.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry/notification_actions.go b/sentry/notification_actions.go index 08a3db3..c75ff38 100644 --- a/sentry/notification_actions.go +++ b/sentry/notification_actions.go @@ -60,8 +60,10 @@ func (s *NotificationActionsService) Create(ctx context.Context, organizationSlu return action, resp, nil } -func (s *NotificationActionsService) Update(ctx context.Context, organizationSlug string, actionId string, params *CreateNotificationActionParams) (*NotificationAction, *Response, error) { - u := fmt.Sprintf("0/organizations/%v/notification/actions/%v/", organizationSlug, actionId) +type UpdateNotificationActionParams = CreateNotificationActionParams + +func (s *NotificationActionsService) Update(ctx context.Context, organizationSlug string, actionId string, params *UpdateNotificationActionParams) (*NotificationAction, *Response, error) { + u := fmt.Sprintf("0/organizations/%v/notifications/actions/%v/", organizationSlug, actionId) req, err := s.client.NewRequest(http.MethodPut, u, params) if err != nil { return nil, nil, err From 771069584cbc76cc9c978238fdd41fe4d4a600e7 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Mon, 11 Dec 2023 18:38:15 +0000 Subject: [PATCH 32/41] feat: project symbol source --- sentry/project_symbol_sources.go | 157 ++++++++++++++++ sentry/project_symbol_sources_test.go | 259 ++++++++++++++++++++++++++ sentry/sentry.go | 2 + 3 files changed, 418 insertions(+) create mode 100644 sentry/project_symbol_sources.go create mode 100644 sentry/project_symbol_sources_test.go diff --git a/sentry/project_symbol_sources.go b/sentry/project_symbol_sources.go new file mode 100644 index 0000000..8a4ce2a --- /dev/null +++ b/sentry/project_symbol_sources.go @@ -0,0 +1,157 @@ +package sentry + +import ( + "context" + "net/http" +) + +type ProjectSymbolSourceLayout struct { + Type *string `json:"type"` + Casing *string `json:"casing"` +} + +type ProjectSymbolSourceHiddenSecret struct { + HiddenSecret *bool `json:"hidden-secret"` +} + +type ProjectSymbolSource struct { + ID *string `json:"id"` + Type *string `json:"type"` + Name *string `json:"name"` + Layout *ProjectSymbolSourceLayout `json:"layout"` + + AppConnectIssuer *string `json:"appconnectIssuer,omitempty"` + AppConnectPrivateKey *ProjectSymbolSourceHiddenSecret `json:"appconnectPrivateKey,omitempty"` + AppId *string `json:"appId,omitempty"` + Url *string `json:"url,omitempty"` + Username *string `json:"username,omitempty"` + Password *ProjectSymbolSourceHiddenSecret `json:"password,omitempty"` + Bucket *string `json:"bucket,omitempty"` + Region *string `json:"region,omitempty"` + AccessKey *string `json:"access_key,omitempty"` + SecretKey *ProjectSymbolSourceHiddenSecret `json:"secret_key,omitempty"` + Prefix *string `json:"prefix,omitempty"` + ClientEmail *string `json:"client_email,omitempty"` + PrivateKey *ProjectSymbolSourceHiddenSecret `json:"private_key,omitempty"` +} + +type ProjectSymbolSourcesService service + +type ProjectSymbolSourceQueryParams struct { + ID *string `url:"id,omitempty"` +} + +func (s *ProjectSymbolSourcesService) List(ctx context.Context, organizationSlug string, projectSlug string, params *ProjectSymbolSourceQueryParams) ([]*ProjectSymbolSource, *Response, error) { + u := "0/projects/" + organizationSlug + "/" + projectSlug + "/symbol-sources/" + u, err := addQuery(u, params) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, nil, err + } + + filters := []*ProjectSymbolSource{} + resp, err := s.client.Do(ctx, req, &filters) + if err != nil { + return nil, resp, err + } + return filters, resp, nil +} + +type CreateProjectSymbolSourceParams struct { + Type *string `json:"type"` + Name *string `json:"name"` + Layout *ProjectSymbolSourceLayout `json:"layout"` + + AppConnectIssuer *string `json:"appconnectIssuer,omitempty"` + AppConnectPrivateKey *string `json:"appconnectPrivateKey,omitempty"` + AppId *string `json:"appId,omitempty"` + Url *string `json:"url,omitempty"` + Username *string `json:"username,omitempty"` + Password *string `json:"password,omitempty"` + Bucket *string `json:"bucket,omitempty"` + Region *string `json:"region,omitempty"` + AccessKey *string `json:"access_key,omitempty"` + SecretKey *string `json:"secret_key,omitempty"` + Prefix *string `json:"prefix,omitempty"` + ClientEmail *string `json:"client_email,omitempty"` + PrivateKey *string `json:"private_key,omitempty"` +} + +func (s *ProjectSymbolSourcesService) Create(ctx context.Context, organizationSlug string, projectSlug string, params *CreateProjectSymbolSourceParams) (*ProjectSymbolSource, *Response, error) { + u := "0/projects/" + organizationSlug + "/" + projectSlug + "/symbol-sources/" + req, err := s.client.NewRequest(http.MethodPost, u, params) + if err != nil { + return nil, nil, err + } + + filter := &ProjectSymbolSource{} + resp, err := s.client.Do(ctx, req, filter) + if err != nil { + return nil, resp, err + } + return filter, resp, nil +} + +type UpdateProjectSymbolSourceParams struct { + ID *string `json:"id"` + Type *string `json:"type"` + Name *string `json:"name"` + Layout *ProjectSymbolSourceLayout `json:"layout"` + + AppConnectIssuer *string `json:"appconnectIssuer,omitempty"` + AppConnectPrivateKey *string `json:"appconnectPrivateKey,omitempty"` + AppId *string `json:"appId,omitempty"` + Url *string `json:"url,omitempty"` + Username *string `json:"username,omitempty"` + Password *string `json:"password,omitempty"` + Bucket *string `json:"bucket,omitempty"` + Region *string `json:"region,omitempty"` + AccessKey *string `json:"access_key,omitempty"` + SecretKey *string `json:"secret_key,omitempty"` + Prefix *string `json:"prefix,omitempty"` + ClientEmail *string `json:"client_email,omitempty"` + PrivateKey *string `json:"private_key,omitempty"` +} + +func (s *ProjectSymbolSourcesService) Update(ctx context.Context, organizationSlug string, projectSlug string, symbolSourceId string, params *UpdateProjectSymbolSourceParams) (*ProjectSymbolSource, *Response, error) { + u := "0/projects/" + organizationSlug + "/" + projectSlug + "/symbol-sources/" + u, err := addQuery(u, &ProjectSymbolSourceQueryParams{ + ID: String(symbolSourceId), + }) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(http.MethodPut, u, params) + if err != nil { + return nil, nil, err + } + + filter := &ProjectSymbolSource{} + resp, err := s.client.Do(ctx, req, filter) + if err != nil { + return nil, resp, err + } + return filter, resp, nil +} + +func (s *ProjectSymbolSourcesService) Delete(ctx context.Context, organizationSlug string, projectSlug string, symbolSourceId string) (*Response, error) { + u := "0/projects/" + organizationSlug + "/" + projectSlug + "/symbol-sources/" + u, err := addQuery(u, &ProjectSymbolSourceQueryParams{ + ID: String(symbolSourceId), + }) + if err != nil { + return nil, err + } + + req, err := s.client.NewRequest(http.MethodDelete, u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/sentry/project_symbol_sources_test.go b/sentry/project_symbol_sources_test.go new file mode 100644 index 0000000..3ca3d64 --- /dev/null +++ b/sentry/project_symbol_sources_test.go @@ -0,0 +1,259 @@ +package sentry + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProjectSymbolSourcesService_List(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/organization_slug/project_slug/symbol-sources/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodGet, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[ + { + "id": "27c5692e-de41-4087-bc14-74ed0fa421ba", + "name": "s3", + "bucket": "bucket", + "region": "us-east-2", + "access_key": "access_key", + "type": "s3", + "layout": { + "casing": "default", + "type": "native" + }, + "secret_key": { + "hidden-secret": true + } + }, + { + "private_key": { + "hidden-secret": true + }, + "id": "f9df862d-45f7-496c-bf9b-ecade4c9f136", + "layout": { + "type": "native", + "casing": "default" + }, + "name": "gcs", + "bucket": "gcs-bucket-name", + "client_email": "test@example.com", + "type": "gcs" + }, + { + "id": "1ccb6083-91ac-4394-a276-40fe0bb10ece", + "name": "http", + "url": "https://example.com", + "layout": { + "type": "native", + "casing": "default" + }, + "username": "admin", + "password": { + "hidden-secret": true + }, + "type": "http" + } + ]`) + }) + + ctx := context.Background() + sources, _, err := client.ProjectSymbolSources.List(ctx, "organization_slug", "project_slug", nil) + assert.NoError(t, err) + + expected := []*ProjectSymbolSource{ + { + ID: String("27c5692e-de41-4087-bc14-74ed0fa421ba"), + Type: String("s3"), + Name: String("s3"), + Layout: &ProjectSymbolSourceLayout{ + Type: String("native"), + Casing: String("default"), + }, + Bucket: String("bucket"), + Region: String("us-east-2"), + AccessKey: String("access_key"), + SecretKey: &ProjectSymbolSourceHiddenSecret{ + HiddenSecret: Bool(true), + }, + }, + { + ID: String("f9df862d-45f7-496c-bf9b-ecade4c9f136"), + Type: String("gcs"), + Name: String("gcs"), + Layout: &ProjectSymbolSourceLayout{ + Type: String("native"), + Casing: String("default"), + }, + Bucket: String("gcs-bucket-name"), + ClientEmail: String("test@example.com"), + PrivateKey: &ProjectSymbolSourceHiddenSecret{ + HiddenSecret: Bool(true), + }, + }, + { + ID: String("1ccb6083-91ac-4394-a276-40fe0bb10ece"), + Type: String("http"), + Name: String("http"), + Layout: &ProjectSymbolSourceLayout{ + Type: String("native"), + Casing: String("default"), + }, + Url: String("https://example.com"), + Username: String("admin"), + Password: &ProjectSymbolSourceHiddenSecret{ + HiddenSecret: Bool(true), + }, + }, + } + assert.Equal(t, expected, sources) +} + +func TestProjectSymbolSourcesService_Create(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/organization_slug/project_slug/symbol-sources/", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, http.MethodPost, r) + assertPostJSON(t, map[string]interface{}{ + "name": "s3", + "bucket": "bucket", + "region": "us-east-2", + "access_key": "access_key", + "type": "s3", + "layout": map[string]interface{}{ + "casing": "default", + "type": "native", + }, + "secret_key": "secret_key", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "id": "27c5692e-de41-4087-bc14-74ed0fa421ba", + "name": "s3", + "bucket": "bucket", + "region": "us-east-2", + "access_key": "access_key", + "type": "s3", + "layout": { + "casing": "default", + "type": "native" + }, + "secret_key": { + "hidden-secret": true + } + }`) + }) + + ctx := context.Background() + params := &CreateProjectSymbolSourceParams{ + Type: String("s3"), + Name: String("s3"), + Layout: &ProjectSymbolSourceLayout{ + Type: String("native"), + Casing: String("default"), + }, + Bucket: String("bucket"), + Region: String("us-east-2"), + AccessKey: String("access_key"), + SecretKey: String("secret_key"), + } + source, _, err := client.ProjectSymbolSources.Create(ctx, "organization_slug", "project_slug", params) + assert.NoError(t, err) + + expected := &ProjectSymbolSource{ + ID: String("27c5692e-de41-4087-bc14-74ed0fa421ba"), + Type: String("s3"), + Name: String("s3"), + Layout: &ProjectSymbolSourceLayout{ + Type: String("native"), + Casing: String("default"), + }, + Bucket: String("bucket"), + Region: String("us-east-2"), + AccessKey: String("access_key"), + SecretKey: &ProjectSymbolSourceHiddenSecret{ + HiddenSecret: Bool(true), + }, + } + assert.Equal(t, expected, source) +} + +func TestProjectSymbolSourcesService_Update(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/api/0/projects/organization_slug/project_slug/symbol-sources/", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "27c5692e-de41-4087-bc14-74ed0fa421ba", r.URL.Query().Get("id")) + assertMethod(t, http.MethodPut, r) + assertPostJSON(t, map[string]interface{}{ + "id": "27c5692e-de41-4087-bc14-74ed0fa421ba", + "name": "s3", + "bucket": "bucket", + "region": "us-east-2", + "access_key": "access_key", + "type": "s3", + "layout": map[string]interface{}{ + "casing": "default", + "type": "native", + }, + "secret_key": "secret_key", + }, r) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ + "id": "27c5692e-de41-4087-bc14-74ed0fa421ba", + "name": "s3", + "bucket": "bucket", + "region": "us-east-2", + "access_key": "access_key", + "type": "s3", + "layout": { + "casing": "default", + "type": "native" + }, + "secret_key": { + "hidden-secret": true + } + }`) + }) + + ctx := context.Background() + params := &UpdateProjectSymbolSourceParams{ + ID: String("27c5692e-de41-4087-bc14-74ed0fa421ba"), + Type: String("s3"), + Name: String("s3"), + Layout: &ProjectSymbolSourceLayout{ + Type: String("native"), + Casing: String("default"), + }, + Bucket: String("bucket"), + Region: String("us-east-2"), + AccessKey: String("access_key"), + SecretKey: String("secret_key"), + } + source, _, err := client.ProjectSymbolSources.Update(ctx, "organization_slug", "project_slug", "27c5692e-de41-4087-bc14-74ed0fa421ba", params) + assert.NoError(t, err) + + expected := &ProjectSymbolSource{ + ID: String("27c5692e-de41-4087-bc14-74ed0fa421ba"), + Type: String("s3"), + Name: String("s3"), + Layout: &ProjectSymbolSourceLayout{ + Type: String("native"), + Casing: String("default"), + }, + Bucket: String("bucket"), + Region: String("us-east-2"), + AccessKey: String("access_key"), + SecretKey: &ProjectSymbolSourceHiddenSecret{ + HiddenSecret: Bool(true), + }, + } + assert.Equal(t, expected, source) +} diff --git a/sentry/sentry.go b/sentry/sentry.go index 19578e9..32b1244 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -63,6 +63,7 @@ type Client struct { ProjectOwnerships *ProjectOwnershipsService ProjectPlugins *ProjectPluginsService Projects *ProjectsService + ProjectSymbolSources *ProjectSymbolSourcesService ReleaseDeployments *ReleaseDeploymentsService SpikeProtections *SpikeProtectionsService TeamMembers *TeamMembersService @@ -103,6 +104,7 @@ func NewClient(httpClient *http.Client) *Client { c.ProjectOwnerships = (*ProjectOwnershipsService)(&c.common) c.ProjectPlugins = (*ProjectPluginsService)(&c.common) c.Projects = (*ProjectsService)(&c.common) + c.ProjectSymbolSources = (*ProjectSymbolSourcesService)(&c.common) c.ReleaseDeployments = (*ReleaseDeploymentsService)(&c.common) c.SpikeProtections = (*SpikeProtectionsService)(&c.common) c.TeamMembers = (*TeamMembersService)(&c.common) From b3fa073010c8fbf460dda442643be3890d8bc53f Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Wed, 13 Dec 2023 09:52:45 +0000 Subject: [PATCH 33/41] ref: convert project id into json.Number --- sentry/project_keys.go | 3 ++- sentry/project_keys_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sentry/project_keys.go b/sentry/project_keys.go index a064d59..09839c0 100644 --- a/sentry/project_keys.go +++ b/sentry/project_keys.go @@ -2,6 +2,7 @@ package sentry import ( "context" + "encoding/json" "fmt" "time" ) @@ -30,7 +31,7 @@ type ProjectKey struct { Label string `json:"label"` Public string `json:"public"` Secret string `json:"secret"` - ProjectID int `json:"projectId"` + ProjectID json.Number `json:"projectId"` IsActive bool `json:"isActive"` RateLimit *ProjectKeyRateLimit `json:"rateLimit"` DSN ProjectKeyDSN `json:"dsn"` diff --git a/sentry/project_keys_test.go b/sentry/project_keys_test.go index 05dd642..e87a881 100644 --- a/sentry/project_keys_test.go +++ b/sentry/project_keys_test.go @@ -63,7 +63,7 @@ func TestProjectKeysService_List(t *testing.T) { Label: "Fabulous Key", Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", Secret: "a07dcd97aa56481f82aeabaed43ca448", - ProjectID: 2, + ProjectID: json.Number("2"), IsActive: true, DSN: ProjectKeyDSN{ Secret: "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", @@ -135,7 +135,7 @@ func TestProjectKeysService_Create(t *testing.T) { Label: "Fabulous Key", Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", Secret: "a07dcd97aa56481f82aeabaed43ca448", - ProjectID: 2, + ProjectID: json.Number("2"), IsActive: true, DSN: ProjectKeyDSN{ Secret: "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", @@ -206,7 +206,7 @@ func TestProjectKeysService_Update(t *testing.T) { Label: "Fabulous Key", Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", Secret: "a07dcd97aa56481f82aeabaed43ca448", - ProjectID: 2, + ProjectID: json.Number("2"), IsActive: true, DSN: ProjectKeyDSN{ Secret: "https://cfc7b0341c6e4f6ea1a9d256a30dba00:a07dcd97aa56481f82aeabaed43ca448@sentry.io/2", @@ -289,7 +289,7 @@ func TestProjectKeysService_Update_RateLimit(t *testing.T) { Label: "Fabulous Key", Public: "cfc7b0341c6e4f6ea1a9d256a30dba00", Secret: "a07dcd97aa56481f82aeabaed43ca448", - ProjectID: 2, + ProjectID: json.Number("2"), IsActive: true, RateLimit: &rateLimit, DSN: ProjectKeyDSN{ From ce941c8db84abb04d057137d7305baadd50c67d4 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Wed, 13 Dec 2023 21:38:03 +0000 Subject: [PATCH 34/41] ref: convert issue alert frequency to json number --- sentry/issue_alerts.go | 3 ++- sentry/issue_alerts_test.go | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/sentry/issue_alerts.go b/sentry/issue_alerts.go index 33e59db..866f507 100644 --- a/sentry/issue_alerts.go +++ b/sentry/issue_alerts.go @@ -2,6 +2,7 @@ package sentry import ( "context" + "encoding/json" "errors" "fmt" "time" @@ -16,7 +17,7 @@ type IssueAlert struct { Actions []*IssueAlertAction `json:"actions,omitempty"` ActionMatch *string `json:"actionMatch,omitempty"` FilterMatch *string `json:"filterMatch,omitempty"` - Frequency *int `json:"frequency,omitempty"` + Frequency *json.Number `json:"frequency,omitempty"` Name *string `json:"name,omitempty"` DateCreated *time.Time `json:"dateCreated,omitempty"` Owner *string `json:"owner,omitempty"` diff --git a/sentry/issue_alerts_test.go b/sentry/issue_alerts_test.go index e8b759c..5aa2e63 100644 --- a/sentry/issue_alerts_test.go +++ b/sentry/issue_alerts_test.go @@ -57,7 +57,7 @@ func TestIssueAlertsService_List(t *testing.T) { ID: String("12345"), ActionMatch: String("any"), Environment: String("production"), - Frequency: Int(30), + Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), Conditions: []*IssueAlertCondition{ { @@ -326,7 +326,7 @@ func TestIssueAlertsService_Get(t *testing.T) { }, ActionMatch: String("any"), FilterMatch: String("any"), - Frequency: Int(30), + Frequency: JsonNumber(json.Number("30")), Name: String("My Rule Name"), DateCreated: Time(mustParseTime("2022-05-23T19:54:30.860115Z")), Owner: String("team:1322366"), @@ -404,7 +404,7 @@ func TestIssueAlertsService_Create(t *testing.T) { params := &IssueAlert{ ActionMatch: String("all"), Environment: String("production"), - Frequency: Int(30), + Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), Conditions: []*IssueAlertCondition{ { @@ -433,7 +433,7 @@ func TestIssueAlertsService_Create(t *testing.T) { ID: String("123456"), ActionMatch: String("all"), Environment: String("production"), - Frequency: Int(30), + Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), Conditions: []*IssueAlertCondition{ { @@ -532,7 +532,7 @@ func TestIssueAlertsService_CreateWithAsyncTask(t *testing.T) { params := &IssueAlert{ ActionMatch: String("all"), Environment: String("production"), - Frequency: Int(30), + Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), Conditions: []*IssueAlertCondition{ { @@ -561,7 +561,7 @@ func TestIssueAlertsService_CreateWithAsyncTask(t *testing.T) { ID: String("123456"), ActionMatch: String("all"), Environment: String("production"), - Frequency: Int(30), + Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), Conditions: []*IssueAlertCondition{ { @@ -596,7 +596,7 @@ func TestIssueAlertsService_Update(t *testing.T) { ActionMatch: String("all"), FilterMatch: String("any"), Environment: String("staging"), - Frequency: Int(30), + Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), Conditions: []*IssueAlertCondition{ { @@ -709,7 +709,7 @@ func TestIssueAlertsService_Update(t *testing.T) { ID: String("12345"), ActionMatch: String("any"), Environment: String("staging"), - Frequency: Int(30), + Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), Conditions: []*IssueAlertCondition{ { From 288c5a250675f8d8c65df1f6767ee45797732487 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Thu, 14 Dec 2023 00:36:49 +0000 Subject: [PATCH 35/41] ref: issue alert conditions, filters, actions use map[string]interface{} --- sentry/issue_alerts.go | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/sentry/issue_alerts.go b/sentry/issue_alerts.go index 866f507..2b1604b 100644 --- a/sentry/issue_alerts.go +++ b/sentry/issue_alerts.go @@ -11,20 +11,20 @@ import ( // IssueAlert represents an issue alert configured for this project. // https://github.com/getsentry/sentry/blob/22.5.0/src/sentry/api/serializers/models/rule.py#L131-L155 type IssueAlert struct { - ID *string `json:"id,omitempty"` - Conditions []*IssueAlertCondition `json:"conditions,omitempty"` - Filters []*IssueAlertFilter `json:"filters,omitempty"` - Actions []*IssueAlertAction `json:"actions,omitempty"` - ActionMatch *string `json:"actionMatch,omitempty"` - FilterMatch *string `json:"filterMatch,omitempty"` - Frequency *json.Number `json:"frequency,omitempty"` - Name *string `json:"name,omitempty"` - DateCreated *time.Time `json:"dateCreated,omitempty"` - Owner *string `json:"owner,omitempty"` - CreatedBy *IssueAlertCreatedBy `json:"createdBy,omitempty"` - Environment *string `json:"environment,omitempty"` - Projects []string `json:"projects,omitempty"` - TaskUUID *string `json:"uuid,omitempty"` // This is actually the UUID of the async task that can be spawned to create the rule + ID *string `json:"id,omitempty"` + Conditions []map[string]interface{} `json:"conditions,omitempty"` + Filters []map[string]interface{} `json:"filters,omitempty"` + Actions []map[string]interface{} `json:"actions,omitempty"` + ActionMatch *string `json:"actionMatch,omitempty"` + FilterMatch *string `json:"filterMatch,omitempty"` + Frequency *json.Number `json:"frequency,omitempty"` + Name *string `json:"name,omitempty"` + DateCreated *time.Time `json:"dateCreated,omitempty"` + Owner *string `json:"owner,omitempty"` + CreatedBy *IssueAlertCreatedBy `json:"createdBy,omitempty"` + Environment *string `json:"environment,omitempty"` + Projects []string `json:"projects,omitempty"` + TaskUUID *string `json:"uuid,omitempty"` // This is actually the UUID of the async task that can be spawned to create the rule } // IssueAlertCreatedBy for defining the rule creator. @@ -34,15 +34,6 @@ type IssueAlertCreatedBy struct { Email *string `json:"email,omitempty"` } -// IssueAlertCondition for defining conditions. -type IssueAlertCondition map[string]interface{} - -// IssueAlertAction for defining actions. -type IssueAlertAction map[string]interface{} - -// IssueAlertFilter for defining actions. -type IssueAlertFilter map[string]interface{} - // IssueAlertTaskDetail represents the inline struct Sentry defines for task details // https://github.com/getsentry/sentry/blob/22.5.0/src/sentry/api/endpoints/project_rule_task_details.py#L29 type IssueAlertTaskDetail struct { From 87a326682444a0cd8e5065f260bbb56616d363dc Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sat, 16 Dec 2023 00:55:21 +0000 Subject: [PATCH 36/41] fix: targetIdentifier can be an integer --- sentry/issue_alerts_test.go | 36 ++++++++--------- sentry/metric_alerts.go | 20 ++++----- sentry/metric_alerts_test.go | 14 +++---- sentry/project_inbound_data_filters_test.go | 2 +- sentry/types.go | 45 +++++++++++++++++++-- 5 files changed, 77 insertions(+), 40 deletions(-) diff --git a/sentry/issue_alerts_test.go b/sentry/issue_alerts_test.go index 5aa2e63..1bcc350 100644 --- a/sentry/issue_alerts_test.go +++ b/sentry/issue_alerts_test.go @@ -59,7 +59,7 @@ func TestIssueAlertsService_List(t *testing.T) { Environment: String("production"), Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), - Conditions: []*IssueAlertCondition{ + Conditions: []map[string]interface{}{ { "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", "name": "An issue is first seen", @@ -67,7 +67,7 @@ func TestIssueAlertsService_List(t *testing.T) { "interval": "1h", }, }, - Actions: []*IssueAlertAction{ + Actions: []map[string]interface{}{ { "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", @@ -221,7 +221,7 @@ func TestIssueAlertsService_Get(t *testing.T) { expected := &IssueAlert{ ID: String("11185158"), - Conditions: []*IssueAlertCondition{ + Conditions: []map[string]interface{}{ { "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", "name": "A new issue is created", @@ -256,7 +256,7 @@ func TestIssueAlertsService_Get(t *testing.T) { "name": "The issue affects more than 100.0 percent of sessions in 1h", }, }, - Filters: []*IssueAlertFilter{ + Filters: []map[string]interface{}{ { "comparison_type": "older", "time": "minute", @@ -300,7 +300,7 @@ func TestIssueAlertsService_Get(t *testing.T) { "name": "The event's level is equal to fatal", }, }, - Actions: []*IssueAlertAction{ + Actions: []map[string]interface{}{ { "targetType": "IssueOwners", "id": "sentry.mail.actions.NotifyEmailAction", @@ -406,7 +406,7 @@ func TestIssueAlertsService_Create(t *testing.T) { Environment: String("production"), Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), - Conditions: []*IssueAlertCondition{ + Conditions: []map[string]interface{}{ { "interval": "1h", "name": "The issue is seen more than 10 times in 1h", @@ -414,7 +414,7 @@ func TestIssueAlertsService_Create(t *testing.T) { "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition", }, }, - Actions: []*IssueAlertAction{ + Actions: []map[string]interface{}{ { "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", @@ -435,7 +435,7 @@ func TestIssueAlertsService_Create(t *testing.T) { Environment: String("production"), Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), - Conditions: []*IssueAlertCondition{ + Conditions: []map[string]interface{}{ { "interval": "1h", "name": "The issue is seen more than 10 times in 1h", @@ -443,7 +443,7 @@ func TestIssueAlertsService_Create(t *testing.T) { "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition", }, }, - Actions: []*IssueAlertAction{ + Actions: []map[string]interface{}{ { "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", @@ -534,7 +534,7 @@ func TestIssueAlertsService_CreateWithAsyncTask(t *testing.T) { Environment: String("production"), Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), - Conditions: []*IssueAlertCondition{ + Conditions: []map[string]interface{}{ { "interval": "1h", "name": "The issue is seen more than 10 times in 1h", @@ -542,7 +542,7 @@ func TestIssueAlertsService_CreateWithAsyncTask(t *testing.T) { "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition", }, }, - Actions: []*IssueAlertAction{ + Actions: []map[string]interface{}{ { "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", @@ -563,7 +563,7 @@ func TestIssueAlertsService_CreateWithAsyncTask(t *testing.T) { Environment: String("production"), Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), - Conditions: []*IssueAlertCondition{ + Conditions: []map[string]interface{}{ { "interval": "1h", "name": "The issue is seen more than 10 times in 1h", @@ -571,7 +571,7 @@ func TestIssueAlertsService_CreateWithAsyncTask(t *testing.T) { "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition", }, }, - Actions: []*IssueAlertAction{ + Actions: []map[string]interface{}{ { "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", @@ -598,14 +598,14 @@ func TestIssueAlertsService_Update(t *testing.T) { Environment: String("staging"), Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), - Conditions: []*IssueAlertCondition{ + Conditions: []map[string]interface{}{ { "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition", "value": 500, "interval": "1h", }, }, - Actions: []*IssueAlertAction{ + Actions: []map[string]interface{}{ { "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", @@ -615,7 +615,7 @@ func TestIssueAlertsService_Update(t *testing.T) { "workspace": "1234", }, }, - Filters: []*IssueAlertFilter{ + Filters: []map[string]interface{}{ { "id": "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter", "name": "The issue has happened at least 4 times", @@ -711,13 +711,13 @@ func TestIssueAlertsService_Update(t *testing.T) { Environment: String("staging"), Frequency: JsonNumber(json.Number("30")), Name: String("Notify errors"), - Conditions: []*IssueAlertCondition{ + Conditions: []map[string]interface{}{ { "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", "name": "An issue is first seen", }, }, - Actions: []*IssueAlertAction{ + Actions: []map[string]interface{}{ { "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction", "name": "Send a notification to the Dummy Slack workspace to #dummy-channel and show tags [environment] in notification", diff --git a/sentry/metric_alerts.go b/sentry/metric_alerts.go index 68428b3..ef9acb2 100644 --- a/sentry/metric_alerts.go +++ b/sentry/metric_alerts.go @@ -51,16 +51,16 @@ type MetricAlertTrigger struct { // MetricAlertTriggerAction represents a metric alert trigger action. // https://github.com/getsentry/sentry/blob/22.5.0/src/sentry/api/serializers/models/alert_rule_trigger_action.py#L42-L66 type MetricAlertTriggerAction struct { - ID *string `json:"id,omitempty"` - AlertRuleTriggerID *string `json:"alertRuleTriggerId,omitempty"` - Type *string `json:"type,omitempty"` - TargetType *string `json:"targetType,omitempty"` - TargetIdentifier *string `json:"targetIdentifier,omitempty"` - InputChannelID *string `json:"inputChannelId,omitempty"` - IntegrationID *int `json:"integrationId,omitempty"` - SentryAppID *string `json:"sentryAppId,omitempty"` - DateCreated *time.Time `json:"dateCreated,omitempty"` - Description *string `json:"desc,omitempty"` + ID *string `json:"id,omitempty"` + AlertRuleTriggerID *string `json:"alertRuleTriggerId,omitempty"` + Type *string `json:"type,omitempty"` + TargetType *string `json:"targetType,omitempty"` + TargetIdentifier *Int64OrString `json:"targetIdentifier,omitempty"` + InputChannelID *string `json:"inputChannelId,omitempty"` + IntegrationID *int `json:"integrationId,omitempty"` + SentryAppID *string `json:"sentryAppId,omitempty"` + DateCreated *time.Time `json:"dateCreated,omitempty"` + Description *string `json:"desc,omitempty"` } // List Alert Rules configured for a project diff --git a/sentry/metric_alerts_test.go b/sentry/metric_alerts_test.go index e4c40a6..221aabb 100644 --- a/sentry/metric_alerts_test.go +++ b/sentry/metric_alerts_test.go @@ -95,7 +95,7 @@ func TestMetricAlertService_List(t *testing.T) { AlertRuleTriggerID: String("12345"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: String("#alert-rule-alerts"), + TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, InputChannelID: String("C038NF00X4F"), IntegrationID: Int(123), DateCreated: Time(mustParseTime("2022-04-07T16:46:49.154638Z")), @@ -195,7 +195,7 @@ func TestMetricAlertService_Get(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: String("#alert-rule-alerts"), + TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(111), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -339,7 +339,7 @@ func TestMetricAlertsService_CreateWithAsyncTask(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: String("#alert-rule-alerts"), + TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(123), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -381,7 +381,7 @@ func TestMetricAlertsService_CreateWithAsyncTask(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: String("#alert-rule-alerts"), + TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(111), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -475,7 +475,7 @@ func TestMetricAlertService_Create(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: String("#alert-rule-alerts"), + TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(123), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -517,7 +517,7 @@ func TestMetricAlertService_Create(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: String("#alert-rule-alerts"), + TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(111), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -668,7 +668,7 @@ func TestMetricAlertService_Update(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: String("#alert-rule-alerts"), + TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(111), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), diff --git a/sentry/project_inbound_data_filters_test.go b/sentry/project_inbound_data_filters_test.go index ed600a5..cce96d2 100644 --- a/sentry/project_inbound_data_filters_test.go +++ b/sentry/project_inbound_data_filters_test.go @@ -57,7 +57,7 @@ func TestProjectInboundDataFiltersService_List(t *testing.T) { }, { ID: "legacy-browsers", - Active: BoolOrStringSlice{IsBool: false, SliceVal: []string{"ie_pre_9"}}, + Active: BoolOrStringSlice{IsStringSlice: true, StringSliceVal: []string{"ie_pre_9"}}, }, { ID: "localhost", diff --git a/sentry/types.go b/sentry/types.go index b372ef0..ebc6c4f 100644 --- a/sentry/types.go +++ b/sentry/types.go @@ -8,9 +8,10 @@ import ( // BoolOrStringSlice is a type that can be unmarshaled from either a bool or a // string slice. type BoolOrStringSlice struct { - IsBool bool - BoolVal bool - SliceVal []string + IsBool bool + IsStringSlice bool + BoolVal bool + StringSliceVal []string } var _ json.Unmarshaler = (*BoolOrStringSlice)(nil) @@ -21,6 +22,7 @@ func (bos *BoolOrStringSlice) UnmarshalJSON(data []byte) error { var boolVal bool if err := json.Unmarshal(data, &boolVal); err == nil { bos.IsBool = true + bos.IsStringSlice = false bos.BoolVal = boolVal return nil } @@ -29,10 +31,45 @@ func (bos *BoolOrStringSlice) UnmarshalJSON(data []byte) error { var sliceVal []string if err := json.Unmarshal(data, &sliceVal); err == nil { bos.IsBool = false - bos.SliceVal = sliceVal + bos.IsStringSlice = true + bos.StringSliceVal = sliceVal return nil } // If neither worked, return an error return fmt.Errorf("unable to unmarshal as bool or string slice: %s", string(data)) } + +// Int64OrString is a type that can be unmarshaled from either an int64 or a +// string. +type Int64OrString struct { + IsInt64 bool + IsString bool + Int64Val int64 + String string +} + +var _ json.Unmarshaler = (*Int64OrString)(nil) + +func (ios *Int64OrString) UnmarshalJSON(data []byte) error { + // Try to unmarshal as an int64 + var int64Val int64 + if err := json.Unmarshal(data, &int64Val); err == nil { + ios.IsInt64 = true + ios.IsString = false + ios.Int64Val = int64Val + return nil + } + + // Try to unmarshal as a string + var stringVal string + if err := json.Unmarshal(data, &stringVal); err == nil { + ios.IsInt64 = false + ios.IsString = true + ios.String = stringVal + return nil + } + + // If neither worked, return an error + return fmt.Errorf("unable to unmarshal as int64 or string: %s", string(data)) +} From a0eb7e65644508e312030ad5e52c8d925cf13300 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sat, 16 Dec 2023 01:05:03 +0000 Subject: [PATCH 37/41] fix: authassignment is now a string --- sentry/project_ownerships.go | 10 +++++----- sentry/project_ownerships_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sentry/project_ownerships.go b/sentry/project_ownerships.go index 5078705..b0c2397 100644 --- a/sentry/project_ownerships.go +++ b/sentry/project_ownerships.go @@ -13,7 +13,7 @@ type ProjectOwnership struct { DateCreated time.Time `json:"dateCreated"` LastUpdated time.Time `json:"lastUpdated"` IsActive bool `json:"isActive"` - AutoAssignment bool `json:"autoAssignment"` + AutoAssignment string `json:"autoAssignment"` CodeownersAutoSync *bool `json:"codeownersAutoSync,omitempty"` } @@ -39,10 +39,10 @@ func (s *ProjectOwnershipsService) Get(ctx context.Context, organizationSlug str // CreateProjectParams are the parameters for ProjectOwnershipService.Update. type UpdateProjectOwnershipParams struct { - Raw string `json:"raw,omitempty"` - FallThrough *bool `json:"fallthrough,omitempty"` - AutoAssignment *bool `json:"autoAssignment,omitempty"` - CodeownersAutoSync *bool `json:"codeownersAutoSync,omitempty"` + Raw string `json:"raw,omitempty"` + FallThrough *bool `json:"fallthrough,omitempty"` + AutoAssignment *string `json:"autoAssignment,omitempty"` + CodeownersAutoSync *bool `json:"codeownersAutoSync,omitempty"` } // Update a Project's Ownership configuration diff --git a/sentry/project_ownerships_test.go b/sentry/project_ownerships_test.go index f3583c0..979f775 100644 --- a/sentry/project_ownerships_test.go +++ b/sentry/project_ownerships_test.go @@ -22,7 +22,7 @@ func TestProjectOwnershipsService_Get(t *testing.T) { "dateCreated": "2021-11-18T13:09:16.819818Z", "lastUpdated": "2022-03-01T14:00:31.317734Z", "isActive": true, - "autoAssignment": true, + "autoAssignment": "Auto Assign to Issue Owner", "codeownersAutoSync": null }`) }) @@ -35,7 +35,7 @@ func TestProjectOwnershipsService_Get(t *testing.T) { Raw: "# assign issues to the product team, no matter the area\nurl:https://example.com/areas/*/*/products/* #product-team", FallThrough: false, IsActive: true, - AutoAssignment: true, + AutoAssignment: "Auto Assign to Issue Owner", CodeownersAutoSync: nil, DateCreated: mustParseTime("2021-11-18T13:09:16.819818Z"), LastUpdated: mustParseTime("2022-03-01T14:00:31.317734Z"), @@ -60,7 +60,7 @@ func TestProjectOwnershipsService_Update(t *testing.T) { "dateCreated": "2021-11-18T13:09:16.819818Z", "lastUpdated": "2022-03-01T14:00:31.317734Z", "isActive": true, - "autoAssignment": true, + "autoAssignment": "Auto Assign to Issue Owner", "codeownersAutoSync": null }`) }) @@ -75,7 +75,7 @@ func TestProjectOwnershipsService_Update(t *testing.T) { Raw: "# assign issues to the product team, no matter the area\nurl:https://example.com/areas/*/*/products/* #product-team", FallThrough: false, IsActive: true, - AutoAssignment: true, + AutoAssignment: "Auto Assign to Issue Owner", CodeownersAutoSync: nil, DateCreated: mustParseTime("2021-11-18T13:09:16.819818Z"), LastUpdated: mustParseTime("2022-03-01T14:00:31.317734Z"), From 6789d91d01d54d76d63ec96e5923c03a84a531d5 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sat, 16 Dec 2023 01:49:34 +0000 Subject: [PATCH 38/41] define custom marshaller --- sentry/metric_alerts_test.go | 14 +++++++------- sentry/types.go | 26 +++++++++++++++++++++----- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/sentry/metric_alerts_test.go b/sentry/metric_alerts_test.go index 221aabb..8412700 100644 --- a/sentry/metric_alerts_test.go +++ b/sentry/metric_alerts_test.go @@ -95,7 +95,7 @@ func TestMetricAlertService_List(t *testing.T) { AlertRuleTriggerID: String("12345"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, + TargetIdentifier: &Int64OrString{IsString: true, StringVal: "#alert-rule-alerts"}, InputChannelID: String("C038NF00X4F"), IntegrationID: Int(123), DateCreated: Time(mustParseTime("2022-04-07T16:46:49.154638Z")), @@ -195,7 +195,7 @@ func TestMetricAlertService_Get(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, + TargetIdentifier: &Int64OrString{IsString: true, StringVal: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(111), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -339,7 +339,7 @@ func TestMetricAlertsService_CreateWithAsyncTask(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, + TargetIdentifier: &Int64OrString{IsString: true, StringVal: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(123), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -381,7 +381,7 @@ func TestMetricAlertsService_CreateWithAsyncTask(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, + TargetIdentifier: &Int64OrString{IsString: true, StringVal: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(111), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -475,7 +475,7 @@ func TestMetricAlertService_Create(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, + TargetIdentifier: &Int64OrString{IsString: true, StringVal: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(123), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -517,7 +517,7 @@ func TestMetricAlertService_Create(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, + TargetIdentifier: &Int64OrString{IsString: true, StringVal: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(111), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), @@ -668,7 +668,7 @@ func TestMetricAlertService_Update(t *testing.T) { AlertRuleTriggerID: String("56789"), Type: String("slack"), TargetType: String("specific"), - TargetIdentifier: &Int64OrString{IsString: true, String: "#alert-rule-alerts"}, + TargetIdentifier: &Int64OrString{IsString: true, StringVal: "#alert-rule-alerts"}, InputChannelID: String("C0XXXFKLXXX"), IntegrationID: Int(111), DateCreated: Time(mustParseTime("2022-04-15T15:06:01.087054Z")), diff --git a/sentry/types.go b/sentry/types.go index ebc6c4f..937caab 100644 --- a/sentry/types.go +++ b/sentry/types.go @@ -15,6 +15,7 @@ type BoolOrStringSlice struct { } var _ json.Unmarshaler = (*BoolOrStringSlice)(nil) +var _ json.Marshaler = (*BoolOrStringSlice)(nil) // UnmarshalJSON implements json.Unmarshaler. func (bos *BoolOrStringSlice) UnmarshalJSON(data []byte) error { @@ -40,16 +41,24 @@ func (bos *BoolOrStringSlice) UnmarshalJSON(data []byte) error { return fmt.Errorf("unable to unmarshal as bool or string slice: %s", string(data)) } +func (bos BoolOrStringSlice) MarshalJSON() ([]byte, error) { + if bos.IsBool { + return json.Marshal(bos.BoolVal) + } + return json.Marshal(bos.StringSliceVal) +} + // Int64OrString is a type that can be unmarshaled from either an int64 or a // string. type Int64OrString struct { - IsInt64 bool - IsString bool - Int64Val int64 - String string + IsInt64 bool + IsString bool + Int64Val int64 + StringVal string } var _ json.Unmarshaler = (*Int64OrString)(nil) +var _ json.Marshaler = (*Int64OrString)(nil) func (ios *Int64OrString) UnmarshalJSON(data []byte) error { // Try to unmarshal as an int64 @@ -66,10 +75,17 @@ func (ios *Int64OrString) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &stringVal); err == nil { ios.IsInt64 = false ios.IsString = true - ios.String = stringVal + ios.StringVal = stringVal return nil } // If neither worked, return an error return fmt.Errorf("unable to unmarshal as int64 or string: %s", string(data)) } + +func (ios Int64OrString) MarshalJSON() ([]byte, error) { + if ios.IsInt64 { + return json.Marshal(ios.Int64Val) + } + return json.Marshal(ios.StringVal) +} From 01d0536dcbc6825143fbcd5b28a3accd024fea63 Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Sat, 16 Dec 2023 02:10:25 +0000 Subject: [PATCH 39/41] feat: add cursor pagination to list teams endpoint --- sentry/teams.go | 7 ++++++- sentry/teams_test.go | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/sentry/teams.go b/sentry/teams.go index 5576bae..30e7b55 100644 --- a/sentry/teams.go +++ b/sentry/teams.go @@ -29,8 +29,13 @@ type TeamsService service // List returns a list of teams bound to an organization. // https://docs.sentry.io/api/teams/list-an-organizations-teams/ -func (s *TeamsService) List(ctx context.Context, organizationSlug string) ([]*Team, *Response, error) { +func (s *TeamsService) List(ctx context.Context, organizationSlug string, params *ListCursorParams) ([]*Team, *Response, error) { u := fmt.Sprintf("0/organizations/%v/teams/", organizationSlug) + u, err := addQuery(u, params) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, nil, err diff --git a/sentry/teams_test.go b/sentry/teams_test.go index e26639a..89fad9e 100644 --- a/sentry/teams_test.go +++ b/sentry/teams_test.go @@ -118,7 +118,7 @@ func TestTeamsService_List(t *testing.T) { }) ctx := context.Background() - teams, _, err := client.Teams.List(ctx, "the-interstellar-jurisdiction") + teams, _, err := client.Teams.List(ctx, "the-interstellar-jurisdiction", nil) assert.NoError(t, err) expected := []*Team{ From 0f22970928fb7ff6e10d87b9633836c0f5089afb Mon Sep 17 00:00:00 2001 From: Jian Yuan Lee Date: Fri, 12 Jan 2024 22:48:07 +0000 Subject: [PATCH 40/41] ref: org role list and team role list --- sentry/organization_members.go | 41 ++--- sentry/organization_members_test.go | 85 +++++++--- sentry/organizations.go | 2 + sentry/organizations_test.go | 250 ++++++++++++++++++++++++++++ sentry/roles.go | 24 +++ sentry/teams.go | 3 +- 6 files changed, 354 insertions(+), 51 deletions(-) create mode 100644 sentry/roles.go diff --git a/sentry/organization_members.go b/sentry/organization_members.go index 5c5868a..92a3796 100644 --- a/sentry/organization_members.go +++ b/sentry/organization_members.go @@ -6,31 +6,24 @@ import ( "time" ) -// https://github.com/getsentry/sentry/blob/8b683002ec84f9edd338348500937a480e49e61c/src/sentry/api/serializers/models/role.py#L32 -type RoleListItem struct { - ID string `json:"id"` - Name string `json:"name"` - IsAllowed bool `json:"isAllowed"` -} - // OrganizationMember represents a User's membership to the organization. // https://github.com/getsentry/sentry/blob/22.5.0/src/sentry/api/serializers/models/organization_member/response.py#L57-L69 type OrganizationMember struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - User User `json:"user"` - OrganizationRole string `json:"orgRole"` - OrganizationRoleList []RoleListItem `json:"orgRoleList"` - Pending bool `json:"pending"` - Expired bool `json:"expired"` - Flags map[string]bool `json:"flags"` - DateCreated time.Time `json:"dateCreated"` - InviteStatus string `json:"inviteStatus"` - InviterName *string `json:"inviterName"` - TeamRoleList []RoleListItem `json:"teamRoleList"` - TeamRoles []TeamRole `json:"teamRoles"` - Teams []string `json:"teams"` + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + User User `json:"user"` + OrgRole string `json:"orgRole"` + OrgRoleList []OrganizationRoleListItem `json:"orgRoleList"` + Pending bool `json:"pending"` + Expired bool `json:"expired"` + Flags map[string]bool `json:"flags"` + DateCreated time.Time `json:"dateCreated"` + InviteStatus string `json:"inviteStatus"` + InviterName *string `json:"inviterName"` + TeamRoleList []TeamRoleListItem `json:"teamRoleList"` + TeamRoles []TeamRole `json:"teamRoles"` + Teams []string `json:"teams"` } const ( @@ -104,8 +97,8 @@ func (s *OrganizationMembersService) Create(ctx context.Context, organizationSlu } type TeamRole struct { - TeamSlug string `json:"teamSlug"` - Role string `json:"role"` + TeamSlug string `json:"teamSlug"` + Role *string `json:"role"` } type UpdateOrganizationMemberParams struct { diff --git a/sentry/organization_members_test.go b/sentry/organization_members_test.go index 828e085..e8c90c2 100644 --- a/sentry/organization_members_test.go +++ b/sentry/organization_members_test.go @@ -590,32 +590,57 @@ func TestOrganizationMembersService_Update(t *testing.T) { }, }, }, - OrganizationRole: OrganizationRoleMember, - OrganizationRoleList: []RoleListItem{ + OrgRole: OrganizationRoleMember, + OrgRoleList: []OrganizationRoleListItem{ { - ID: "billing", - Name: "Billing", - IsAllowed: true, + ID: "billing", + Name: "Billing", + Desc: "Can manage subscription and billing details.", + Scopes: []string{"org:billing"}, + IsAllowed: true, + IsRetired: false, + IsGlobal: false, + MinimumTeamRole: "contributor", }, { - ID: "member", - Name: "Member", - IsAllowed: true, + ID: "member", + Name: "Member", + Desc: "Members can view and act on events, as well as view most other data within the organization.", + Scopes: []string{"team:read", "project:releases", "org:read", "event:read", "alerts:write", "member:read", "alerts:read", "event:admin", "project:read", "event:write"}, + IsAllowed: true, + IsRetired: false, + IsGlobal: false, + MinimumTeamRole: "contributor", }, { - ID: "admin", - Name: "Admin", - IsAllowed: true, + ID: "admin", + Name: "Admin", + Desc: "Admin privileges on any teams of which they're a member. They can create new teams and projects, as well as remove teams and projects on which they already hold membership (or all teams, if open membership is enabled). Additionally, they can manage memberships of teams that they are members of. They cannot invite members to the organization.", + Scopes: []string{"team:admin", "org:integrations", "project:admin", "team:read", "project:releases", "org:read", "team:write", "event:read", "alerts:write", "member:read", "alerts:read", "event:admin", "project:read", "event:write", "project:write"}, + IsAllowed: true, + IsRetired: true, + IsGlobal: false, + MinimumTeamRole: "admin", }, { - ID: "manager", - Name: "Manager", - IsAllowed: true, + ID: "manager", + Name: "Manager", + Desc: "Gains admin access on all teams as well as the ability to add and remove members.", + Scopes: []string{"team:admin", "org:integrations", "project:releases", "team:write", "member:read", "org:write", "project:write", "project:admin", "team:read", "org:read", "event:read", "member:write", "alerts:write", "alerts:read", "event:admin", "project:read", "event:write", "member:admin"}, + IsAllowed: true, + IsRetired: false, + IsGlobal: true, + MinimumTeamRole: "admin", }, { - ID: "owner", - Name: "Owner", - IsAllowed: true, + ID: "owner", + Name: "Owner", + Desc: "Unrestricted access to the organization, its data, and its settings. Can add, modify, and delete projects and members, as well as make billing and plan changes.", + Scopes: []string{"team:admin", "org:integrations", "project:releases", "org:admin", "team:write", "member:read", "org:write", "project:write", "project:admin", "team:read", "org:read", "event:read", "member:write", "alerts:write", "org:billing", "alerts:read", "event:admin", "project:read", "event:write", "member:admin"}, + IsAllowed: true, + IsRetired: false, + IsGlobal: true, + MinimumTeamRole: "admin", }, }, Pending: false, @@ -631,26 +656,34 @@ func TestOrganizationMembersService_Update(t *testing.T) { DateCreated: mustParseTime("2021-07-06T21:13:01.120263Z"), InviteStatus: "approved", InviterName: &inviterName, - TeamRoleList: []RoleListItem{ + TeamRoleList: []TeamRoleListItem{ { - ID: "contributor", - Name: "Contributor", - IsAllowed: false, + ID: "contributor", + Name: "Contributor", + Desc: "Contributors can view and act on events, as well as view most other data within the team's projects.", + Scopes: []string{"team:read", "project:releases", "org:read", "event:read", "member:read", "alerts:read", "project:read", "event:write"}, + IsAllowed: false, + IsRetired: false, + IsMinimumRoleFor: nil, }, { - ID: "admin", - Name: "Team Admin", - IsAllowed: false, + ID: "admin", + Name: "Team Admin", + Desc: "Admin privileges on the team. They can create and remove projects, and can manage the team's memberships. They cannot invite members to the organization.", + Scopes: []string{"team:admin", "org:integrations", "project:admin", "team:read", "project:releases", "org:read", "team:write", "event:read", "alerts:write", "member:read", "alerts:read", "event:admin", "project:read", "event:write", "project:write"}, + IsAllowed: false, + IsRetired: false, + IsMinimumRoleFor: String("admin"), }, }, TeamRoles: []TeamRole{ { TeamSlug: "ancient-gabelers", - Role: TeamRoleAdmin, + Role: String(TeamRoleAdmin), }, { TeamSlug: "powerful-abolitionist", - Role: TeamRoleContributor, + Role: String(TeamRoleContributor), }, }, Teams: []string{ diff --git a/sentry/organizations.go b/sentry/organizations.go index 52b00b3..f30cede 100644 --- a/sentry/organizations.go +++ b/sentry/organizations.go @@ -47,6 +47,8 @@ type Organization struct { IsDefault *bool `json:"isDefault,omitempty"` DefaultRole *string `json:"defaultRole,omitempty"` AvailableRoles []OrganizationAvailableRole `json:"availableRoles,omitempty"` + OrgRoleList []OrganizationRoleListItem `json:"orgRoleList,omitempty"` + TeamRoleList []TeamRoleListItem `json:"teamRoleList,omitempty"` OpenMembership *bool `json:"openMembership,omitempty"` AllowSharedIssues *bool `json:"allowSharedIssues,omitempty"` EnhancedPrivacy *bool `json:"enhancedPrivacy,omitempty"` diff --git a/sentry/organizations_test.go b/sentry/organizations_test.go index e05926a..b3943ad 100644 --- a/sentry/organizations_test.go +++ b/sentry/organizations_test.go @@ -149,6 +149,184 @@ func TestOrganizationsService_Get(t *testing.T) { "name": "Owner" } ], + "orgRoleList": [ + { + "id": "billing", + "name": "Billing", + "desc": "Can manage subscription and billing details.", + "scopes": [ + "org:billing" + ], + "allowed": false, + "isAllowed": false, + "isRetired": false, + "is_global": false, + "isGlobal": false, + "minimumTeamRole": "contributor" + }, + { + "id": "member", + "name": "Member", + "desc": "Members can view and act on events, as well as view most other data within the organization.", + "scopes": [ + "project:releases", + "alerts:read", + "event:write", + "member:read", + "team:read", + "alerts:write", + "event:admin", + "project:read", + "org:read", + "event:read" + ], + "allowed": false, + "isAllowed": false, + "isRetired": false, + "is_global": false, + "isGlobal": false, + "minimumTeamRole": "contributor" + }, + { + "id": "admin", + "name": "Admin", + "desc": "Admin privileges on any teams of which they're a member. They can create new teams and projects, as well as remove teams and projects on which they already hold membership (or all teams, if open membership is enabled). Additionally, they can manage memberships of teams that they are members of. They cannot invite members to the organization.", + "scopes": [ + "org:integrations", + "project:admin", + "project:releases", + "alerts:read", + "team:write", + "event:write", + "team:read", + "member:read", + "alerts:write", + "event:admin", + "team:admin", + "project:read", + "org:read", + "event:read", + "project:write" + ], + "allowed": false, + "isAllowed": false, + "isRetired": true, + "is_global": false, + "isGlobal": false, + "minimumTeamRole": "admin" + }, + { + "id": "manager", + "name": "Manager", + "desc": "Gains admin access on all teams as well as the ability to add and remove members.", + "scopes": [ + "member:admin", + "alerts:read", + "member:read", + "team:admin", + "alerts:write", + "project:read", + "org:read", + "event:read", + "org:integrations", + "project:admin", + "org:write", + "member:write", + "team:write", + "event:write", + "team:read", + "event:admin", + "project:releases", + "project:write" + ], + "allowed": false, + "isAllowed": false, + "isRetired": false, + "is_global": true, + "isGlobal": true, + "minimumTeamRole": "admin" + }, + { + "id": "owner", + "name": "Owner", + "desc": "Unrestricted access to the organization, its data, and its settings. Can add, modify, and delete projects and members, as well as make billing and plan changes.", + "scopes": [ + "member:admin", + "alerts:read", + "org:admin", + "team:admin", + "member:read", + "alerts:write", + "project:read", + "org:read", + "event:read", + "org:billing", + "org:integrations", + "project:admin", + "org:write", + "member:write", + "team:write", + "event:write", + "team:read", + "event:admin", + "project:releases", + "project:write" + ], + "allowed": false, + "isAllowed": false, + "isRetired": false, + "is_global": true, + "isGlobal": true, + "minimumTeamRole": "admin" + } + ], + "teamRoleList": [ + { + "id": "contributor", + "name": "Contributor", + "desc": "Contributors can view and act on events, as well as view most other data within the team's projects.", + "scopes": [ + "project:releases", + "event:write", + "org:read", + "alerts:read", + "event:read", + "team:read", + "project:read", + "member:read" + ], + "allowed": false, + "isAllowed": false, + "isRetired": false, + "isMinimumRoleFor": null + }, + { + "id": "admin", + "name": "Team Admin", + "desc": "Admin privileges on the team. They can create and remove projects, and can manage the team's memberships. They cannot invite members to the organization.", + "scopes": [ + "project:releases", + "event:write", + "project:write", + "project:admin", + "team:write", + "org:read", + "org:integrations", + "project:read", + "alerts:read", + "event:read", + "team:read", + "event:admin", + "member:read", + "alerts:write", + "team:admin" + ], + "allowed": false, + "isAllowed": false, + "isRetired": false, + "isMinimumRoleFor": "admin" + } + ], "openMembership": true, "allowSharedIssues": true, "enhancedPrivacy": false, @@ -281,6 +459,78 @@ func TestOrganizationsService_Get(t *testing.T) { Name: String("Owner"), }, }, + OrgRoleList: []OrganizationRoleListItem{ + { + ID: "billing", + Name: "Billing", + Desc: "Can manage subscription and billing details.", + Scopes: []string{"org:billing"}, + IsAllowed: false, + IsRetired: false, + IsGlobal: false, + MinimumTeamRole: "contributor", + }, + { + ID: "member", + Name: "Member", + Desc: "Members can view and act on events, as well as view most other data within the organization.", + Scopes: []string{"project:releases", "alerts:read", "event:write", "member:read", "team:read", "alerts:write", "event:admin", "project:read", "org:read", "event:read"}, + IsAllowed: false, + IsRetired: false, + IsGlobal: false, + MinimumTeamRole: "contributor", + }, + { + ID: "admin", + Name: "Admin", + Desc: "Admin privileges on any teams of which they're a member. They can create new teams and projects, as well as remove teams and projects on which they already hold membership (or all teams, if open membership is enabled). Additionally, they can manage memberships of teams that they are members of. They cannot invite members to the organization.", + Scopes: []string{"org:integrations", "project:admin", "project:releases", "alerts:read", "team:write", "event:write", "team:read", "member:read", "alerts:write", "event:admin", "team:admin", "project:read", "org:read", "event:read", "project:write"}, + IsAllowed: false, + IsRetired: true, + IsGlobal: false, + MinimumTeamRole: "admin", + }, + { + ID: "manager", + Name: "Manager", + Desc: "Gains admin access on all teams as well as the ability to add and remove members.", + Scopes: []string{"member:admin", "alerts:read", "member:read", "team:admin", "alerts:write", "project:read", "org:read", "event:read", "org:integrations", "project:admin", "org:write", "member:write", "team:write", "event:write", "team:read", "event:admin", "project:releases", "project:write"}, + IsAllowed: false, + IsRetired: false, + IsGlobal: true, + MinimumTeamRole: "admin", + }, + { + ID: "owner", + Name: "Owner", + Desc: "Unrestricted access to the organization, its data, and its settings. Can add, modify, and delete projects and members, as well as make billing and plan changes.", + Scopes: []string{"member:admin", "alerts:read", "org:admin", "team:admin", "member:read", "alerts:write", "project:read", "org:read", "event:read", "org:billing", "org:integrations", "project:admin", "org:write", "member:write", "team:write", "event:write", "team:read", "event:admin", "project:releases", "project:write"}, + IsAllowed: false, + IsRetired: false, + IsGlobal: true, + MinimumTeamRole: "admin", + }, + }, + TeamRoleList: []TeamRoleListItem{ + { + ID: "contributor", + Name: "Contributor", + Desc: "Contributors can view and act on events, as well as view most other data within the team's projects.", + Scopes: []string{"project:releases", "event:write", "org:read", "alerts:read", "event:read", "team:read", "project:read", "member:read"}, + IsAllowed: false, + IsRetired: false, + IsMinimumRoleFor: nil, + }, + { + ID: "admin", + Name: "Team Admin", + Desc: "Admin privileges on the team. They can create and remove projects, and can manage the team's memberships. They cannot invite members to the organization.", + Scopes: []string{"project:releases", "event:write", "project:write", "project:admin", "team:write", "org:read", "org:integrations", "project:read", "alerts:read", "event:read", "team:read", "event:admin", "member:read", "alerts:write", "team:admin"}, + IsAllowed: false, + IsRetired: false, + IsMinimumRoleFor: String("admin"), + }, + }, OpenMembership: Bool(true), AllowSharedIssues: Bool(true), EnhancedPrivacy: Bool(false), diff --git a/sentry/roles.go b/sentry/roles.go new file mode 100644 index 0000000..3d7989b --- /dev/null +++ b/sentry/roles.go @@ -0,0 +1,24 @@ +package sentry + +// https://github.com/getsentry/sentry/blob/23.12.1/src/sentry/api/serializers/models/role.py#L62-L74 +type OrganizationRoleListItem struct { + ID string `json:"id"` + Name string `json:"name"` + Desc string `json:"desc"` + Scopes []string `json:"scopes"` + IsAllowed bool `json:"isAllowed"` + IsRetired bool `json:"isRetired"` + IsGlobal bool `json:"isGlobal"` + MinimumTeamRole string `json:"minimumTeamRole"` +} + +// https://github.com/getsentry/sentry/blob/23.12.1/src/sentry/api/serializers/models/role.py#L77-L85 +type TeamRoleListItem struct { + ID string `json:"id"` + Name string `json:"name"` + Desc string `json:"desc"` + Scopes []string `json:"scopes"` + IsAllowed bool `json:"isAllowed"` + IsRetired bool `json:"isRetired"` + IsMinimumRoleFor *string `json:"isMinimumRoleFor"` +} diff --git a/sentry/teams.go b/sentry/teams.go index 30e7b55..556535d 100644 --- a/sentry/teams.go +++ b/sentry/teams.go @@ -7,7 +7,7 @@ import ( ) // Team represents a Sentry team that is bound to an organization. -// https://github.com/getsentry/sentry/blob/22.5.0/src/sentry/api/serializers/models/team.py#L109-L119 +// https://github.com/getsentry/sentry/blob/23.12.1/src/sentry/api/serializers/models/team.py#L155C7-L190 type Team struct { ID *string `json:"id,omitempty"` Slug *string `json:"slug,omitempty"` @@ -19,6 +19,7 @@ type Team struct { IsPending *bool `json:"isPending,omitempty"` MemberCount *int `json:"memberCount,omitempty"` Avatar *Avatar `json:"avatar,omitempty"` + OrgRole *string `json:"orgRole,omitempty"` // TODO: externalTeams // TODO: projects } From 50b9617d0974bfef28c27ba346cea3064abfde2a Mon Sep 17 00:00:00 2001 From: Foad Nosrati Habibi Date: Thu, 21 Mar 2024 13:01:25 +1100 Subject: [PATCH 41/41] Sync with upstream --- go.sum | 20 ---- sentry/project_filter_test.go | 8 +- sentry/project_filters.go | 120 ----------------------- sentry/project_filters_test.go | 173 --------------------------------- sentry/projects.go | 1 - sentry/projects_test.go | 1 - sentry/sentry.go | 4 +- 7 files changed, 6 insertions(+), 321 deletions(-) delete mode 100644 sentry/project_filters.go delete mode 100644 sentry/project_filters_test.go diff --git a/go.sum b/go.sum index 148666f..e80cf89 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,17 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/peterhellberg/link v1.1.0 h1:s2+RH8EGuI/mI4QwrWGSYQCRz7uNgip9BaM04HKu5kc= -github.com/peterhellberg/link v1.1.0/go.mod h1:gtSlOT4jmkY8P47hbTc8PTgiDDWpdPbFYl75keYyBB8= github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= -github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= -github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sentry/project_filter_test.go b/sentry/project_filter_test.go index 709f5b1..3cae9b1 100644 --- a/sentry/project_filter_test.go +++ b/sentry/project_filter_test.go @@ -22,7 +22,7 @@ func TestProjectFilterService_GetWithLegacyExtension(t *testing.T) { }) ctx := context.Background() - filterConfig, _, err := client.ProjectFilter.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") + filterConfig, _, err := client.ProjectFilters.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") assert.NoError(t, err) expected := FilterConfig{ @@ -43,7 +43,7 @@ func TestProjectFilterService_GetWithoutLegacyExtension(t *testing.T) { }) ctx := context.Background() - filterConfig, _, err := client.ProjectFilter.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") + filterConfig, _, err := client.ProjectFilters.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") assert.NoError(t, err) expected := FilterConfig{ @@ -77,7 +77,7 @@ func TestBrowserExtensionFilter(t *testing.T) { }) ctx := context.Background() - _, err := client.ProjectFilter.UpdateBrowserExtensions(ctx, "test_org", "test_project", true) + _, err := client.ProjectFilters.UpdateBrowserExtensions(ctx, "test_org", "test_project", true) assert.NoError(t, err) } @@ -95,7 +95,7 @@ func TestLegacyBrowserFilter(t *testing.T) { ctx := context.Background() browsers := []string{"ie_pre_9", "ie10"} - _, err := client.ProjectFilter.UpdateLegacyBrowser(ctx, "test_org", "test_project", browsers) + _, err := client.ProjectFilters.UpdateLegacyBrowser(ctx, "test_org", "test_project", browsers) assert.NoError(t, err) } diff --git a/sentry/project_filters.go b/sentry/project_filters.go deleted file mode 100644 index 4ddccad..0000000 --- a/sentry/project_filters.go +++ /dev/null @@ -1,120 +0,0 @@ -package sentry - -import ( - "context" - "encoding/json" - "fmt" - "net/http" -) - -// ProjectFilter represents inbounding filters applied to a project. -type ProjectFilter struct { - ID string `json:"id"` - Active json.RawMessage `json:"active"` -} - -// ProjectFiltersService provides methods for accessing Sentry project -// filters API endpoints. -type ProjectFiltersService service - -// Get the filters. -func (s *ProjectFiltersService) Get(ctx context.Context, organizationSlug string, projectSlug string) ([]*ProjectFilter, *Response, error) { - url := fmt.Sprintf("0/projects/%v/%v/filters/", organizationSlug, projectSlug) - req, err := s.client.NewRequest(http.MethodGet, url, nil) - if err != nil { - return nil, nil, err - } - - var filters []*ProjectFilter - resp, err := s.client.Do(ctx, req, &filters) - if err != nil { - return nil, resp, err - } - - return filters, resp, nil -} - -// FilterConfig represents configuration for project filter -type FilterConfig struct { - BrowserExtension bool - LegacyBrowsers []string -} - -// GetFilterConfig retrieves filter configuration. -func (s *ProjectFiltersService) GetFilterConfig(ctx context.Context, organizationSlug string, projectSlug string) (*FilterConfig, *Response, error) { - filters, resp, err := s.Get(ctx, organizationSlug, projectSlug) - if err != nil { - return nil, resp, err - } - - var filterConfig FilterConfig - - for _, filter := range filters { - switch filter.ID { - case "browser-extensions": - if string(filter.Active) == "true" { - filterConfig.BrowserExtension = true - } - - case "legacy-browsers": - if string(filter.Active) != "false" { - err = json.Unmarshal(filter.Active, &filterConfig.LegacyBrowsers) - if err != nil { - return nil, resp, err - } - } - } - } - - return &filterConfig, resp, err -} - -// BrowserExtensionParams defines parameters for browser extension request -type BrowserExtensionParams struct { - Active bool `json:"active"` -} - -// UpdateBrowserExtensions updates configuration for browser extension filter -func (s *ProjectFiltersService) UpdateBrowserExtensions(ctx context.Context, organizationSlug string, projectSlug string, active bool) (*Response, error) { - url := fmt.Sprintf("0/projects/%v/%v/filters/browser-extensions/", organizationSlug, projectSlug) - params := BrowserExtensionParams{active} - req, err := s.client.NewRequest(http.MethodPut, url, params) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// LegacyBrowserParams defines parameters for legacy browser request -type LegacyBrowserParams struct { - Browsers []string `json:"subfilters"` -} - -// UpdateLegacyBrowser updates configuration for legacy browser filters -func (s *ProjectFiltersService) UpdateLegacyBrowser(ctx context.Context, organizationSlug string, projectSlug string, browsers []string) (*Response, error) { - url := fmt.Sprintf("0/projects/%v/%v/filters/legacy-browsers/", organizationSlug, projectSlug) - params := LegacyBrowserParams{browsers} - - req, err := s.client.NewRequest(http.MethodPut, url, params) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -type UpdateProjectFilterParams struct { - Active bool `json:"active"` - Subfilters []string `json:"subfilters"` -} - -func (s *ProjectFiltersService) Update(ctx context.Context, organizationSlug string, projectSlug string, filterID string, params *UpdateProjectFilterParams) (*Response, error) { - url := fmt.Sprintf("0/projects/%v/%v/filters/%v/", organizationSlug, projectSlug, filterID) - req, err := s.client.NewRequest(http.MethodPut, url, params) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} diff --git a/sentry/project_filters_test.go b/sentry/project_filters_test.go deleted file mode 100644 index e2fbb40..0000000 --- a/sentry/project_filters_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package sentry - -import ( - "context" - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestProjectFiltersService_GetWithLegacyExtension(t *testing.T) { - client, mux, _, teardown := setup() - defer teardown() - - mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/powerful-abolitionist/filters/", func(w http.ResponseWriter, r *http.Request) { - assertMethod(t, "GET", r) - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, getWithLegacyExtensionHeader) - }) - - ctx := context.Background() - filterConfig, _, err := client.ProjectFilters.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") - assert.NoError(t, err) - - expected := FilterConfig{ - LegacyBrowsers: []string{"ie_pre_9"}, - BrowserExtension: false, - } - assert.Equal(t, &expected, filterConfig) -} - -func TestProjectFiltersService_GetWithoutLegacyExtension(t *testing.T) { - client, mux, _, teardown := setup() - defer teardown() - - mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/powerful-abolitionist/filters/", func(w http.ResponseWriter, r *http.Request) { - assertMethod(t, "GET", r) - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, getWithoutLegacyExtensionHeader) - }) - - ctx := context.Background() - filterConfig, _, err := client.ProjectFilters.GetFilterConfig(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist") - assert.NoError(t, err) - - expected := FilterConfig{ - LegacyBrowsers: nil, - BrowserExtension: true, - } - assert.Equal(t, &expected, filterConfig) -} - -func TestBrowserExtensionFilter(t *testing.T) { - client, mux, _, teardown := setup() - defer teardown() - - mux.HandleFunc("/api/0/projects/test_org/test_project/filters/browser-extensions/", func(w http.ResponseWriter, r *http.Request) { - assertMethod(t, "PUT", r) - assertPostJSON(t, map[string]interface{}{ - "active": true, - }, r) - w.Header().Set("Content-Type", "application/json") - }) - - ctx := context.Background() - _, err := client.ProjectFilters.UpdateBrowserExtensions(ctx, "test_org", "test_project", true) - assert.NoError(t, err) -} - -func TestLegacyBrowserFilter(t *testing.T) { - client, mux, _, teardown := setup() - defer teardown() - - mux.HandleFunc("/api/0/projects/test_org/test_project/filters/legacy-browsers/", func(w http.ResponseWriter, r *http.Request) { - assertMethod(t, "PUT", r) - assertPostJSON(t, map[string]interface{}{ - "subfilters": []interface{}{"ie_pre_9", "ie10"}, - }, r) - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, "") - }) - - ctx := context.Background() - browsers := []string{"ie_pre_9", "ie10"} - _, err := client.ProjectFilters.UpdateLegacyBrowser(ctx, "test_org", "test_project", browsers) - assert.NoError(t, err) -} - -var ( - getWithLegacyExtensionHeader = `[ - { - "id":"browser-extensions", - "active":false, - "description":"description_1", - "name":"name_1", - "hello":"hello_1" - }, - { - "id":"localhost", - "active":false, - "description":"description_2", - "name":"name_2", - "hello":"hello_2" - }, - { - "id":"legacy-browsers", - "active":["ie_pre_9"], - "description":"description_3", - "name":"name_3", - "hello":"hello_3" - }, - { - "id":"web-crawlers", - "active":true, - "description":"description_4", - "name":"name_4", - "hello":"hello_4" - } - ]` - getWithoutLegacyExtensionHeader = `[ - { - "id":"browser-extensions", - "active":true, - "description":"description_1", - "name":"name_1", - "hello":"hello_1" - }, - { - "id":"localhost", - "active":false, - "description":"description_2", - "name":"name_2", - "hello":"hello_2" - }, - { - "id":"legacy-browsers", - "active":false, - "description":"description_3", - "name":"name_3", - "hello":"hello_3" - }, - { - "id":"web-crawlers", - "active":true, - "description":"description_4", - "name":"name_4", - "hello":"hello_4" - } - ]` -) - -func TestProjectFiltersService_Update(t *testing.T) { - client, mux, _, teardown := setup() - defer teardown() - - mux.HandleFunc("/api/0/projects/the-interstellar-jurisdiction/powerful-abolitionist/filters/filter-id/", func(w http.ResponseWriter, r *http.Request) { - assertMethod(t, http.MethodPut, r) - assertPostJSON(t, map[string]interface{}{ - "active": true, - "subfilters": []interface{}{"ie_pre_9", "ie9"}, - }, r) - }) - - params := &UpdateProjectFilterParams{ - Active: true, - Subfilters: []string{"ie_pre_9", "ie9"}, - } - ctx := context.Background() - _, err := client.ProjectFilters.Update(ctx, "the-interstellar-jurisdiction", "powerful-abolitionist", "filter-id", params) - assert.NoError(t, err) - -} diff --git a/sentry/projects.go b/sentry/projects.go index 0e4a5a1..e2532aa 100644 --- a/sentry/projects.go +++ b/sentry/projects.go @@ -41,7 +41,6 @@ type Project struct { DataScrubber bool `json:"dataScrubber"` DataScrubberDefaults bool `json:"dataScrubberDefaults"` FingerprintingRules string `json:"fingerprintingRules"` - GroupingEnhancements string `json:"groupingEnhancements"` SafeFields []string `json:"safeFields"` SensitiveFields []string `json:"sensitiveFields"` SubjectTemplate string `json:"subjectTemplate"` diff --git a/sentry/projects_test.go b/sentry/projects_test.go index a3bece5..18c9e4a 100644 --- a/sentry/projects_test.go +++ b/sentry/projects_test.go @@ -421,7 +421,6 @@ func TestProjectsService_Get(t *testing.T) { DataScrubber: true, DataScrubberDefaults: true, FingerprintingRules: "fingerprinting rule", - GroupingEnhancements: "pump-station grouping enhancement rule", SafeFields: []string{}, SensitiveFields: []string{}, SubjectTemplate: "$shortID - $title", diff --git a/sentry/sentry.go b/sentry/sentry.go index 32b1244..6a8e96e 100644 --- a/sentry/sentry.go +++ b/sentry/sentry.go @@ -57,7 +57,7 @@ type Client struct { OrganizationMembers *OrganizationMembersService OrganizationRepositories *OrganizationRepositoriesService Organizations *OrganizationsService - ProjectFilters *ProjectFiltersService + ProjectFilters *ProjectFilterService ProjectInboundDataFilters *ProjectInboundDataFiltersService ProjectKeys *ProjectKeysService ProjectOwnerships *ProjectOwnershipsService @@ -98,7 +98,7 @@ func NewClient(httpClient *http.Client) *Client { c.OrganizationMembers = (*OrganizationMembersService)(&c.common) c.OrganizationRepositories = (*OrganizationRepositoriesService)(&c.common) c.Organizations = (*OrganizationsService)(&c.common) - c.ProjectFilters = (*ProjectFiltersService)(&c.common) + c.ProjectFilters = (*ProjectFilterService)(&c.common) c.ProjectInboundDataFilters = (*ProjectInboundDataFiltersService)(&c.common) c.ProjectKeys = (*ProjectKeysService)(&c.common) c.ProjectOwnerships = (*ProjectOwnershipsService)(&c.common)