├── HISTORY.rst ├── .settings └── .gitignore ├── requirements_base.txt ├── requirements_dev.txt ├── tests ├── __init__.py ├── conftest.py └── test_plugin.py ├── MANIFEST.in ├── sentry_zendesk ├── __init__.py ├── client.py └── plugin.py ├── .project ├── .pydevproject ├── tox.ini ├── .gitignore ├── setup.py ├── .travis.yml ├── README.rst ├── CONTRIBUTING.rst └── LICENSE /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | -------------------------------------------------------------------------------- /.settings/.gitignore: -------------------------------------------------------------------------------- 1 | org.python.pydev.yaml 2 | *.prefs 3 | -------------------------------------------------------------------------------- /requirements_base.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pytest 3 | pytest-cov 4 | responses 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements_base.txt 2 | sentry 3 | sentry-plugins 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function, unicode_literals 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | include CONTRIBUTING.rst 4 | include HISTORY.rst 5 | include LICENSE 6 | include README.rst 7 | 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /sentry_zendesk/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function, unicode_literals 2 | 3 | import logging 4 | 5 | from django.conf import settings # noqa 6 | 7 | from ._version import version as VERSION # noqa 8 | 9 | logger = logging.getLogger('sentry_zendesk') 10 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | sentry-zendesk 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /${PROJECT_DIR_NAME}/tests 5 | /${PROJECT_DIR_NAME} 6 | 7 | python 2.7 8 | Default 9 | 10 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function, unicode_literals 2 | 3 | import os 4 | 5 | from django.conf import settings 6 | 7 | 8 | # Run tests against sqlite for simplicity 9 | os.environ.setdefault('DB', 'sqlite') 10 | pytest_plugins = [b'sentry.utils.pytest'] 11 | 12 | 13 | def pytest_configure(config): 14 | settings.INSTALLED_APPS = tuple(settings.INSTALLED_APPS) + ( 15 | 'sentry_zendesk', 16 | ) 17 | 18 | from sentry.plugins import plugins 19 | from sentry_zendesk.plugin import ZendeskPlugin 20 | plugins.register(ZendeskPlugin) 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27-sentry811 4 | py27-sentry812 5 | py27-sentry813 6 | py27-sentry814 7 | py27-sentrylatest 8 | coverage 9 | linting 10 | 11 | [testenv] 12 | deps = 13 | -r{toxinidir}/requirements_base.txt 14 | sentry811: sentry==8.11.* 15 | sentry811: sentry-plugins==8.11.* 16 | sentry812: sentry==8.12.* 17 | sentry812: sentry-plugins==8.12.* 18 | sentry813: sentry==8.13.* 19 | sentry813: sentry-plugins==8.13.* 20 | sentry814: sentry==8.14.* 21 | sentry814: sentry-plugins==8.14.* 22 | sentrylatest: sentry 23 | sentrylatest: sentry-plugins 24 | commands = py.test -vvv {posargs:tests} 25 | 26 | [testenv:linting] 27 | deps = 28 | flake8 29 | # pygments required by rst-lint 30 | pygments 31 | restructuredtext_lint 32 | commands = 33 | flake8 sentry_zendesk tests 34 | rst-lint CONTRIBUTING.rst HISTORY.rst README.rst 35 | 36 | # Run on a separate env and job so we can run the tests in develop mode, which 37 | # makes it easier to run coverage and specify the sources. 38 | [testenv:coverage] 39 | usedevelop=True 40 | deps = 41 | -r{toxinidir}/requirements_dev.txt 42 | commands = 43 | py.test -vvv --cov sentry_zendesk --cov-config .coveragerc --cov-report xml {posargs:tests} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | .env* 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # PyCharm 93 | .idea 94 | 95 | sentry_zendesk/_version.py 96 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import 3 | 4 | from setuptools import setup 5 | 6 | with open('README.rst') as readme_file: 7 | readme = readme_file.read() 8 | 9 | with open('HISTORY.rst') as history_file: 10 | history = history_file.read() 11 | 12 | tests_require = [ 13 | 'exam', 14 | 'flake8>=2.0,<2.1', 15 | 'responses', 16 | 'sentry>=8.11.0', 17 | 'sentry-plugins>=8.11.0', 18 | ] 19 | 20 | install_requires = [ 21 | 'sentry>=8.11.0', 22 | 'sentry-plugins>=8.11.0', 23 | ] 24 | 25 | setup( 26 | name='sentry-zendesk', 27 | author='ESSS', 28 | url='https://github.com/ESSS/sentry-zendesk', 29 | description='Plugin for Sentry which allows linking Zendesk tickets to ' 30 | 'Sentry issues', 31 | long_description=readme + '\n\n' + history, 32 | use_scm_version={'write_to': 'sentry_zendesk/_version.py'}, 33 | setup_requires=['setuptools_scm'], 34 | license='Apache', 35 | packages=['sentry_zendesk'], 36 | zip_safe=False, 37 | install_requires=install_requires, 38 | entry_points={ 39 | 'sentry.apps': [ 40 | 'zendesk = sentry_zendesk', 41 | ], 42 | 'sentry.plugins': [ 43 | 'zendesk = sentry_zendesk.plugin:ZendeskPlugin', 44 | ], 45 | }, 46 | classifiers=[ 47 | 'Intended Audience :: Developers', 48 | 'Intended Audience :: System Administrators', 49 | 'License :: OSI Approved :: Apache Software License', 50 | 'Operating System :: POSIX :: Linux', 51 | "Programming Language :: Python :: 2", 52 | 'Programming Language :: Python :: 2.7', 53 | 'Topic :: Software Development' 54 | ], 55 | test_suite='tests', 56 | tests_require=tests_require, 57 | ) 58 | -------------------------------------------------------------------------------- /sentry_zendesk/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function, unicode_literals 2 | 3 | from django.utils.encoding import force_bytes # noqa 4 | from requests.exceptions import HTTPError 5 | from sentry.http import build_session 6 | from sentry_plugins.exceptions import ApiError 7 | 8 | from sentry_zendesk import logger 9 | 10 | 11 | class ZendeskClient(object): 12 | 13 | SEARCH_URL = '/api/v2/search.json' 14 | CREATE_URL = '/api/v2/tickets.json' 15 | HTTP_TIMEOUT = 5 16 | 17 | def __init__(self, zendesk_url, username, password): 18 | self.zendesk_url = zendesk_url.rstrip('/') 19 | self.username = username 20 | self.password = password 21 | 22 | def create_ticket(self, title, comment, ticket_type, problem_id): 23 | params = { 24 | 'ticket': { 25 | 'type': ticket_type, 26 | 'subject': title, 27 | 'comment': comment, 28 | } 29 | } 30 | if problem_id is not None: 31 | params['ticket']['problem_id'] = problem_id 32 | 33 | response = self.make_request('post', self.CREATE_URL, params) 34 | created_ticket = response.json()['ticket'] 35 | ticket_id = unicode(created_ticket['id']) 36 | logger.info('Created new ticket id "{}"'.format(ticket_id)) 37 | return ticket_id 38 | 39 | def search_tickets(self, query): 40 | params = {'query': 'type:ticket subject:{}*'.format(query)} 41 | response = self.make_request('get', self.SEARCH_URL, params) 42 | return response.json() 43 | 44 | def make_request(self, method, url, payload=None): 45 | if url[:4] != "http": 46 | url = self.zendesk_url + url 47 | auth = self.username.encode('utf8'), self.password.encode('utf8') 48 | session = build_session() 49 | if method == 'get': 50 | response = session.get(url, params=payload, auth=auth, 51 | verify=False, timeout=self.HTTP_TIMEOUT) 52 | else: 53 | response = session.post(url, json=payload, auth=auth, 54 | verify=False, timeout=self.HTTP_TIMEOUT) 55 | 56 | try: 57 | response.raise_for_status() 58 | except HTTPError as e: 59 | raise ApiError.from_response(e.response) 60 | return response 61 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | services: 4 | - redis-server 5 | python: 6 | - '2.7' 7 | cache: 8 | directories: 9 | - .tox 10 | - $HOME/.cache/pip/wheels 11 | - $HOME/virtualenv/python2.7/src 12 | env: 13 | global: 14 | - PIP_DISABLE_PIP_VERSION_CHECK=on 15 | - SENTRY_SKIP_BACKEND_VALIDATION=1 16 | matrix: 17 | - TOX_ENV=py27-sentry811 18 | - TOX_ENV=py27-sentry812 19 | - TOX_ENV=py27-sentry813 20 | - TOX_ENV=py27-sentry814 21 | - TOX_ENV=py27-sentrylatest 22 | - TOX_ENV=coverage 23 | - TOX_ENV=linting 24 | install: 25 | - pip install tox 26 | - pip freeze 27 | script: 28 | - tox -e $TOX_ENV 29 | after_success: 30 | - bash <(curl -s https://codecov.io/bash) 31 | deploy: 32 | # test PyPI 33 | - provider: pypi 34 | distributions: "sdist bdist_wheel" 35 | server: https://testpypi.python.org/pypi 36 | user: gqmelo 37 | password: 38 | secure: "gD2wyAs85+wdRw4EEROpMEQ97hmeUCwJa+KXaJT9Ubsub/EXHxLdDjOSssALYXKhj5BPwEcH/XjuNw2wtGI0zV4tpNfwGHZovOY+cQvGQxabGoh6msbCvgpKjUEHZO96GzZLrQOpxT0XeeguIJXW75e61bhsMDFtAHGaLqaVYZL1O8ucsdUO0cy1QTFWyfpLo5KPuT9zdiuQVncVbT2zicNFGo3Ht7OnLyj1A/PsHptDZ3DqmVv+fnF9tu/jHe7uqSAB8iWDmnueqVpeNLMSRBZYhx4+buFqCJmyLE9yz5LJAb5Wqz5Y1R1Hw6fgMs93vptCcXPjyTyFht7aw/NACeYbxhEoTQmv0fTiYwTCRwYquvk3QYtTn7fuUnrPibDoxDlyqXtcEZhjCTh3qQCKt8LGXO0zrVwAtgWblg5zXwkhZ2jH7rzb85updladhriplZiR9mBUC9a/dmj8TSDDN+zRFw7egksrno1ksATfEW4pDUOE4na7NfWzCkjWtJ+mabT5ebrzAQxwQDVjMuzMNBNGUL6fHp2CH9qA3Mi/4+xkaTsiy0gcl72enO32vMEbdzoZwcdT3DCvpHxiCYrSaZ4KUXnkDt5vhm9QdJwqjICJI4s6nyxCVeHuDyggbp5eT26WiJBmK4eeuTkFTUujHX+flknjl4UIsV+82AGDpFg=" 39 | on: 40 | branch: master 41 | tags: false 42 | repo: ESSS/sentry-zendesk 43 | condition: $TOX_ENV = "py27-sentrylatest" 44 | 45 | # production PyPI 46 | - provider: pypi 47 | distributions: "sdist bdist_wheel" 48 | user: gqmelo 49 | password: 50 | secure: "gD2wyAs85+wdRw4EEROpMEQ97hmeUCwJa+KXaJT9Ubsub/EXHxLdDjOSssALYXKhj5BPwEcH/XjuNw2wtGI0zV4tpNfwGHZovOY+cQvGQxabGoh6msbCvgpKjUEHZO96GzZLrQOpxT0XeeguIJXW75e61bhsMDFtAHGaLqaVYZL1O8ucsdUO0cy1QTFWyfpLo5KPuT9zdiuQVncVbT2zicNFGo3Ht7OnLyj1A/PsHptDZ3DqmVv+fnF9tu/jHe7uqSAB8iWDmnueqVpeNLMSRBZYhx4+buFqCJmyLE9yz5LJAb5Wqz5Y1R1Hw6fgMs93vptCcXPjyTyFht7aw/NACeYbxhEoTQmv0fTiYwTCRwYquvk3QYtTn7fuUnrPibDoxDlyqXtcEZhjCTh3qQCKt8LGXO0zrVwAtgWblg5zXwkhZ2jH7rzb85updladhriplZiR9mBUC9a/dmj8TSDDN+zRFw7egksrno1ksATfEW4pDUOE4na7NfWzCkjWtJ+mabT5ebrzAQxwQDVjMuzMNBNGUL6fHp2CH9qA3Mi/4+xkaTsiy0gcl72enO32vMEbdzoZwcdT3DCvpHxiCYrSaZ4KUXnkDt5vhm9QdJwqjICJI4s6nyxCVeHuDyggbp5eT26WiJBmK4eeuTkFTUujHX+flknjl4UIsV+82AGDpFg=" 51 | on: 52 | tags: true 53 | repo: ESSS/sentry-zendesk 54 | condition: $TOX_ENV = "py27-sentrylatest" 55 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Sentry Zendesk 2 | ============== 3 | 4 | .. image:: https://img.shields.io/pypi/v/sentry-zendesk.svg 5 | :target: https://pypi.python.org/pypi/sentry-zendesk 6 | 7 | .. image:: https://img.shields.io/pypi/pyversions/sentry-zendesk.svg 8 | :target: https://pypi.python.org/pypi/sentry-zendesk 9 | 10 | .. image:: https://img.shields.io/pypi/l/sentry-zendesk.svg 11 | :target: https://pypi.python.org/pypi/sentry-zendesk 12 | 13 | .. image:: https://travis-ci.org/ESSS/sentry-zendesk.svg?branch=master 14 | :target: https://travis-ci.org/ESSS/sentry-zendesk 15 | :alt: See Build Status on Travis CI 16 | 17 | .. image:: https://codecov.io/gh/ESSS/sentry-zendesk/branch/master/graph/badge.svg 18 | :target: https://codecov.io/gh/ESSS/sentry-zendesk?branch=master 19 | :alt: Coverage Status 20 | 21 | 22 | Plugin for Sentry which allows linking Zendesk tickets to Sentry issues 23 | 24 | **DISCLAIMER**: Sentry API under development and `is not frozen `_. 25 | Therefore this plugin is not guaranteed to work with all Sentry versions. Currently it is being 26 | tested against versions **8.11, 8.12, 8.13, 8.14**. 27 | 28 | Features 29 | -------- 30 | 31 | Currently this plugin offers very basic functionality: 32 | 33 | - Manually link a Sentry issue to an existing Zendesk ticket 34 | - Automatically create a Zendesk ticket of type **problem** when a new event arrives 35 | - Automatically create a Zendesk ticket of type **incident** when a recurrent event arrives. In this case, there must be a problem linked to the Sentry issue when the event arrives. 36 | 37 | Limitations 38 | ----------- 39 | 40 | Some features present on similar plugins (e.g. Jira) are not implemented yet for 41 | Zendesk. For example, the following are currently **not possible**: 42 | 43 | - Manually create a new Zendesk ticket through the UI button 44 | - Customize fields of automatically created tickets. Most fields are left blank and the ones that are filled are automatically generated. 45 | - Add comment to the Zendesk ticket when it is linked to a Sentry issue 46 | 47 | Installation 48 | ------------ 49 | 50 | Using pip: 51 | 52 | .. code-block:: bash 53 | 54 | pip install sentry-zendesk 55 | 56 | or from source: 57 | 58 | .. code-block:: bash 59 | 60 | python setup.py install 61 | 62 | Then restart the sentry server. Please note that sentry is composed by multiple 63 | processes. **Make sure you restart all of them** or at least the web and workers 64 | processes. 65 | 66 | If you are using ``docker-compose`` with `onpremise`_ repo you probably can just 67 | add sentry-zendesk to ``requirements.txt`` and restart all services. 68 | 69 | .. _`onpremise`: https://github.com/getsentry/onpremise 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Here's how to set up `sentry-zendesk` for local development: 9 | 10 | 1. You will need a Linux machine. Make sure you have ``g++``, ``python 2.7``, ``virtualenv`` and ``redis`` installed on the system. On Ubuntu you can install with: 11 | 12 | .. code-block:: bash 13 | 14 | sudo apt-get install g++ python2.7 redis-server virtualenv 15 | 16 | 2. Fork the `sentry-zendesk` repo on GitHub. 17 | 3. Clone your fork locally and install flake8 pre-commit hook 18 | 19 | .. code-block:: bash 20 | 21 | git clone https://github.com/your_name_here/sentry-zendesk.git 22 | cd sentry-zendesk 23 | flake8 --install-hook=git 24 | git config --local flake8.strict true 25 | 26 | 4. Create a new virtualenv environment for developing 27 | 28 | .. code-block:: bash 29 | 30 | virtualenv -p python2.7 venv 31 | source venv/bin/activate # If using bash, otherwise use the appropriate activate script 32 | pip install -r requirements_dev.txt 33 | 34 | 4. Create a branch for local development 35 | 36 | .. code-block:: bash 37 | 38 | git checkout -b name-of-your-bugfix-or-feature 39 | 40 | Now you can make your changes locally. 41 | 42 | 5. After each change make sure the tests still pass 43 | 44 | .. code-block:: bash 45 | 46 | py.test tests 47 | 48 | 49 | 6. Commit your changes and push your branch to GitHub 50 | 51 | .. code-block:: bash 52 | 53 | git add . 54 | git commit -m "Your detailed description of your changes." 55 | git push origin name-of-your-bugfix-or-feature 56 | 57 | 7. Submit a pull request through the GitHub website. 58 | 59 | Pull Request Guidelines 60 | ----------------------- 61 | 62 | Before you submit a pull request, check that it meets these guidelines: 63 | 64 | 1. The pull request should include tests. 65 | 2. If the pull request adds functionality, the docs should be updated. Please 66 | add the feature to the list in README.rst. 67 | 3. The pull request should work for all sentry versions being tested on CI. Check 68 | https://travis-ci.org/ESSS/sentry-zendesk/pull_requests 69 | and make sure that the tests pass for all supported Sentry versions. 70 | 4. Additionally if the changes are complex or make use of new methods/hooks 71 | executed by the sentry server it is recommended to test against a full sentry 72 | server (unless you can find a good way to write integrated tests). To run a 73 | a sentry server locally the easiest option is the `onpremise`_ repo. 74 | 75 | Tips 76 | ---- 77 | 78 | To run a specific test: 79 | 80 | .. code-block:: bash 81 | 82 | py.test tests -k test_name 83 | 84 | Sometimes is very useful to see a coverage report to check if you are forgetting 85 | to test something. To generate an html report: 86 | 87 | .. code-block:: bash 88 | 89 | py.test --cov sentry_zendesk --cov-config .coveragerc --cov-report html tests 90 | 91 | .. _`onpremise`: https://github.com/getsentry/onpremise 92 | -------------------------------------------------------------------------------- /sentry_zendesk/plugin.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import absolute_import, print_function, unicode_literals 3 | 4 | from django.conf.urls import url 5 | from rest_framework.response import Response 6 | from sentry.models import GroupMeta 7 | from sentry.plugins.bases.issue2 import IssuePlugin2, IssueGroupActionEndpoint 8 | from sentry.utils.http import absolute_uri 9 | from sentry_plugins.utils import get_secret_field_config 10 | 11 | from sentry_zendesk import logger 12 | 13 | from . import VERSION 14 | 15 | 16 | class ZendeskPlugin(IssuePlugin2): 17 | title = 'Zendesk' 18 | slug = 'sentry_zendesk' 19 | description = 'Provides linking Zendesk tickets to Sentry issues.' 20 | version = VERSION 21 | author = 'ESSS' 22 | author_url = 'https://github.com/ESSS/sentry-zendesk' 23 | resource_links = [ 24 | ('Bug Tracker', 'https://github.com/ESSS/sentry-zendesk/issues'), 25 | ('Source', 'https://github.com/ESSS/sentry-zendesk'), 26 | ] 27 | 28 | conf_key = 'sentry_zendesk' 29 | conf_title = title 30 | 31 | # Disable create action until it is implemented 32 | allowed_actions = ('link', 'unlink') 33 | 34 | def get_group_urls(self): 35 | _patterns = super(ZendeskPlugin, self).get_group_urls() 36 | _patterns.append( 37 | url( 38 | r'^autocomplete', 39 | IssueGroupActionEndpoint.as_view( 40 | view_method_name='view_autocomplete', 41 | plugin=self 42 | ) 43 | ) 44 | ) 45 | return _patterns 46 | 47 | def is_configured(self, request, project, **kwargs): 48 | """ 49 | Used by sentry to know if this plugin hooks should be executed. 50 | If the plugin is not configured its behavior is like if it was 51 | disabled. 52 | """ 53 | 54 | if not self.get_option('zendesk_url', project): 55 | return False 56 | return True 57 | 58 | def get_config(self, *args, **kwargs): 59 | """ 60 | Called by the web process when user wants to configure the plugin. 61 | """ 62 | project = kwargs['project'] 63 | pw = self.get_option('password', project) 64 | secret_field = get_secret_field_config(pw, '') 65 | secret_field.update({ 66 | 'name': 'password', 67 | 'label': 'Password' 68 | }) 69 | 70 | return [{ 71 | 'name': 'zendesk_url', 72 | 'label': 'Zendesk URL', 73 | 'default': self.get_option('zendesk_url', project), 74 | 'type': 'text', 75 | 'placeholder': 'e.g. "https://mycompany.zendesk.com"', 76 | 'help': 'It must be visible to the Sentry server' 77 | }, { 78 | 'name': 'username', 79 | 'label': 'Username', 80 | 'default': self.get_option('username', project), 81 | 'type': 'text', 82 | 'help': 'Ensure the Zendesk user has admin permissions on the ' 83 | 'project' 84 | }, secret_field, { 85 | 'name': 'auto_create_problems', 86 | 'label': 'Automatically create Zendesk problems', 87 | 'default': self.get_option('auto_create_problems', project) or False, # noqa 88 | 'type': 'bool', 89 | 'required': False, 90 | 'help': 'Automatically create a Zendesk ticket of type problem ' 91 | 'for EVERY new issue' 92 | }, { 93 | 'name': 'auto_create_incidents', 94 | 'label': 'Automatically create Zendesk incidents', 95 | 'default': self.get_option('auto_create_incidents', project) or False, # noqa 96 | 'type': 'bool', 97 | 'required': False, 98 | 'help': 'Automatically create a Zendesk ticket of type incident ' \ 99 | 'for EVERY event after the first one, linking it to the ' \ 100 | 'previously created problem.' 101 | }] 102 | 103 | def post_process(self, group, event, is_new, is_sample, **kwargs): 104 | """ 105 | Called by the worker process whenever a new event arrives. 106 | """ 107 | logger.info('event: {}, is_new: {}'.format(event, is_new)) 108 | 109 | if is_new: 110 | if not self.get_option('auto_create_problems', group.project): 111 | return 112 | logger.info('New problem') 113 | problem_id = self._get_linked_ticket(group) 114 | if problem_id: 115 | logger.error('There is already a problem linked to this event') 116 | return 117 | 118 | logger.info('Creating new problem') 119 | ticket_id = self._create_ticket( 120 | group, event, ticket_type='problem') 121 | GroupMeta.objects.set_value( 122 | group, '%s:tid' % self.get_conf_key(), ticket_id) 123 | elif self.get_option('auto_create_incidents', group.project): 124 | problem_id = self._get_linked_ticket(group) 125 | if not problem_id: 126 | logger.info( 127 | 'Cannot create incident because there is no linked problem' 128 | ) 129 | return 130 | 131 | logger.info( 132 | 'Creating new incident linked to problem "{}"' 133 | .format(problem_id)) 134 | self._create_ticket( 135 | group, event, ticket_type='incident', problem_id=problem_id) 136 | 137 | def _get_linked_ticket(self, group): 138 | # XXX(dcramer): Sentry doesn't expect GroupMeta referenced here so we 139 | # need to populate the cache 140 | GroupMeta.objects.populate_cache([group]) 141 | problem_id = GroupMeta.objects.get_value( 142 | group, '%s:tid' % self.get_conf_key(), None) 143 | return problem_id 144 | 145 | def _create_ticket(self, group, event, ticket_type, problem_id=None): 146 | client = self.get_client(group.project) 147 | title = self.get_group_title(None, group, event) 148 | comment = '[{0}]({0})'.format(absolute_uri(group.get_absolute_url())) 149 | return client.create_ticket(title=title, 150 | ticket_type=ticket_type, 151 | problem_id=problem_id, 152 | comment=comment) 153 | 154 | def get_link_existing_issue_fields(self, request, group, event, **kwargs): 155 | """ 156 | Called by the web process when showing a dialog to link to an external 157 | issue (ticket) 158 | """ 159 | return [{ 160 | 'name': 'issue_id', 161 | 'label': 'Ticket', 162 | 'default': '', 163 | 'type': 'select', 164 | 'has_autocomplete': True 165 | }, { 166 | 'name': 'comment', 167 | 'label': 'Comment', 168 | 'default': absolute_uri(group.get_absolute_url()), 169 | 'type': 'textarea', 170 | 'help': ('Leave blank if you don\'t want to ' 171 | 'add a comment to the Zendesk ticket.'), 172 | 'required': False 173 | }] 174 | 175 | def get_issue_url(self, group, issue_id, **kwargs): 176 | """ 177 | Called by the web process to show a link of the external issue (ticket) 178 | """ 179 | instance = self.get_option('zendesk_url', group.project) 180 | return "%s/tickets/%s" % (instance, issue_id) 181 | 182 | def view_autocomplete(self, request, group, **kwargs): 183 | """ 184 | Called by the web process when user wants to link sentry issue to an 185 | existing Zendesk ticket. 186 | """ 187 | query = request.GET.get('autocomplete_query') 188 | field = request.GET.get('autocomplete_field') 189 | client = self.get_client(group.project) 190 | 191 | data = client.search_tickets(query) 192 | issues = [{ 193 | 'text': '(%s) %s' % (i['id'], i['subject']), 194 | 'id': unicode(i['id']) 195 | } for i in data.get('results', [])] 196 | 197 | return Response({field: issues}) 198 | 199 | def get_client(self, project): 200 | from sentry_zendesk.client import ZendeskClient 201 | 202 | url = self.get_option('zendesk_url', project) 203 | username = self.get_option('username', project) 204 | password = self.get_option('password', project) 205 | return ZendeskClient(url, username, password) 206 | 207 | def create_issue(self, request, group, form_data, **kwargs): 208 | """ 209 | Called by the web process when the user wants to create a new Zendesk 210 | ticket and link to the sentry issue. 211 | """ 212 | raise NotImplementedError('This feature is not implemented yet') 213 | 214 | def link_issue(self, request, group, form_data, **kwargs): 215 | """ 216 | Called by the web process to link to an existing Zendesk ticket 217 | """ 218 | # TODO: Add comment to Zendesk ticket with sentry url 219 | return { 220 | 'title': form_data['issue_id'] 221 | } 222 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016 Sentry 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function, unicode_literals 2 | 3 | from urllib import urlencode 4 | 5 | from django.test import RequestFactory 6 | from exam import fixture 7 | from sentry.testutils import TestCase 8 | from sentry.utils import json 9 | from sentry_plugins.exceptions import ApiError 10 | import pytest 11 | import responses 12 | 13 | from sentry_zendesk.plugin import ZendeskPlugin 14 | 15 | 16 | class ZendeskPluginTest(TestCase): 17 | 18 | @fixture 19 | def plugin(self): 20 | return ZendeskPlugin() 21 | 22 | @fixture 23 | def request(self): 24 | return RequestFactory() 25 | 26 | def test_conf_key(self): 27 | assert self.plugin.conf_key == 'sentry_zendesk' 28 | 29 | def test_get_issue_label(self): 30 | group = self.create_group(message='Hello world', culprit='foo.bar') 31 | assert self.plugin.get_issue_label(group, '12345') == '#12345' 32 | 33 | def test_get_issue_url(self): 34 | self.plugin.set_option( 35 | 'zendesk_url', 'https://foocompany.zendesk.com', self.project) 36 | group = self.create_group(message='Hello world', culprit='foo.bar') 37 | assert self.plugin.get_issue_url( 38 | group, '12345') == 'https://foocompany.zendesk.com/tickets/12345' 39 | 40 | def test_is_configured(self): 41 | assert self.plugin.is_configured(None, self.project) is False 42 | self.plugin.set_option( 43 | 'zendesk_url', 'https://foocompany.zendesk.com', self.project) 44 | assert self.plugin.is_configured(None, self.project) is True 45 | 46 | @responses.activate 47 | def test_dont_create_problem_after_event_when_option_is_disabled(self): 48 | self._configure_plugin() 49 | self.plugin.set_option('auto_create_problems', False, self.project) 50 | group = self.create_group(message='Hello world', culprit='foo.bar') 51 | 52 | # Should do nothing, and therefore not raise 53 | self.plugin.post_process( 54 | group, event=self.event, is_new=True, is_sample=False) 55 | 56 | def _configure_plugin(self): 57 | self.plugin.set_option( 58 | 'zendesk_url', 'https://foocompany.zendesk.com', self.project) 59 | self.plugin.set_option('username', 'Bob', self.project) 60 | self.plugin.set_option('password', 'bob123', self.project) 61 | 62 | @responses.activate 63 | def test_create_problem_after_event_raises_on_http_error(self): 64 | self._configure_plugin() 65 | self.plugin.set_option('auto_create_problems', True, self.project) 66 | group = self.create_group(message='Hello world', culprit='foo.bar') 67 | 68 | responses.add( 69 | responses.POST, 70 | 'https://foocompany.zendesk.com/api/v2/tickets.json', 71 | body='Error creating ticket', 72 | status=500, 73 | ) 74 | 75 | with pytest.raises(ApiError): 76 | self.plugin.post_process( 77 | group, event=self.event, is_new=True, is_sample=False) 78 | 79 | @responses.activate 80 | def test_create_problem_after_event_does_nothing_when_already_linked(self): 81 | """ 82 | In the case it is possible for a new event to arrive, but there is 83 | already a ticket linked to the sentry issue, nothing should be done. 84 | 85 | This behavior may be reconsidered in the future. Maybe it is better 86 | to raise some error. 87 | """ 88 | from sentry.models.groupmeta import GroupMeta 89 | 90 | self._configure_plugin() 91 | self.plugin.set_option('auto_create_problems', True, self.project) 92 | group = self.create_group(message='Hello world', culprit='foo.bar') 93 | GroupMeta.objects.set_value(group, 94 | '%s:tid' % self.plugin.get_conf_key(), 95 | '12345') 96 | 97 | # Should not raise 98 | self._process_new_event(group) 99 | 100 | @responses.activate 101 | def test_create_problem_after_event(self): 102 | self._configure_plugin() 103 | self.plugin.set_option('auto_create_problems', True, self.project) 104 | group = self.create_group(message='Hello world', culprit='foo.bar') 105 | 106 | self._process_new_event(group) 107 | 108 | assert len(responses.calls) == 1 109 | sent_data = json.loads(responses.calls[0].request.body) 110 | assert sent_data == { 111 | 'ticket': { 112 | 'comment': '[http://testserver/baz/bar/issues/1/](http://testserver/baz/bar/issues/1/)', # noqa 113 | 'type': 'problem', 114 | 'subject': self.event.error(), 115 | } 116 | } 117 | # Newly created problem should be linked to the sentry issue 118 | assert self._get_linked_ticket_id(group) == unicode( 119 | create_problem_response['ticket']['id']) 120 | 121 | def _process_new_event(self, group): 122 | responses.add( 123 | responses.POST, 124 | 'https://foocompany.zendesk.com/api/v2/tickets.json', 125 | json=create_problem_response, 126 | content_type='application/json', 127 | ) 128 | 129 | self.plugin.post_process( 130 | group, event=self.event, is_new=True, is_sample=False) 131 | 132 | def _process_repeated_event(self, group): 133 | responses.add( 134 | responses.POST, 135 | 'https://foocompany.zendesk.com/api/v2/tickets.json', 136 | json=create_incident_response, 137 | content_type='application/json', 138 | ) 139 | 140 | self.plugin.post_process( 141 | group, event=self.event, is_new=False, is_sample=False) 142 | 143 | def _get_linked_ticket_id(self, group): 144 | from sentry.models.groupmeta import GroupMeta 145 | return GroupMeta.objects.get_value( 146 | group, '%s:tid' % self.plugin.get_conf_key()) 147 | 148 | @responses.activate 149 | def test_dont_create_incident_after_event_when_option_is_disabled(self): 150 | self._configure_plugin() 151 | self.plugin.set_option('auto_create_problems', True, self.project) 152 | self.plugin.set_option('auto_create_incidents', False, self.project) 153 | group = self.create_group(message='Hello world', culprit='foo.bar') 154 | 155 | # Should do nothing, and therefore not raise 156 | self.plugin.post_process( 157 | group, event=self.event, is_new=False, is_sample=False) 158 | 159 | @responses.activate 160 | def test_dont_create_incident_after_event_when_no_problem_is_linked(self): 161 | self._configure_plugin() 162 | self.plugin.set_option('auto_create_problems', False, self.project) 163 | self.plugin.set_option('auto_create_incidents', True, self.project) 164 | group = self.create_group(message='Hello world', culprit='foo.bar') 165 | 166 | # As there is no problem previously linked, there is no way to create 167 | # an incident. Should do nothing, and therefore not raise 168 | self.plugin.post_process( 169 | group, event=self.event, is_new=False, is_sample=False) 170 | 171 | @responses.activate 172 | def test_create_incident_after_event_when_problem_is_already_linked(self): 173 | """ 174 | Even if auto_create_problems setting is disabled, and incident should 175 | be created if the user manually linked to a problem. 176 | """ 177 | from sentry.models.groupmeta import GroupMeta 178 | 179 | self._configure_plugin() 180 | self.plugin.set_option('auto_create_problems', False, self.project) 181 | self.plugin.set_option('auto_create_incidents', True, self.project) 182 | group = self.create_group(message='Hello world', culprit='foo.bar') 183 | # Emulates as if the user had manually linked to a ticket 184 | GroupMeta.objects.set_value(group, 185 | '%s:tid' % self.plugin.get_conf_key(), 186 | '12345') 187 | 188 | self._process_repeated_event(group) 189 | 190 | assert len(responses.calls) == 1 191 | sent_data = json.loads(responses.calls[0].request.body) 192 | assert sent_data == { 193 | 'ticket': { 194 | 'comment': '[http://testserver/baz/bar/issues/1/](http://testserver/baz/bar/issues/1/)', # noqa 195 | 'type': 'incident', 196 | 'problem_id': '12345', 197 | 'subject': self.event.error() 198 | } 199 | } 200 | # Original problem created on first event should still be linked to the 201 | # sentry issue 202 | assert self._get_linked_ticket_id(group) == '12345' 203 | 204 | @responses.activate 205 | def test_create_incident_after_event_raises_on_http_error(self): 206 | from sentry.models.groupmeta import GroupMeta 207 | 208 | self._configure_plugin() 209 | self.plugin.set_option('auto_create_incidents', True, self.project) 210 | group = self.create_group(message='Hello world', culprit='foo.bar') 211 | # Emulates as if the user had manually linked to a ticket 212 | GroupMeta.objects.set_value(group, 213 | '%s:tid' % self.plugin.get_conf_key(), 214 | '12345') 215 | 216 | responses.add( 217 | responses.POST, 218 | 'https://foocompany.zendesk.com/api/v2/tickets.json', 219 | body='Error creating ticket', 220 | status=500, 221 | ) 222 | 223 | with pytest.raises(ApiError): 224 | self.plugin.post_process( 225 | group, event=self.event, is_new=False, is_sample=False) 226 | 227 | @responses.activate 228 | def test_create_problem_and_incident_after_two_events(self): 229 | self._configure_plugin() 230 | self.plugin.set_option('auto_create_problems', True, self.project) 231 | self.plugin.set_option('auto_create_incidents', True, self.project) 232 | group = self.create_group(message='Hello world', culprit='foo.bar') 233 | 234 | self._process_new_event(group) 235 | self._process_repeated_event(group) 236 | 237 | assert len(responses.calls) == 2 238 | sent_data = json.loads(responses.calls[1].request.body) 239 | assert sent_data == { 240 | 'ticket': { 241 | 'comment': '[http://testserver/baz/bar/issues/1/](http://testserver/baz/bar/issues/1/)', # noqa 242 | 'type': 'incident', 243 | 'problem_id': unicode(create_problem_response['ticket']['id']), 244 | 'subject': self.event.error() 245 | } 246 | } 247 | # Original problem created on first event should still be linked to the 248 | # sentry issue 249 | assert self._get_linked_ticket_id(group) == unicode( 250 | create_problem_response['ticket']['id']) 251 | 252 | @responses.activate 253 | def test_search_when_autocompleting(self): 254 | self._configure_plugin() 255 | group = self.create_group(message='Hello world', culprit='foo.bar') 256 | 257 | responses.add( 258 | responses.GET, 'https://foocompany.zendesk.com/api/v2/search.json', 259 | json=search_response, 260 | content_type='application/json', 261 | ) 262 | # This would be the request sent when the user fills the issue field on 263 | # UI 264 | request = self.request.get( 265 | '/', 266 | data={'autocomplete_query': 'foo', 267 | 'autocomplete_field': 'issue_id'} 268 | ) 269 | 270 | assert self.plugin.view_autocomplete(request, group).data == { 271 | 'issue_id': [ 272 | {'id': '4178', 'text': '(4178) Cannot run foo'}, 273 | {'id': '5289', 'text': '(5289) Problem running bar with foo'} 274 | ]} 275 | assert len(responses.calls) == 1 276 | assert urlencode({'query': 'type:ticket subject:foo*'} 277 | ) in responses.calls[0].request.url 278 | 279 | @responses.activate 280 | def test_search_when_autocompleting_raises_on_http_error(self): 281 | self._configure_plugin() 282 | group = self.create_group(message='Hello world', culprit='foo.bar') 283 | 284 | responses.add( 285 | responses.GET, 'https://foocompany.zendesk.com/api/v2/search.json', 286 | body='Error searching', 287 | status=500, 288 | ) 289 | # This would be the request sent when the user fills the issue field on 290 | # UI 291 | request = self.request.get( 292 | '/', 293 | data={'autocomplete_query': 'foo', 294 | 'autocomplete_field': 'issue_id'} 295 | ) 296 | 297 | with pytest.raises(ApiError): 298 | self.plugin.view_autocomplete(request, group) 299 | 300 | 301 | problem_ticket = { 302 | 'allow_channelback': False, 303 | 'assignee_id': 111222333, 304 | 'brand_id': 120120, 305 | 'collaborator_ids': [], 306 | 'created_at': '2011-01-17T15:41:29Z', 307 | 'custom_fields': [{'id': 23000032, 'value': 'basic'}], 308 | 'description': 'I had some problems while running foo', 309 | 'due_at': None, 310 | 'external_id': None, 311 | 'fields': [{'id': 23000032, 'value': 'basic'}], 312 | 'forum_topic_id': None, 313 | 'group_id': 21617466, 314 | 'has_incidents': False, 315 | 'id': 4178, 316 | 'is_public': True, 317 | 'organization_id': 274274274, 318 | 'priority': None, 319 | 'problem_id': None, 320 | 'raw_subject': 'Cannot run foo', 321 | 'recipient': None, 322 | 'requester_id': 341341341, 323 | 'result_type': 'ticket', 324 | 'satisfaction_rating': {'score': 'unoffered'}, 325 | 'sharing_agreement_ids': [20022002], 326 | 'status': 'open', 327 | 'subject': 'Cannot run foo', 328 | 'submitter_id': 18161816, 329 | 'tags': ['basic', 'foo'], 330 | 'type': 'problem', 331 | 'updated_at': '2011-01-17T15:41:29Z', 332 | 'url': 'https://foocompany.zendesk.com/api/v2/tickets/4178.json', 333 | 'via': { 334 | 'channel': 'web', 335 | 'source': { 336 | 'from': {'subject': 'Cannot run foo', 'ticket_id': 4193}, 337 | 'rel': 'follow_up', 338 | 'to': {} 339 | } 340 | } 341 | } 342 | 343 | 344 | incident_ticket = { 345 | 'allow_channelback': False, 346 | 'assignee_id': 111222333, 347 | 'brand_id': 120120, 348 | 'collaborator_ids': [], 349 | 'created_at': '2013-08-13T10:59:34Z', 350 | 'custom_fields': [{'id': 23000032, 'value': ''}], 351 | 'description': 'After installing foo I can\'t run bar', 352 | 'due_at': None, 353 | 'external_id': None, 354 | 'fields': [{'id': 23000032, 'value': ''}], 355 | 'forum_topic_id': None, 356 | 'group_id': 21617466, 357 | 'has_incidents': False, 358 | 'id': 5289, 359 | 'is_public': True, 360 | 'organization_id': 274274274, 361 | 'priority': None, 362 | 'problem_id': 4178, 363 | 'raw_subject': 'Problem running bar with foo', 364 | 'recipient': None, 365 | 'requester_id': 18161816, 366 | 'result_type': 'ticket', 367 | 'satisfaction_rating': {'score': 'unoffered'}, 368 | 'sharing_agreement_ids': [20022002], 369 | 'status': 'open', 370 | 'subject': 'Problem running bar with foo', 371 | 'submitter_id': 18161816, 372 | 'tags': ['foo', 'bar'], 373 | 'type': 'incident', 374 | 'updated_at': '2013-08-13T11:59:34Z', 375 | 'url': 'https://foocompany.zendesk.com/api/v2/tickets/5289.json', 376 | 'via': {'channel': 'web', 377 | 'source': {'from': {}, 'rel': None, 'to': {}}} 378 | } 379 | 380 | 381 | search_response = { 382 | 'count': 2, 383 | 'facets': None, 384 | 'next_page': None, 385 | 'previous_page': None, 386 | 'results': [problem_ticket, incident_ticket] 387 | } 388 | 389 | 390 | create_problem_response = { 391 | 'audit': { 392 | 'author_id': 111222111, 393 | 'created_at': '2017-03-30T19:52:24Z', 394 | 'events': [{'attachments': [], 395 | 'audit_id': 191819181918, 396 | 'author_id': 111222111, 397 | 'body': 'I had some problems while running foo', 398 | 'html_body': '
\n

I had some problems while running foo

', # noqa 399 | 'id': 191874783063, 400 | 'plain_body': 'I had some problems while running foo', 401 | 'public': True, 402 | 'type': 'Comment'}, 403 | {'field_name': 'subject', 404 | 'id': 191874783223, 405 | 'type': 'Create', 406 | 'value': 'Cannot run foo'}], 407 | 'id': 191819181918, 408 | 'metadata': {'custom': {}, 409 | 'system': {'client': 'python-requests/2.12.5', 410 | 'ip_address': '174.89.241.195', 411 | 'latitude': -26.38399899999999, # noqa 412 | 'location': 'Unknown City, 26, Some Country', 413 | 'longitude': -124.10156399999999}}, # noqa 414 | 'ticket_id': 4178, 415 | 'via': {'channel': 'api', 416 | 'source': {'from': {}, 'rel': None, 'to': {}}}}, 417 | 'ticket': problem_ticket 418 | } 419 | 420 | 421 | create_incident_response = { 422 | 'audit': { 423 | 'author_id': 111222111, 424 | 'created_at': '2017-03-30T19:50:28Z', 425 | 'events': [{'attachments': [], 426 | 'audit_id': 191874224843, 427 | 'author_id': 111222111, 428 | 'body': 'After installing foo I can\'t run bar', 429 | 'html_body': '
\n

After installing foo I can\'t run bar

', # noqa 430 | 'id': 191874224863, 431 | 'plain_body': 'After installing foo I can\'t run bar', 432 | 'public': True, 433 | 'type': 'Comment'}, 434 | {'field_name': 'subject', 435 | 'id': 191874224903, 436 | 'type': 'Create', 437 | 'value': 'Problem running bar with foo'}, 438 | {'field_name': 'assignee_id', 439 | 'id': 191874224963, 440 | 'type': 'Create', 441 | 'value': '111222111'}, 442 | ], 443 | 'id': 191874224843, 444 | 'metadata': {'custom': {}, 445 | 'system': {'client': 'python-requests/2.12.5', 446 | 'ip_address': '174.89.241.195', 447 | 'latitude': -26.38399899999999, # noqa 448 | 'location': 'Unknown City, 26, Some Country', 449 | 'longitude': -124.10156399999999}}, # noqa 450 | 'ticket_id': 5289, 451 | 'via': {'channel': 'api', 452 | 'source': {'from': {}, 'rel': None, 'to': {}}}}, 453 | 'ticket': incident_ticket 454 | } 455 | --------------------------------------------------------------------------------