Files
zulip/zerver/webhooks/sentry/view.py
Hemanth V. Alluri fb757e91c1 webhooks/sentry: Add back support for the legacy integrations.
When the integration was originally rewritten, support for the
deprecated webhook payloads was removed. We later noticed that some
people using Zulip were still using versions of Sentry that required
the older integration code.

Thus this commit adds back the older integration code and whenever the
Sentry webhook payload does not have a "data" field (which must be
present in all modern payloads as per the documentation at
https://docs.sentry.io/workflow/integrations/integration-platform/webhooks)
we will use the older Sentry integration code.

Signed-off-by: Hemanth V. Alluri <hdrive1999@gmail.com>
2020-05-07 11:26:19 -07:00

234 lines
7.0 KiB
Python

from typing import Any, Dict, List, Tuple, Optional
from django.http import HttpRequest, HttpResponse
from zerver.decorator import api_key_only_webhook_view
from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success
from zerver.lib.webhooks.common import UnexpectedWebhookEventType, check_send_webhook_message
from zerver.models import UserProfile
DEPRECATED_EXCEPTION_MESSAGE_TEMPLATE = """
New [issue]({url}) (level: {level}):
``` quote
{message}
```
"""
MESSAGE_EVENT_TEMPLATE = """
**New message event:** [{title}]({web_link})
```quote
**level:** {level}
**timestamp:** {datetime}
```
"""
EXCEPTION_EVENT_TEMPLATE = """
**New exception:** [{title}]({web_link})
```quote
**level:** {level}
**timestamp:** {datetime}
**filename:** {filename}
```
"""
EXCEPTION_EVENT_TEMPLATE_WITH_TRACEBACK = EXCEPTION_EVENT_TEMPLATE + """
Traceback:
```{platform}
{pre_context}---> {context_line}{post_context}\
```
"""
# Because of the \n added at the end of each context element,
# this will actually look better in the traceback.
ISSUE_CREATED_MESSAGE_TEMPLATE = """
**New issue created:** {title}
```quote
**level:** {level}
**timestamp:** {datetime}
**assignee:** {assignee}
```
"""
ISSUE_ASSIGNED_MESSAGE_TEMPLATE = """
Issue **{title}** has now been assigned to **{assignee}** by **{actor}**.
"""
ISSUE_RESOLVED_MESSAGE_TEMPLATE = """
Issue **{title}** was marked as resolved by **{actor}**.
"""
ISSUE_IGNORED_MESSAGE_TEMPLATE = """
Issue **{title}** was ignored by **{actor}**.
"""
platforms_map = {
"go": "go",
"node": "javascript",
"python": "python3",
} # We can expand this as and when users use this integration with different platforms.
def convert_lines_to_traceback_string(lines: Optional[List[str]]) -> str:
traceback = ""
if lines is not None:
for line in lines:
if (line == ""):
traceback += "\n"
else:
traceback += " {}\n".format(line)
return traceback
def handle_event_payload(event: Dict[str, Any]) -> Tuple[str, str]:
""" Handle either an exception type event or a message type event payload."""
subject = event["title"]
# We shouldn't support the officially deprecated Raven series of SDKs.
if int(event["version"]) < 7:
raise UnexpectedWebhookEventType("Sentry", "Raven SDK")
context = {
"title": subject,
"level": event["level"],
"web_link": event["web_url"],
"datetime": event["datetime"].split(".")[0].replace("T", " ")
}
if "exception" in event:
# The event was triggered by a sentry.capture_exception() call
# (in the Python Sentry SDK) or something similar.
filename = event["metadata"]["filename"]
platform = platforms_map[event["platform"]]
stacktrace = None
for value in event["exception"]["values"]:
if "stacktrace" in value:
stacktrace = value["stacktrace"]
break
if stacktrace:
exception_frame = None
for frame in stacktrace["frames"]:
if frame["filename"] == filename:
exception_frame = frame
break
if exception_frame:
pre_context = convert_lines_to_traceback_string(exception_frame["pre_context"])
context_line = exception_frame["context_line"] + "\n"
if not context_line:
context_line = "\n" # nocoverage
post_context = convert_lines_to_traceback_string(exception_frame["post_context"])
context.update({
"platform": platform,
"filename": filename,
"pre_context": pre_context,
"context_line": context_line,
"post_context": post_context
})
body = EXCEPTION_EVENT_TEMPLATE_WITH_TRACEBACK.format(**context)
return (subject, body)
context.update({"filename": filename}) # nocoverage
body = EXCEPTION_EVENT_TEMPLATE.format(**context) # nocoverage
return (subject, body) # nocoverage
elif "logentry" in event:
# The event was triggered by a sentry.capture_message() call
# (in the Python Sentry SDK) or something similar.
body = MESSAGE_EVENT_TEMPLATE.format(**context)
else:
raise UnexpectedWebhookEventType("Sentry", "unknown-event type")
return (subject, body)
def handle_issue_payload(action: str, issue: Dict[str, Any], actor: Dict[str, Any]) -> Tuple[str, str]:
""" Handle either an issue type event. """
subject = issue["title"]
datetime = issue["lastSeen"].split(".")[0].replace("T", " ")
if issue["assignedTo"]:
if issue["assignedTo"]["type"] == "team":
assignee = "team {}".format(issue["assignedTo"]["name"])
else:
assignee = issue["assignedTo"]["name"]
else:
assignee = "No one"
if action == "created":
context = {
"title": subject,
"level": issue["level"],
"datetime": datetime,
"assignee": assignee
}
body = ISSUE_CREATED_MESSAGE_TEMPLATE.format(**context)
elif action == "resolved":
context = {
"title": subject,
"actor": actor["name"]
}
body = ISSUE_RESOLVED_MESSAGE_TEMPLATE.format(**context)
elif action == "assigned":
context = {
"title": subject,
"assignee": assignee,
"actor": actor["name"]
}
body = ISSUE_ASSIGNED_MESSAGE_TEMPLATE.format(**context)
elif action == "ignored":
context = {
"title": subject,
"actor": actor["name"]
}
body = ISSUE_IGNORED_MESSAGE_TEMPLATE.format(**context)
else:
raise UnexpectedWebhookEventType("Sentry", "unknown-issue-action type")
return (subject, body)
def handle_deprecated_payload(payload: Dict[str, Any]) -> Tuple[str, str]:
subject = "{}".format(payload.get('project_name'))
body = DEPRECATED_EXCEPTION_MESSAGE_TEMPLATE.format(
level=payload['level'].upper(),
url=payload.get('url'),
message=payload.get('message')
)
return (subject, body)
@api_key_only_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)
# We currently support two types of payloads: events and issues.
if data:
if "event" in data:
subject, body = handle_event_payload(data["event"])
elif "issue" in data:
subject, body = handle_issue_payload(payload["action"], data["issue"], payload["actor"])
else:
raise UnexpectedWebhookEventType("Sentry", str((list(data.keys()))))
else:
subject, body = handle_deprecated_payload(payload)
check_send_webhook_message(request, user_profile, subject, body)
return json_success()