├── 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': '
', # 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': '', # 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 |
--------------------------------------------------------------------------------
I had some problems while running foo