def test_superuser_sees_unowned_published_requests(self): self.login_as(user=self.superuser, superuser=True) buffer = SentryAppWebhookRequestsBuffer(self.unowned_published_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.unowned_published_app.webhook_url, ) buffer.add_request( response_code=500, org_id=self.org.id, event="issue.assigned", url=self.unowned_published_app.webhook_url, ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.unowned_published_app.slug]) response = self.client.get(url, format="json") assert response.status_code == 200 assert len(response.data) == 2 assert response.data[0]["organization"]["slug"] == self.org.slug assert response.data[0][ "sentryAppSlug"] == self.unowned_published_app.slug assert response.data[0]["responseCode"] == 500
def setUp(self): self.sentry_app = self.create_sentry_app( name="Test App", events=["issue.resolved", "issue.ignored", "issue.assigned"]) self.project = self.create_project() self.buffer = SentryAppWebhookRequestsBuffer(self.sentry_app)
def send_and_save_sentry_app_request(url, sentry_app, org_id, event, **kwargs): """ Send a webhook request, and save the request into the Redis buffer for the app dashboard request log Returns the response of the request kwargs ends up being the arguments passed into safe_urlopen """ buffer = SentryAppWebhookRequestsBuffer(sentry_app) slug = sentry_app.slug_for_metrics try: resp = safe_urlopen(url=url, **kwargs) except RequestException: track_response_code("timeout", slug, event) # Response code of 0 represents timeout buffer.add_request(response_code=0, org_id=org_id, event=event, url=url) # Re-raise the exception because some of these tasks might retry on the exception raise track_response_code(resp.status_code, slug, event) buffer.add_request( response_code=resp.status_code, org_id=org_id, event=event, url=url, error_id=resp.headers.get("Sentry-Hook-Error"), project_id=resp.headers.get("Sentry-Hook-Project"), ) return resp
def test_linked_error_not_returned_if_project_does_not_exist(self): self.login_as(user=self.user) self.store_event( data={ "event_id": self.event_id, "timestamp": iso_format(before_now(minutes=1)) }, project_id=self.project.id, ) buffer = SentryAppWebhookRequestsBuffer(self.published_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.unpublished_app.webhook_url, error_id=self.event_id, project_id="1000", ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.published_app.slug]) response = self.client.get(url, format="json") assert response.status_code == 200 assert len(response.data) == 1 assert response.data[0]["organization"]["slug"] == self.org.slug assert response.data[0]["sentryAppSlug"] == self.published_app.slug assert "errorUrl" not in response.data[0]
def get(self, request, sentry_app): """ :qparam string eventType: Optionally specify a specific event type to filter requests :qparam bool errorsOnly: If this is true, only return error/warning requests (300-599) """ event_type = request.GET.get("eventType") errors_only = request.GET.get("errorsOnly") kwargs = {} if event_type: if event_type not in EXTENDED_VALID_EVENTS: return Response({"detail": "Invalid event type."}, status=400) kwargs["event"] = event_type if errors_only: kwargs["errors_only"] = True buffer = SentryAppWebhookRequestsBuffer(sentry_app) formatted_requests = [ self.format_request(req, sentry_app) for req in buffer.get_requests(**kwargs) ] return Response(formatted_requests)
def test_makes_request(self): options = [ {"label": "An Issue", "value": "123", "default": True}, {"label": "Another Issue", "value": "456"}, ] responses.add( method=responses.GET, url=f"https://example.com/get-issues?installationId={self.install.uuid}&projectSlug={self.project.slug}", json=options, status=200, content_type="application/json", ) result = SelectRequester.run(install=self.install, project=self.project, uri="/get-issues") assert result == { "choices": [["123", "An Issue"], ["456", "Another Issue"]], "defaultValue": "123", } request = responses.calls[0].request assert request.headers["Sentry-App-Signature"] buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 1 assert requests[0]["response_code"] == 200 assert requests[0]["event_type"] == "select_options.requested"
def send_and_save_webhook_request(url, sentry_app, app_platform_event): buffer = SentryAppWebhookRequestsBuffer(sentry_app) org_id = app_platform_event.install.organization_id event = "{}.{}".format(app_platform_event.resource, app_platform_event.action) try: resp = safe_urlopen(url=url, data=app_platform_event.body, headers=app_platform_event.headers, timeout=5) except RequestException: # Response code of 0 represents timeout buffer.add_request(response_code=0, org_id=org_id, event=event, url=url) # Re-raise the exception because some of these tasks might retry on the exception raise buffer.add_request( response_code=resp.status_code, org_id=org_id, event=event, url=url, error_id=resp.headers.get("Sentry-Hook-Error"), project_id=resp.headers.get("Sentry-Hook-Project"), ) return resp
def test_send_alert_event_with_additional_payload(self, safe_urlopen): event = self.store_event(data={}, project_id=self.project.id) settings = { "alert_prefix": "[Not Good]", "channel": "#ignored-errors", "best_emoji": ":fire:", } rule_future = RuleFuture( rule=self.rule, kwargs={"sentry_app": self.sentry_app, "schema_defined_settings": settings}, ) with self.tasks(): notify_sentry_app(event, [rule_future]) payload = json.loads(faux(safe_urlopen).kwargs["data"]) assert payload["action"] == "triggered" assert payload["data"]["triggered_rule"] == self.rule.label assert payload["data"]["issue_alert"] == { "id": self.rule.id, "title": self.rule.label, "sentry_app_id": self.sentry_app.id, "settings": settings, } buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 1 assert requests[0]["response_code"] == 200 assert requests[0]["event_type"] == "event_alert.triggered"
def send_and_save_sentry_app_request(url, sentry_app, org_id, event, **kwargs): """ Send a webhook request, and save the request into the Redis buffer for the app dashboard request log Returns the response of the request kwargs ends up being the arguments passed into safe_urlopen """ buffer = SentryAppWebhookRequestsBuffer(sentry_app) try: resp = safe_urlopen(url=url, **kwargs) except RequestException: # Response code of 0 represents timeout buffer.add_request(response_code=0, org_id=org_id, event=event, url=url) # Re-raise the exception because some of these tasks might retry on the exception raise buffer.add_request(response_code=resp.status_code, org_id=org_id, event=event, url=url) return resp
def test_500_response(self): responses.add( method=responses.POST, url="https://example.com/link-issue", body="Something failed", status=500, ) with self.assertRaises(APIError): IssueLinkRequester.run( install=self.install, project=self.project, group=self.group, uri="/link-issue", fields={}, user=self.user, action="create", ) buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 1 assert requests[0]["response_code"] == 500 assert requests[0]["event_type"] == "external_issue.created"
def send_and_save_webhook_request(sentry_app, app_platform_event, url=None): """ Notify a SentryApp's webhook about an incident and log response on redis. :param sentry_app: The SentryApp to notify via a webhook. :param app_platform_event: Incident data. See AppPlatformEvent. :param url: The URL to hit for this webhook if it is different from `sentry_app.webhook_url`. :return: Webhook response """ buffer = SentryAppWebhookRequestsBuffer(sentry_app) org_id = app_platform_event.install.organization_id event = f"{app_platform_event.resource}.{app_platform_event.action}" slug = sentry_app.slug_for_metrics url = url or sentry_app.webhook_url try: resp = safe_urlopen( url=url, data=app_platform_event.body, headers=app_platform_event.headers, timeout=5 ) except (Timeout, ConnectionError) as e: error_type = e.__class__.__name__.lower() logger.info( "send_and_save_webhook_request.timeout", extra={ "error_type": error_type, "organization_id": org_id, "integration_slug": sentry_app.slug, }, ) track_response_code(error_type, slug, event) # Response code of 0 represents timeout buffer.add_request(response_code=0, org_id=org_id, event=event, url=url) # Re-raise the exception because some of these tasks might retry on the exception raise else: track_response_code(resp.status_code, slug, event) buffer.add_request( response_code=resp.status_code, org_id=org_id, event=event, url=url, error_id=resp.headers.get("Sentry-Hook-Error"), project_id=resp.headers.get("Sentry-Hook-Project"), ) if resp.status_code == 503: raise ApiHostError.from_request(resp.request) elif resp.status_code == 504: raise ApiTimeoutError.from_request(resp.request) if 400 <= resp.status_code < 500: raise ClientError(resp.status_code, url, response=resp) resp.raise_for_status() return resp
def test_errors_only_filter(self): self.login_as(user=self.user) buffer = SentryAppWebhookRequestsBuffer(self.published_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.published_app.webhook_url, ) buffer.add_request( response_code=500, org_id=self.org.id, event="issue.assigned", url=self.published_app.webhook_url, ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.published_app.slug]) errors_only_response = self.client.get( "{}?errorsOnly=true".format(url), format="json") assert errors_only_response.status_code == 200 assert len(errors_only_response.data) == 1 response = self.client.get(url, format="json") assert response.status_code == 200 assert len(response.data) == 2
def test_500_response(self): responses.add( method=responses.GET, url= u"https://example.com/get-issues?installationId={}&projectSlug={}". format(self.install.uuid, self.project.slug), body="Something failed", status=500, ) with self.assertRaises(APIError): SelectRequester.run( install=self.install, project=self.project, group=self.group, uri="/get-issues", fields={}, ) buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 1 assert requests[0]["response_code"] == 500 assert requests[0]["event_type"] == "select_options.requested"
def test_webhook_request_saved(self, safe_urlopen): InstallationNotifier.run(install=self.install, user=self.user, action="created") InstallationNotifier.run(install=self.install, user=self.user, action="deleted") buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 2 assert requests[0]["event_type"] == "installation.deleted" assert requests[1]["event_type"] == "installation.created"
def test_send_alert_event(self, safe_urlopen): event = self.store_event(data={}, project_id=self.project.id) group = event.group rule_future = RuleFuture(rule=self.rule, kwargs={"sentry_app": self.sentry_app}) with self.tasks(): notify_sentry_app(event, [rule_future]) data = json.loads(faux(safe_urlopen).kwargs["data"]) assert data == { "action": "triggered", "installation": {"uuid": self.install.uuid}, "data": { "event": DictContaining( event_id=event.event_id, url=absolute_uri( reverse( "sentry-api-0-project-event-details", args=[self.organization.slug, self.project.slug, event.event_id], ) ), web_url=absolute_uri( reverse( "sentry-organization-event-detail", args=[self.organization.slug, group.id, event.event_id], ) ), issue_url=absolute_uri(f"/api/0/issues/{group.id}/"), ), "triggered_rule": self.rule.label, }, "actor": {"type": "application", "id": "sentry", "name": "Sentry"}, } assert faux(safe_urlopen).kwarg_equals( "headers", DictContaining( "Content-Type", "Request-ID", "Sentry-Hook-Resource", "Sentry-Hook-Timestamp", "Sentry-Hook-Signature", ), ) buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 1 assert requests[0]["response_code"] == 200 assert requests[0]["event_type"] == "event_alert.triggered"
def get(self, request, sentry_app): # TODO add optional query params for event type # for now I'm just getting all requests for all events buffer = SentryAppWebhookRequestsBuffer(sentry_app) formatted_requests = [ self.format_request(req, sentry_app) for req in buffer.get_requests() ] return Response(formatted_requests)
def send_webhooks(installation, event, **kwargs): try: servicehook = ServiceHook.objects.get( organization_id=installation.organization_id, actor_id=installation.id ) except ServiceHook.DoesNotExist: return if event not in servicehook.events: return # The service hook applies to all projects if there are no # ServiceHookProject records. Otherwise we want check if # the event is within the allowed projects. project_limited = ServiceHookProject.objects.filter(service_hook_id=servicehook.id).exists() if not project_limited: resource, action = event.split(".") kwargs["resource"] = resource kwargs["action"] = action kwargs["install"] = installation request_data = AppPlatformEvent(**kwargs) buffer = SentryAppWebhookRequestsBuffer(installation.sentry_app) try: resp = safe_urlopen( url=servicehook.sentry_app.webhook_url, data=request_data.body, headers=request_data.headers, timeout=5, ) except RequestException: # Response code of 0 represents timeout buffer.add_request( response_code=0, org_id=installation.organization_id, event=event, url=servicehook.sentry_app.webhook_url, ) # Re-raise the exception because some of these tasks might retry on the exception raise buffer.add_request( response_code=resp.status_code, org_id=installation.organization_id, event=event, url=servicehook.sentry_app.webhook_url, )
def setUp(self): self.project = self.create_project() self.user = self.create_user() self.sentry_app = self.create_sentry_app( name="Test App", organization=self.project.organization, events=["issue.resolved", "issue.ignored", "issue.assigned"], ) self.install = self.create_sentry_app_installation( organization=self.project.organization, slug=self.sentry_app.slug) self.issue = self.create_group(project=self.project) self.buffer = SentryAppWebhookRequestsBuffer(self.sentry_app)
def test_user_does_not_see_unowned_published_requests(self): self.login_as(user=self.user) buffer = SentryAppWebhookRequestsBuffer(self.unowned_published_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.unowned_published_app.webhook_url, ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.unowned_published_app.slug]) response = self.client.get(url, format="json") assert response.status_code == 403 assert response.data["detail"] == "You do not have permission to perform this action."
def test_user_sees_owned_unpublished_requests(self): self.login_as(user=self.user) buffer = SentryAppWebhookRequestsBuffer(self.unpublished_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.unpublished_app.webhook_url, ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.unpublished_app.slug]) response = self.client.get(url, format="json") assert response.status_code == 200 assert len(response.data) == 1
def send_and_save_sentry_app_request(url, sentry_app, org_id, event, **kwargs): """ Send a webhook request, and save the request into the Redis buffer for the app dashboard request log Returns the response of the request kwargs ends up being the arguments passed into safe_urlopen """ buffer = SentryAppWebhookRequestsBuffer(sentry_app) slug = sentry_app.slug_for_metrics try: resp = safe_urlopen(url=url, **kwargs) except (Timeout, ConnectionError) as e: error_type = e.__class__.__name__.lower() logger.info( "send_and_save_sentry_app_request.timeout", extra={ "error_type": error_type, "organization_id": org_id, "integration_slug": sentry_app.slug, }, ) track_response_code(error_type, slug, event) # Response code of 0 represents timeout buffer.add_request(response_code=0, org_id=org_id, event=event, url=url) # Re-raise the exception because some of these tasks might retry on the exception raise else: track_response_code(resp.status_code, slug, event) buffer.add_request( response_code=resp.status_code, org_id=org_id, event=event, url=url, error_id=resp.headers.get("Sentry-Hook-Error"), project_id=resp.headers.get("Sentry-Hook-Project"), ) resp.raise_for_status() return resp
def test_internal_app_requests_does_not_have_organization_field(self): self.login_as(user=self.user) buffer = SentryAppWebhookRequestsBuffer(self.internal_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.internal_app.webhook_url, ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.internal_app.slug]) response = self.client.get(url, format="json") assert response.status_code == 200 assert len(response.data) == 1 assert "organization" not in response.data[0] assert response.data[0]["sentryAppSlug"] == self.internal_app.slug assert response.data[0]["responseCode"] == 200
def test_makes_failed_request(self): responses.add( method=responses.POST, url="https://example.com/sentry/alert-rule", status=401, json="Channel not found!", ) result = AlertRuleActionRequester.run( install=self.install, uri="/sentry/alert-rule", fields=self.fields, ) assert not result["success"] assert result["message"] == 'foo: "Channel not found!"' request = responses.calls[0].request data = { "fields": [ {"name": "title", "value": "An Alert"}, {"name": "description", "value": "threshold reached"}, { "name": "assignee_id", "value": "user-1", }, ], "installationId": self.install.uuid, } payload = json.loads(request.body) assert payload == data assert request.headers["Sentry-App-Signature"] == self.sentry_app.build_signature( json.dumps(payload) ) buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 1 assert requests[0]["response_code"] == 401 assert requests[0]["event_type"] == "alert_rule_action.requested"
def test_linked_error_not_returned_if_project_does_not_exist(self): self.login_as(user=self.user) buffer = SentryAppWebhookRequestsBuffer(self.published_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.unpublished_app.webhook_url, error_id="d5111da2c28645c5889d072017e3445d", project_id="1000", ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.published_app.slug]) response = self.client.get(url, format="json") assert response.status_code == 200 assert len(response.data) == 1 assert response.data[0]["organization"]["slug"] == self.org.slug assert response.data[0]["sentryAppSlug"] == self.published_app.slug assert "errorUrl" not in response.data[0]
def test_event_type_filter(self): self.login_as(user=self.user) buffer = SentryAppWebhookRequestsBuffer(self.published_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.published_app.webhook_url, ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.published_app.slug]) response1 = self.client.get("{}?eventType=issue.created".format(url), format="json") assert response1.status_code == 200 assert len(response1.data) == 0 response2 = self.client.get("{}?eventType=issue.assigned".format(url), format="json") assert response2.status_code == 200 assert len(response2.data) == 1
def test_makes_request(self): fields = { "title": "An Alert", "description": "threshold reached", "assignee": "user-1" } responses.add( method=responses.POST, url="https://example.com/sentry/alert-rule", status=200, json={}, ) result = AlertRuleActionRequester.run( install=self.install, uri="/sentry/alert-rule", fields=fields, ) assert result["success"] request = responses.calls[0].request assert request.headers["Sentry-App-Signature"] data = { "fields": { "title": "An Alert", "description": "threshold reached", "assignee": "user-1", }, "installationId": self.install.uuid, } payload = json.loads(request.body) assert payload == data buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 1 assert requests[0]["response_code"] == 200 assert requests[0]["event_type"] == "alert_rule_action.requested"
def send_and_save_webhook_request(url, sentry_app, app_platform_event): buffer = SentryAppWebhookRequestsBuffer(sentry_app) org_id = app_platform_event.install.organization_id event = "{}.{}".format(app_platform_event.resource, app_platform_event.action) slug = sentry_app.slug_for_metrics try: resp = safe_urlopen( url=url, data=app_platform_event.body, headers=app_platform_event.headers, timeout=5 ) except (Timeout, ConnectionError) as e: track_response_code(e.__class__.__name__.lower(), slug, event) # Response code of 0 represents timeout buffer.add_request(response_code=0, org_id=org_id, event=event, url=url) # Re-raise the exception because some of these tasks might retry on the exception raise else: track_response_code(resp.status_code, slug, event) buffer.add_request( response_code=resp.status_code, org_id=org_id, event=event, url=url, error_id=resp.headers.get("Sentry-Hook-Error"), project_id=resp.headers.get("Sentry-Hook-Project"), ) if resp.status_code == 503: raise ApiHostError.from_request(resp.request) elif resp.status_code == 504: raise ApiTimeoutError.from_request(resp.request) resp.raise_for_status() return resp
def test_linked_error_not_returned_if_event_does_not_exist(self): self.login_as(user=self.user) # event_id doesn't correspond to an existing event because we didn't call store_event buffer = SentryAppWebhookRequestsBuffer(self.published_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.unpublished_app.webhook_url, error_id=self.event_id, project_id=self.project.id, ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.published_app.slug]) response = self.client.get(url, format="json") assert response.status_code == 200 assert len(response.data) == 1 assert response.data[0]["organization"]["slug"] == self.org.slug assert response.data[0]["sentryAppSlug"] == self.published_app.slug assert "errorUrl" not in response.data[0]
def test_linked_error_not_returned_if_project_doesnt_belong_to_org(self): self.login_as(user=self.user) unowned_project = self.create_project( organization=self.create_organization()) buffer = SentryAppWebhookRequestsBuffer(self.published_app) buffer.add_request( response_code=200, org_id=self.org.id, event="issue.assigned", url=self.unpublished_app.webhook_url, error_id=self.event_id, project_id=unowned_project.id, ) url = reverse("sentry-api-0-sentry-app-requests", args=[self.published_app.slug]) response = self.client.get(url, format="json") assert response.status_code == 200 assert len(response.data) == 1 assert response.data[0]["organization"]["slug"] == self.org.slug assert response.data[0]["sentryAppSlug"] == self.published_app.slug assert "errorUrl" not in response.data[0]
def test_makes_request(self): fields = { "title": "An Issue", "description": "a bug was found", "assignee": "user-1" } responses.add( method=responses.POST, url="https://example.com/link-issue", json={ "project": "ProjectName", "webUrl": "https://example.com/project/issue-id", "identifier": "issue-1", }, status=200, content_type="application/json", ) result = IssueLinkRequester.run( install=self.install, project=self.project, group=self.group, uri="/link-issue", fields=fields, user=self.user, action="create", ) assert result == { "project": "ProjectName", "webUrl": "https://example.com/project/issue-id", "identifier": "issue-1", } request = responses.calls[0].request data = { "fields": { "title": "An Issue", "description": "a bug was found", "assignee": "user-1" }, "issueId": self.group.id, "installationId": self.install.uuid, "webUrl": self.group.get_absolute_url(), "project": { "id": self.project.id, "slug": self.project.slug }, "actor": { "type": "user", "id": self.user.id, "name": self.user.name }, } payload = json.loads(request.body) assert payload == data assert request.headers[ "Sentry-App-Signature"] == self.sentry_app.build_signature( json.dumps(payload)) buffer = SentryAppWebhookRequestsBuffer(self.sentry_app) requests = buffer.get_requests() assert len(requests) == 1 assert requests[0]["response_code"] == 200 assert requests[0]["event_type"] == "external_issue.created"