From 358f1f9ba71b3a9885b3f1bcdc6f4b6a73ccce82 Mon Sep 17 00:00:00 2001 From: Puneeth Chaganti Date: Fri, 23 Oct 2020 11:50:24 +0530 Subject: [PATCH] webhooks/sentry: Support integration configured as webhook. Sentry allows adding simple webhooks without going through the process of creating an Internal Integration in Sentry's Integration Platform[1] (which our docs recommend). The payload from sent from such a (simple) webhook integration is slightly different from the payload sent by an Internal Integration webhook. This commit tries to wrangle this payload into a form that is usable by our webhook handler to send a notification message. [1]: https://sentry.io/integration-platform/ --- zerver/webhooks/sentry/doc.md | 6 + .../webhook_event_for_exception_python.json | 199 ++++++++++++++++++ zerver/webhooks/sentry/tests.py | 23 ++ zerver/webhooks/sentry/view.py | 25 +++ 4 files changed, 253 insertions(+) create mode 100644 zerver/webhooks/sentry/fixtures/webhook_event_for_exception_python.json diff --git a/zerver/webhooks/sentry/doc.md b/zerver/webhooks/sentry/doc.md index 8d338bc50a..d3dee54ce2 100644 --- a/zerver/webhooks/sentry/doc.md +++ b/zerver/webhooks/sentry/doc.md @@ -21,6 +21,12 @@ you can scroll down to **Webhooks** on the same page and check the box that says **issues**. Make sure that you set up the permissions so that the integration will visible to the right people. + **NOTE:** Zulip also supports configuring this as a webhook in Sentry +— which, while easier to configure (Navigate to **Settings > Integrations +> WebHooks**) may not include the full breadth of event types. For instance, +some events, like issue assignments or issues being resolved, will not trigger +notifications with this configuration. + 4. Once you've saved the internal integration, go to you're project's settings (**settings** > **Projects** > Select the project). Once there go to **Alerts** and click on the **New Alert Rule** button to diff --git a/zerver/webhooks/sentry/fixtures/webhook_event_for_exception_python.json b/zerver/webhooks/sentry/fixtures/webhook_event_for_exception_python.json new file mode 100644 index 0000000000..1251ac9a2a --- /dev/null +++ b/zerver/webhooks/sentry/fixtures/webhook_event_for_exception_python.json @@ -0,0 +1,199 @@ +{ + "project_name": "live", + "message": "", + "id": "1972208801", + "culprit": "__main__ in ", + "project_slug": "foo-live", + "url": "https://sentry.io/organizations/bar-foundation/issues/1972208801/?referrer=webhooks_plugin", + "level": "error", + "triggering_rules": [ + "Send a notification for new events" + ], + "event": { + "extra": { + "sys.argv": [ + "data/trigger-exception.py" + ] + }, + "_ref_version": 2, + "_ref": 218288, + "id": "c916dccfd58e41dcabaebef0091f0736", + "_metrics": { + "bytes.ingested.event": 2229, + "bytes.stored.event": 3021 + }, + "culprit": "__main__ in ", + "title": "ValueError: new sentry error.", + "event_id": "c916dccfd58e41dcabaebef0091f0736", + "grouping_config": { + "enhancements": "eJybzDhxY05qemJypZWRgaGlroGxrqHRBABbEwcC", + "id": "legacy:2019-03-12" + }, + "platform": "python", + "version": "7", + "location": "trigger-exception.py", + "logger": "", + "type": "error", + "metadata": { + "function": "", + "type": "ValueError", + "value": "new sentry error.", + "filename": "trigger-exception.py" + }, + "tags": [ + [ + "level", + "error" + ], + [ + "runtime", + "CPython 3.8.5" + ], + [ + "runtime.name", + "CPython" + ], + [ + "server_name", + "enterprise" + ] + ], + "timestamp": 1603322711.147672, + "fingerprint": [ + "{{ default }}" + ], + "hashes": [ + "05eeb8cc2a9103790797a56728cfa1e8" + ], + "sdk": { + "version": "0.19.1", + "name": "sentry.python", + "packages": [ + { + "version": "0.19.1", + "name": "pypi:sentry-sdk" + } + ], + "integrations": [ + "argv", + "atexit", + "dedupe", + "excepthook", + "logging", + "modules", + "stdlib", + "threading" + ] + }, + "received": 1603322712.317751, + "exception": { + "values": [ + { + "stacktrace": { + "frames": [ + { + "function": "", + "abs_path": "/home/user/software/bar/bar-foo/data/trigger-exception.py", + "pre_context": [ + "", + "", + "if __name__ == \"__main__\":", + " sentry_sdk.init(dsn=DSN_SECRET)", + " try:" + ], + "post_context": [ + " except Exception as e:", + " sentry_sdk.capture_exception(e)" + ], + "vars": { + "__spec__": "None", + "__builtins__": "", + "__annotations__": null, + "__file__": "'data/trigger-exception.py'", + "__loader__": "<_frozen_importlib_external.SourceFileLoader object at 0x7f80a18acc70>", + "__cached__": "None", + "__name__": "'__main__'", + "__package__": "None", + "__doc__": "None", + "sentry_sdk": "" + }, + "module": "__main__", + "filename": "trigger-exception.py", + "lineno": 10, + "in_app": false, + "data": { + "orig_in_app": 1 + }, + "context_line": " raise ValueError(\"new sentry error.\")" + } + ] + }, + "type": "ValueError", + "value": "new sentry error." + } + ] + }, + "_meta": { + "exception": { + "values": { + "0": { + "stacktrace": { + "frames": { + "0": { + "vars": { + "": { + "len": 12 + } + } + } + } + } + } + } + } + }, + "level": "error", + "contexts": { + "runtime": { + "version": "3.8.5", + "type": "runtime", + "name": "CPython", + "build": "3.8.5 (default, Jul 28 2020, 12:59:40) \n[GCC 9.3.0]" + } + }, + "modules": { + "six": "1.14.0", + "packaging": "20.3", + "pip": "20.0.2", + "pyparsing": "2.4.6", + "html5lib": "1.0.1", + "appdirs": "1.4.3", + "distlib": "0.3.0", + "msgpack": "0.6.2", + "sentry-sdk": "0.19.1", + "pytoml": "0.1.21", + "ipaddr": "2.2.0", + "webencodings": "0.5.1", + "progress": "1.5", + "certifi": "2019.11.28", + "distro": "1.4.0", + "wheel": "0.34.2", + "pkg-resources": "0.0.0", + "urllib3": "1.25.8", + "retrying": "1.3.3", + "colorama": "0.4.3", + "cachecontrol": "0.12.6", + "lockfile": "0.12.2", + "pep517": "0.8.2", + "contextlib2": "0.6.0", + "chardet": "3.0.4", + "setuptools": "44.0.0", + "requests": "2.22.0", + "idna": "2.8" + }, + "project": 218288, + "key_id": "430951" + }, + "project": "foo-live", + "logger": null +} diff --git a/zerver/webhooks/sentry/tests.py b/zerver/webhooks/sentry/tests.py index 488b600aa8..cf61f35317 100644 --- a/zerver/webhooks/sentry/tests.py +++ b/zerver/webhooks/sentry/tests.py @@ -82,6 +82,29 @@ Traceback: ```""" self.check_webhook("event_for_exception_python", expected_topic, expected_message) + def test_webhook_event_for_exception_python(self) -> None: + expected_topic = "ValueError: new sentry error." + expected_message = """ +**New exception:** [ValueError: new sentry error.](https://sentry.io/organizations/bar-foundation/issues/1972208801/event/c916dccfd58e41dcabaebef0091f0736/) +```quote +**level:** error +**timestamp:** 2020-10-21 23:25:11 +**filename:** trigger-exception.py +``` + +Traceback: +```python3 + + + if __name__ == "__main__": + sentry_sdk.init(dsn=DSN_SECRET) + try: +---> raise ValueError("new sentry error.") + except Exception as e: + sentry_sdk.capture_exception(e) +```""" + self.check_webhook("webhook_event_for_exception_python", expected_topic, expected_message) + def test_event_for_message_golang(self) -> None: expected_topic = "A test message event from golang." expected_message = """ diff --git a/zerver/webhooks/sentry/view.py b/zerver/webhooks/sentry/view.py index 7260a0b662..449f9a29d9 100644 --- a/zerver/webhooks/sentry/view.py +++ b/zerver/webhooks/sentry/view.py @@ -1,5 +1,7 @@ import logging +from datetime import datetime from typing import Any, Dict, List, Optional, Tuple +from urllib.parse import urljoin from django.http import HttpRequest, HttpResponse @@ -220,12 +222,35 @@ def handle_deprecated_payload(payload: Dict[str, Any]) -> Tuple[str, str]: return (subject, body) +def transform_webhook_payload(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Attempt to use webhook payload for the notification. + + When the integration is configured as a webhook, instead of being added as + an Internal Integration, the payload is slightly different, but has all the + required information for sending a notification. We transform this payload to + look like the payload from a "properly configured" integration. + """ + event = payload.get('event', {}) + # deprecated payloads don't have event_id + event_id = event.get('event_id') + if not event_id: + return None + + event_path = f"event/{event_id}/" + event['web_url'] = urljoin(payload['url'], event_path) + event['datetime'] = datetime.fromtimestamp(event['timestamp']).isoformat() + return payload + + @webhook_view('Sentry') @has_request_variables def api_sentry_webhook(request: HttpRequest, user_profile: UserProfile, payload: Dict[str, Any] = REQ(argument_type="body")) -> HttpResponse: data = payload.get("data", None) + if data is None: + data = transform_webhook_payload(payload) + # We currently support two types of payloads: events and issues. if data: if "event" in data: