diff --git a/static/images/integrations/crashlytics/001.png b/static/images/integrations/crashlytics/001.png
new file mode 100644
index 0000000000000..677a7add0e054
Binary files /dev/null and b/static/images/integrations/crashlytics/001.png differ
diff --git a/static/images/integrations/logos/crashlytics.png b/static/images/integrations/logos/crashlytics.png
new file mode 100644
index 0000000000000..c0d85c25c3a19
Binary files /dev/null and b/static/images/integrations/logos/crashlytics.png differ
diff --git a/templates/zerver/integrations.html b/templates/zerver/integrations.html
index 060e28f915ca3..b6d04529592ce 100644
--- a/templates/zerver/integrations.html
+++ b/templates/zerver/integrations.html
@@ -89,6 +89,12 @@
Codeship
+
+
+
+
Zulip supports integration with Crashlytics and can notify you
+ about Crashlytics issues.
+
+
First, create the stream you'd like to use for Crashlytics notifications,
+ and subscribe all interested parties to this stream. We
+ recommend the name crashlytics
.
+
+
{{ external_api_uri }}/v1/external/crashlytics?api_key=abcdefgh&stream=crashlytics
+
+
where api_key
is the API key of your Zulip bot,
+ and stream
is the stream name you want the
+ notifications sent to.
+
+
Click on the app in
+ your Crashlytics settings panel.
+ Next, on the integrations subpage, click “Web Hook,” enter the URL we created above and click
+ Verify
.
+
Congratulations! You're done!
When an issue occurs,
+ you'll get a notification like this:
+
+
![](/static/images/integrations/crashlytics/001.png)
+
+
+
diff --git a/zerver/fixtures/crashlytics/crashlytics_issue_message.json b/zerver/fixtures/crashlytics/crashlytics_issue_message.json
new file mode 100644
index 0000000000000..caa132a6a5f40
--- /dev/null
+++ b/zerver/fixtures/crashlytics/crashlytics_issue_message.json
@@ -0,0 +1,13 @@
+{
+ "event": "issue_impact_change",
+ "payload_type": "issue",
+ "payload": {
+ "display_id": 123 ,
+ "title": "Issue Title" ,
+ "method": "methodName of issue",
+ "impact_level": 2,
+ "crashes_count": 54,
+ "impacted_devices_count": 16,
+ "url": "http://crashlytics.com/full/url/to/issue"
+ }
+}
diff --git a/zerver/fixtures/crashlytics/crashlytics_verification.json b/zerver/fixtures/crashlytics/crashlytics_verification.json
new file mode 100644
index 0000000000000..bac2386487b52
--- /dev/null
+++ b/zerver/fixtures/crashlytics/crashlytics_verification.json
@@ -0,0 +1,4 @@
+{
+ "event": "verification",
+ "payload_type": "none"
+}
diff --git a/zerver/tests/test_hooks.py b/zerver/tests/test_hooks.py
index 056c7a6c0d798..4a790810950cc 100644
--- a/zerver/tests/test_hooks.py
+++ b/zerver/tests/test_hooks.py
@@ -1222,3 +1222,22 @@ def build_webhook_url(self):
def get_body(self, fixture_name):
return {}
+
+class CrashlyticsHookTests(WebhookTestCase):
+ STREAM_NAME = 'crashlytics'
+ URL_TEMPLATE = "/api/v1/external/crashlytics?stream={stream}&api_key={api_key}"
+ FIXTURE_DIR_NAME = 'crashlytics'
+
+ def test_crashlytics_verification_message(self):
+ last_message_before_request = self.get_last_message()
+ payload = self.get_body('verification')
+ url = self.build_webhook_url()
+ result = self.client.post(url, payload, content_type="application/json")
+ last_message_after_request = self.get_last_message()
+ self.assert_json_success(result)
+ self.assertEqual(last_message_after_request.pk, last_message_before_request.pk)
+
+ def test_crashlytics_build_in_success_status(self):
+ expected_subject = u"123: Issue Title"
+ expected_message = u"[Issue](http://crashlytics.com/full/url/to/issue) impacts at least 16 device(s)."
+ self.send_and_test_stream_message('issue_message', expected_subject, expected_message)
diff --git a/zerver/views/webhooks/crashlytics.py b/zerver/views/webhooks/crashlytics.py
new file mode 100644
index 0000000000000..05c6c2ce902b1
--- /dev/null
+++ b/zerver/views/webhooks/crashlytics.py
@@ -0,0 +1,37 @@
+# Webhooks for external integrations.
+from __future__ import absolute_import
+from django.utils.translation import ugettext as _
+from zerver.lib.actions import check_send_message
+from zerver.lib.response import json_success, json_error
+from zerver.decorator import REQ, has_request_variables, api_key_only_webhook_view
+
+
+CRASHLYTICS_SUBJECT_TEMPLATE = '{display_id}: {title}'
+CRASHLYTICS_MESSAGE_TEMPLATE = '[Issue]({url}) impacts at least {impacted_devices_count} device(s).'
+
+VERIFICATION_EVENT = 'verification'
+
+
+@api_key_only_webhook_view('Crashlytics')
+@has_request_variables
+def api_crashlytics_webhook(request, user_profile, client, payload=REQ(argument_type='body'),
+ stream=REQ(default='crashlytics')):
+ try:
+ event = payload['event']
+ if event == VERIFICATION_EVENT:
+ return json_success()
+ issue_body = payload['payload']
+ subject = CRASHLYTICS_SUBJECT_TEMPLATE.format(
+ display_id=issue_body['display_id'],
+ title=issue_body['title']
+ )
+ body = CRASHLYTICS_MESSAGE_TEMPLATE.format(
+ impacted_devices_count=issue_body['impacted_devices_count'],
+ url=issue_body['url']
+ )
+ except KeyError as e:
+ return json_error(_("Missing key {} in JSON".format(e.message)))
+
+ check_send_message(user_profile, client, 'stream', [stream],
+ subject, body)
+ return json_success()
diff --git a/zproject/urls.py b/zproject/urls.py
index 589e9d9080126..a67218e902cd7 100644
--- a/zproject/urls.py
+++ b/zproject/urls.py
@@ -150,6 +150,7 @@
url(r'^api/v1/external/bitbucket$', 'webhooks.bitbucket.api_bitbucket_webhook'),
url(r'^api/v1/external/circleci', 'webhooks.circleci.api_circleci_webhook'),
url(r'^api/v1/external/codeship', 'webhooks.codeship.api_codeship_webhook'),
+ url(r'^api/v1/external/crashlytics', 'webhooks.crashlytics.api_crashlytics_webhook'),
url(r'^api/v1/external/desk$', 'webhooks.deskdotcom.api_deskdotcom_webhook'),
url(r'^api/v1/external/freshdesk$', 'webhooks.freshdesk.api_freshdesk_webhook'),
url(r'^api/v1/external/github$', 'webhooks.github.api_github_landing'),