├── .gitignore ├── .gitmodules ├── docs ├── _static │ └── flask-exceptional.png ├── Makefile ├── make.bat ├── index.rst └── conf.py ├── .travis.yml ├── setup.cfg ├── MANIFEST.in ├── Makefile ├── README.md ├── LICENSE ├── setup.py ├── CHANGES ├── tests.py └── flask_exceptional.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.egg 3 | *.egg-info 4 | build/ 5 | dist/ 6 | docs/_build -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git://github.com/mitsuhiko/flask-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /docs/_static/flask-exceptional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jzempel/flask-exceptional/HEAD/docs/_static/flask-exceptional.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | install: "python setup.py install" 6 | script: "python setup.py test" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/_build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/_build/html -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES LICENSE 2 | recursive-include docs * 3 | recursive-exclude docs *.pyc 4 | recursive-exclude docs *.pyo 5 | prune docs/_build 6 | prune docs/_themes/.git 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean upload 2 | 3 | clean: 4 | rm -rf Flask_Exceptional.egg-info 5 | rm -rf dist 6 | rm -rf docs/_build 7 | rm -rf docs/_themes 8 | git checkout docs/_themes 9 | 10 | upload: docs/_build 11 | python setup.py sdist upload 12 | python setup.py upload_sphinx 13 | 14 | docs/_build: docs/_themes/README 15 | python setup.py develop 16 | python setup.py build_sphinx 17 | 18 | docs/_themes/README: 19 | git submodule init 20 | git submodule update 21 | git submodule foreach git pull origin master 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Flask-Exceptional 2 | 3 | [Exceptional](http://new.exceptional.io) has announced that they are 4 | discontinuing support for their legacy error tracking product in favor 5 | of [Airbrake.io](https://airbrake.io). Effective **November 2014** the 6 | Exceptional error tracking product will be decommissioned. 7 | 8 | ![Bye-bye Exceptional. Hello Airbrake!](http://cl.ly/image/3l3d1F2O293Z/unnamed.png) 9 | 10 | Please transition ASAP to an Airbrake library (i.e. 11 | [Airbrake Python](https://github.com/airbrake/airbrake-python)). 12 | 13 | Cheers, 14 | Jonathan. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 by Jonathan Zempel. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Exceptional 3 | ----------------- 4 | 5 | Adds `Exceptional`_ support to Flask applications. 6 | 7 | Links 8 | ````` 9 | 10 | * `documentation `_ 11 | * `development version 12 | `_ 13 | 14 | .. _Exceptional: http://www.exceptional.io/ 15 | 16 | """ 17 | 18 | from setuptools import setup 19 | from sys import argv, version_info 20 | 21 | if version_info < (2, 6): 22 | install_requires = ['Flask', 'simplejson >= 1.9.1'] 23 | else: 24 | install_requires = ['Flask'] 25 | 26 | if "develop" in argv: 27 | install_requires.append('Sphinx') 28 | install_requires.append('Sphinx-PyPI-upload') 29 | 30 | setup( 31 | name='Flask-Exceptional', 32 | version='0.5.4', 33 | url='http://github.com/jzempel/flask-exceptional', 34 | license='BSD', 35 | author='Jonathan Zempel', 36 | author_email='jzempel@gmail.com', 37 | description='Adds Exceptional support to Flask applications', 38 | long_description=__doc__, 39 | py_modules=['flask_exceptional'], 40 | zip_safe=False, 41 | platforms='any', 42 | install_requires=install_requires, 43 | test_suite='tests', 44 | classifiers=[ 45 | 'Development Status :: 4 - Beta', 46 | 'Environment :: Web Environment', 47 | 'Intended Audience :: Developers', 48 | 'License :: OSI Approved :: BSD License', 49 | 'Operating System :: OS Independent', 50 | 'Programming Language :: Python', 51 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 52 | 'Topic :: Software Development :: Libraries :: Python Modules' 53 | ] 54 | ) 55 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | Version 0.5.4 5 | ^^^^^^^^^^^^^ 6 | 7 | * Updated JSON implementation import to work with Flask 0.10. 8 | 9 | Version 0.5.3 10 | ^^^^^^^^^^^^^ 11 | 12 | * Fixed :meth:`Exceptional.publish` to no longer dereference a request context. 13 | 14 | Version 0.5.2 15 | ^^^^^^^^^^^^^ 16 | 17 | * Unwind broken _app_ctx_stack usage. 18 | 19 | Version 0.5.1 20 | ^^^^^^^^^^^^^ 21 | 22 | * Handle malformed HTTP response status-line from Exceptional. 23 | 24 | Version 0.5 25 | ^^^^^^^^^^^ 26 | 27 | * Updated with Flask 0.8 extension structure recommendations and 0.9 28 | _app_ctx_stack. 29 | * Added ``{'application_environment': 'loaded_libraries': {...}}`` API data. 30 | 31 | Version 0.4.9 32 | ^^^^^^^^^^^^^ 33 | 34 | * Added the :meth:`Exceptional.context` method to support Exceptional's extra 35 | context data API. 36 | * Updated to reference the new exceptional.io domain. 37 | 38 | Version 0.4.8 39 | ^^^^^^^^^^^^^ 40 | 41 | * Updated to publish UTF-8 encoded data to Exceptional. 42 | * Added support for ``request.json`` data. 43 | 44 | Version 0.4.7 45 | ^^^^^^^^^^^^^ 46 | 47 | * Added the :meth:`Exceptional.publish` method to support Exceptional tracking 48 | outside the context of a request. 49 | 50 | Version 0.4.6 51 | ^^^^^^^^^^^^^ 52 | 53 | * Corrected ``occurred_at`` timestamp to be formatted as Zulu. 54 | * Fixed JSON serialization issue by coercing all environment variables to 55 | strings. 56 | 57 | Version 0.4.5 58 | ^^^^^^^^^^^^^ 59 | 60 | * Updated to log a warning on repeated extension initialization attempts. 61 | 62 | Version 0.4.4 63 | ^^^^^^^^^^^^^ 64 | 65 | * Fixed to workaround Python 2.5 issue where :meth:`urlopen` raises a 66 | :class:`HTTPError` even though the HTTP response code indicates success. 67 | 68 | Version 0.4.3 69 | ^^^^^^^^^^^^^ 70 | 71 | * Changed so that ``app.extensions['exceptional']`` targets the 72 | :class:`Exceptional` extension instance. 73 | 74 | Version 0.4.2 75 | ^^^^^^^^^^^^^ 76 | 77 | * Updated to support Python 2.5. 78 | 79 | Version 0.4.1 80 | ^^^^^^^^^^^^^ 81 | 82 | * Updated to support Flask 0.7 blueprints. 83 | 84 | Version 0.4 85 | ^^^^^^^^^^^ 86 | 87 | * Updated to support Python 2.6. 88 | * Added `EXCEPTIONAL_DEBUG_URL` testing environment variable override. 89 | 90 | Version 0.3 91 | ^^^^^^^^^^^ 92 | 93 | * Updated to handle unreachable Exceptional service API. 94 | 95 | Version 0.2 96 | ^^^^^^^^^^^ 97 | 98 | * Added :meth:`Exceptional.test` method. 99 | 100 | Version 0.1 101 | ^^^^^^^^^^^ 102 | 103 | * Initial public release. 104 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Exceptional.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Exceptional.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Exceptional" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Exceptional" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Exceptional.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Exceptional.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-Exceptional 2 | ================= 3 | 4 | .. module:: flask.ext.exceptional 5 | 6 | Flask-Exceptional adds `Exceptional`_ support to `Flask`_. Exceptional 7 | tracks errors in your application, reports them real-time, and gathers 8 | the info you need to fix them fast. Visit http://www.exceptional.io 9 | to give it a try. 10 | 11 | Installation 12 | ------------ 13 | 14 | The remaining documentation assumes you have access to an Exceptional 15 | account. Installing the extension is simple with pip:: 16 | 17 | $ pip install Flask-Exceptional 18 | 19 | or alternatively with easy_install:: 20 | 21 | $ easy_install Flask-Exceptional 22 | 23 | Quickstart 24 | ---------- 25 | 26 | After installing Flask-Exceptional, all you have to do is create a Flask 27 | application, configure the Exceptional API key, and create the 28 | :class:`Exceptional` object. It's this easy:: 29 | 30 | from flask import Flask 31 | from flask.ext.exceptional import Exceptional 32 | 33 | app = Flask(__name__) 34 | app.config["EXCEPTIONAL_API_KEY"] = "exceptional_forty_character_unique_key" 35 | exceptional = Exceptional(app) 36 | 37 | Your application is configured for cloud-based error monitoring! You can 38 | verify your configuration is working by calling the 39 | :meth:`Exceptional.test` method:: 40 | 41 | Exceptional.test(app.config) 42 | 43 | Check out the following section for more detail on the available 44 | Flask-Exceptional configuration settings. 45 | 46 | Configuration 47 | ------------- 48 | 49 | The following configuration settings exist for Flask-Exceptional: 50 | 51 | =================================== ====================================== 52 | `EXCEPTIONAL_API_KEY` The Exceptional API key for your 53 | application. Login to Exceptional, 54 | select your app, and click the *APP 55 | SETTINGS* link. The displayed API key 56 | is the value to use here. 57 | 58 | Attempting to create the extension 59 | without supplying an API key will 60 | result in a logged warning, but the 61 | app will continue to run as normal. 62 | `EXCEPTIONAL_DEBUG_URL` If your app is running in debug mode, 63 | errors are not tracked with 64 | Exceptional. Configure this value to 65 | capture error data in debug mode. For 66 | example, you may use a `RequestBin`_ 67 | URL to debug your application. JSON 68 | error data is POSTed uncompressed to 69 | this URL, whereas Exceptional requires 70 | the data to be compressed. 71 | `EXCEPTIONAL_HTTP_CODES` A list of codes for HTTP errors that 72 | will be tracked with Exceptional. 73 | 74 | Defaults to standard HTTP 4xx codes. 75 | `EXCEPTIONAL_PARAMETER_FILTER` A list of values to filter from the 76 | parameter data sent to Exceptional. 77 | Parameter data includes everything 78 | in ``request.form`` and 79 | ``request.files``. 80 | 81 | For example, to filter passwords you 82 | might use: 83 | 84 | ``['password', 'password_confirm']`` 85 | `EXCEPTIONAL_ENVIRONMENT_FILTER` A list of values to filter from the 86 | environment data sent to Exceptional. 87 | The environment data includes the 88 | Flask application config plus the 89 | current OS environment. OS environment 90 | values are prefixed by ``'os.'``. 91 | 92 | For example, to filter the SQL 93 | Alchemy database URI and all OS 94 | environment values, use: 95 | 96 | ``['SQLALCHEMY_DATABASE_URI', 'os.*']`` 97 | 98 | Defaults to ``['SECRET_KEY']`` 99 | `EXCEPTIONAL_SESSION_FILTER` A list of values to filter from the 100 | session data sent to Exceptional. 101 | `EXCEPTIONAL_HEADER_FILTER` A list of values to filter from the 102 | HTTP header data sent to Exceptional. 103 | `EXCEPTIONAL_COOKIE_FILTER` A list of names to filter from the 104 | HTTP Cookie header data sent to 105 | Exceptional. 106 | =================================== ====================================== 107 | 108 | .. note:: All configuration filter lists accept both strings and regular 109 | expression patterns. 110 | 111 | API 112 | --- 113 | 114 | .. autoclass:: Exceptional 115 | :members: 116 | 117 | .. include:: ../CHANGES 118 | 119 | .. _Exceptional: http://www.exceptional.io/ 120 | .. _Flask: http://flask.pocoo.org/ 121 | .. _RequestBin: http://requestb.in/ 122 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-Exceptional documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Mar 20 10:52:05 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.append(os.path.abspath('_themes')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Flask-Exceptional' 44 | copyright = u'2012, Jonathan Zempel' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | import pkg_resources 50 | try: 51 | release = pkg_resources.get_distribution('Flask-Exceptional').version 52 | except pkg_resources.DistributionNotFound: 53 | print 'To build the documentation, The distribution information of' 54 | print 'Flask-Exceptional has to be available. Either install the package' 55 | print 'into your development environment or run "setup.py develop"' 56 | print 'to setup the metadata. A virtualenv is recommended!' 57 | sys.exit(1) 58 | del pkg_resources 59 | 60 | if 'dev' in release: 61 | release = release.split('dev')[0] + 'dev' 62 | version = '.'.join(release.split('.')[:2]) 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | #language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ['_build'] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all documents. 79 | #default_role = None 80 | 81 | # If true, '()' will be appended to :func: etc. cross-reference text. 82 | #add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | #add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | #show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | #pygments_style = 'sphinx' 94 | 95 | # A list of ignored prefixes for module index sorting. 96 | #modindex_common_prefix = [] 97 | 98 | 99 | # -- Options for HTML output --------------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | html_theme = 'flask_small' 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | html_theme_options = { 109 | 'index_logo': 'flask-exceptional.png', 110 | 'github_fork': 'jzempel/flask-exceptional' 111 | } 112 | 113 | # Add any paths that contain custom themes here, relative to this directory. 114 | html_theme_path = ['_themes'] 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | #html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | #html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | #html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | #html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | html_static_path = ['_static'] 136 | 137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 138 | # using the given strftime format. 139 | #html_last_updated_fmt = '%b %d, %Y' 140 | 141 | # If true, SmartyPants will be used to convert quotes and dashes to 142 | # typographically correct entities. 143 | #html_use_smartypants = True 144 | 145 | # Custom sidebar templates, maps document names to template names. 146 | #html_sidebars = {} 147 | 148 | # Additional templates that should be rendered to pages, maps page names to 149 | # template names. 150 | #html_additional_pages = {} 151 | 152 | # If false, no module index is generated. 153 | #html_domain_indices = True 154 | 155 | # If false, no index is generated. 156 | #html_use_index = True 157 | 158 | # If true, the index is split into individual pages for each letter. 159 | #html_split_index = False 160 | 161 | # If true, links to the reST sources are added to the pages. 162 | #html_show_sourcelink = True 163 | 164 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 165 | #html_show_sphinx = True 166 | 167 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 168 | #html_show_copyright = True 169 | 170 | # If true, an OpenSearch description file will be output, and all pages will 171 | # contain a tag referring to it. The value of this option must be the 172 | # base URL from which the finished HTML is served. 173 | #html_use_opensearch = '' 174 | 175 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 176 | #html_file_suffix = None 177 | 178 | # Output file base name for HTML help builder. 179 | htmlhelp_basename = 'Flask-Exceptionaldoc' 180 | 181 | 182 | # -- Options for LaTeX output -------------------------------------------------- 183 | 184 | # The paper size ('letter' or 'a4'). 185 | #latex_paper_size = 'letter' 186 | 187 | # The font size ('10pt', '11pt' or '12pt'). 188 | #latex_font_size = '10pt' 189 | 190 | # Grouping the document tree into LaTeX files. List of tuples 191 | # (source start file, target name, title, author, documentclass [howto/manual]). 192 | latex_documents = [ 193 | ('index', 'Flask-Exceptional.tex', u'Flask-Exceptional Documentation', 194 | u'Jonathan Zempel', 'manual'), 195 | ] 196 | 197 | # The name of an image file (relative to this directory) to place at the top of 198 | # the title page. 199 | #latex_logo = None 200 | 201 | # For "manual" documents, if this is true, then toplevel headings are parts, 202 | # not chapters. 203 | #latex_use_parts = False 204 | 205 | # If true, show page references after internal links. 206 | #latex_show_pagerefs = False 207 | 208 | # If true, show URL addresses after external links. 209 | #latex_show_urls = False 210 | 211 | # Additional stuff for the LaTeX preamble. 212 | #latex_preamble = '' 213 | 214 | # Documents to append as an appendix to all manuals. 215 | #latex_appendices = [] 216 | 217 | # If false, no module index is generated. 218 | #latex_domain_indices = True 219 | 220 | 221 | # -- Options for manual page output -------------------------------------------- 222 | 223 | # One entry per manual page. List of tuples 224 | # (source start file, name, description, authors, manual section). 225 | man_pages = [ 226 | ('index', 'flask-exceptional', u'Flask-Exceptional Documentation', 227 | [u'Jonathan Zempel'], 1) 228 | ] 229 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests 4 | ~~~~~ 5 | 6 | Flask Exceptional extension unit testing. 7 | 8 | :copyright: (c) 2011 by Jonathan Zempel. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | from __future__ import with_statement 13 | from flask import abort, Flask, g, json 14 | from flask.ext.exceptional import Exceptional 15 | from functools import wraps 16 | from os import environ 17 | from sys import exc_info 18 | from werkzeug.debug.tbtools import Traceback 19 | import unittest 20 | 21 | 22 | class ExceptionalTestCase(unittest.TestCase): 23 | """Exceptional extension test cases. 24 | """ 25 | 26 | debug_url = environ.get("EXCEPTIONAL_DEBUG_URL") 27 | 28 | @staticmethod 29 | def create_application(): 30 | """Create a test Flask application. 31 | """ 32 | ret_val = Flask(__name__) 33 | ret_val.testing = True 34 | ret_val.config["EXCEPTIONAL_API_KEY"] = "key" 35 | ret_val.config["EXCEPTIONAL_DEBUG_URL"] = environ.get( 36 | "EXCEPTIONAL_DEBUG_URL", "http://posttestserver.com/post.php") 37 | ret_val.config["PROPAGATE_EXCEPTIONS"] = False 38 | 39 | @ret_val.route("/error") 40 | def error(): 41 | """Do something that raises an exception. 42 | """ 43 | 1 / 0 44 | 45 | @ret_val.route("/http/") 46 | def http(code): 47 | """Raises an HTTP exception. 48 | """ 49 | abort(code) 50 | 51 | return ret_val 52 | 53 | def setUp(self): 54 | """Set up each test. 55 | """ 56 | self.app = self.create_application() 57 | self.exceptional = Exceptional(self.app) 58 | 59 | def test_01_exception(self): 60 | """Test mandatory data requirements for the Exceptional API. 61 | See http://docs.exceptional.io/api/publish for details. 62 | """ 63 | with self.app.test_client() as client: 64 | client.get("/error") 65 | data = json.loads(g.exceptional) 66 | exception = data["exception"] 67 | assert "backtrace" in exception 68 | assert "exception_class" in exception 69 | assert "message" in exception 70 | assert "occurred_at" in exception 71 | environment = data["application_environment"] 72 | assert environment[ 73 | "application_root_directory"] == self.app.root_path 74 | assert "env" in environment 75 | 76 | def test_02_http_exception(self): 77 | """Test logging an HTTP exception. 78 | """ 79 | with self.app.test_client() as client: 80 | client.get("/http/404") 81 | data = json.loads(g.exceptional) 82 | exception = data["exception"] 83 | assert "404" in exception["message"] 84 | 85 | def test_03_post_form(self): 86 | """Test POSTing form data. 87 | """ 88 | data = {"foo": "bar", "baz": "qux"} 89 | 90 | with self.app.test_client() as client: 91 | client.post("/error", data=data) 92 | data = json.loads(g.exceptional) 93 | request = data["request"] 94 | parameters = request["parameters"] 95 | assert parameters["foo"] == "bar" 96 | assert parameters["baz"] == "qux" 97 | 98 | def test_04_post_file(self): 99 | """Test POSTing file data. 100 | """ 101 | resource = self.app.open_resource("README") 102 | data = {"file": resource} 103 | 104 | with self.app.test_client() as client: 105 | client.post("/error", data=data) 106 | data = json.loads(g.exceptional) 107 | request = data["request"] 108 | parameters = request["parameters"] 109 | assert "file" in parameters 110 | 111 | def test_05_filter_header(self): 112 | """Test header data filtering. 113 | """ 114 | self.app.config["EXCEPTIONAL_HEADER_FILTER"] = ["Host"] 115 | Exceptional(self.app) 116 | 117 | with self.app.test_client() as client: 118 | client.get("/error") 119 | data = json.loads(g.exceptional) 120 | request = data["request"] 121 | headers = request["headers"] 122 | assert headers["Host"] == "[FILTERED]" 123 | 124 | def test_06_filter_parameter(self): 125 | """Test parameter data filtering. 126 | """ 127 | data = {"foo": "bar", "baz": "qux"} 128 | self.app.config["EXCEPTIONAL_PARAMETER_FILTER"] = ["baz"] 129 | Exceptional(self.app) 130 | 131 | with self.app.test_client() as client: 132 | client.post("/error", data=data) 133 | data = json.loads(g.exceptional) 134 | request = data["request"] 135 | parameters = request["parameters"] 136 | assert parameters["baz"] == "[FILTERED]" 137 | 138 | def test_07_unexceptional(self): 139 | """Test disabled Exceptional logging. 140 | """ 141 | self.app = self.create_application() 142 | del self.app.config["EXCEPTIONAL_API_KEY"] 143 | Exceptional(self.app) 144 | 145 | with self.app.test_client() as client: 146 | client.get("/error") 147 | assert hasattr(g, "exceptional") is False 148 | 149 | def test_08_http_unexceptional(self): 150 | """Test non-logged HTTP error code. 151 | """ 152 | with self.app.test_client() as client: 153 | client.get("/http/500") 154 | assert hasattr(g, "exceptional") is False 155 | 156 | def test_09_debug(self): 157 | """Test exception in debug mode. 158 | """ 159 | self.app = self.create_application() 160 | self.app.debug = True 161 | exceptional = Exceptional(self.app) 162 | self.app.config["EXCEPTIONAL_ENVIRONMENT_FILTER"].append("os.*") 163 | self.app.config["PROPAGATE_EXCEPTIONS"] = None 164 | assert exceptional.url == self.app.config["EXCEPTIONAL_DEBUG_URL"] 165 | 166 | with self.app.test_client() as client: 167 | self.assertRaises(ZeroDivisionError, client.get, "/error") 168 | json.loads(g.exceptional) 169 | print "See %s for HTTP request details." % exceptional.url 170 | 171 | def test_10_publish(self): 172 | """Test direct exception publishing. 173 | """ 174 | self.app = self.create_application() 175 | 176 | try: 177 | raise ValueError 178 | except ValueError: 179 | type, exception, traceback = exc_info() 180 | traceback = Traceback(type, exception, traceback) 181 | 182 | data = json.loads(Exceptional.publish(self.app.config, traceback)) 183 | exception = data["exception"] 184 | assert exception["exception_class"] == ValueError.__name__ 185 | 186 | def test_11_utf8_decode(self): 187 | """Test sending an invalid UTF-8 byte sequence through Exceptional. 188 | """ 189 | self.app.config["INVALID_UTF-8"] = "\xf0" 190 | Exceptional(self.app) 191 | 192 | with self.app.test_client() as client: 193 | client.get("/error") 194 | data = json.loads(g.exceptional) 195 | application_environment = data["application_environment"] 196 | environment = application_environment["env"] 197 | assert environment["INVALID_UTF-8"] == u"\ufffd" 198 | 199 | def test_12_json(self): 200 | """Test JSON request handling. 201 | """ 202 | self.app = self.create_application() 203 | Exceptional(self.app) 204 | data = json.dumps({"foo": {"bar": "baz"}}) 205 | 206 | with self.app.test_client() as client: 207 | client.post("/error", content_type="application/json", data=data) 208 | data = json.loads(g.exceptional) 209 | request = data["request"] 210 | parameters = request["parameters"] 211 | assert "bar" in parameters["foo"] 212 | 213 | def test_13_invalid_json(self): 214 | """Test invalid JSON request handling. 215 | """ 216 | data = '{"foo": {"bar": invalid}}' 217 | 218 | with self.app.test_client() as client: 219 | client.post("/error", content_type="application/json", data=data) 220 | data = json.loads(g.exceptional) 221 | request = data["request"] 222 | parameters = request["parameters"] 223 | assert "INVALID_JSON" in parameters 224 | 225 | def test_14_context(self): 226 | """Test exception context data. 227 | """ 228 | 229 | def exception_handler(app): 230 | handle_exception = app.handle_exception 231 | 232 | @wraps(handle_exception) 233 | def ret_val(exception): 234 | Exceptional.context(foo="bar") 235 | 236 | return handle_exception(exception) 237 | 238 | return ret_val 239 | 240 | handle_exception = self.app.handle_exception 241 | self.app.handle_exception = exception_handler(self.app) 242 | 243 | with self.app.test_client() as client: 244 | client.get("/error") 245 | data = json.loads(g.exceptional) 246 | context = data["context"] 247 | assert context["foo"] == "bar" 248 | 249 | self.app.handle_exception = handle_exception 250 | 251 | with self.app.test_client() as client: 252 | client.get("/error") 253 | data = json.loads(g.exceptional) 254 | context = data["context"] 255 | assert context is None 256 | 257 | if __name__ == "__main__": 258 | unittest.main() 259 | -------------------------------------------------------------------------------- /flask_exceptional.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_exceptional 4 | ~~~~~~~~~~~~~~~~~ 5 | 6 | Adds Exceptional support to Flask. 7 | 8 | :copyright: (c) 2012 by Jonathan Zempel. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | from __future__ import with_statement 13 | from Cookie import SimpleCookie 14 | from datetime import datetime 15 | from flask import _request_ctx_stack as stack, Config, Flask, g 16 | from functools import wraps 17 | from httplib import BadStatusLine 18 | from re import match 19 | from urllib2 import HTTPError, Request, urlopen, URLError 20 | from werkzeug import Headers 21 | from werkzeug.debug import tbtools 22 | from zlib import compress 23 | import os 24 | import sys 25 | 26 | try: 27 | from flask.json import _json as json 28 | except ImportError: 29 | from flask import json 30 | 31 | try: 32 | import pkg_resources 33 | except ImportError: 34 | pkg_resources = None # NOQA 35 | 36 | EXCEPTIONAL_URL = "http://api.exceptional.io/api/errors" 37 | 38 | 39 | class Exceptional(object): 40 | """Extension for tracking application errors with Exceptional. 41 | Errors are not tracked if DEBUG is True. The application will 42 | log a warning if no ``EXCEPTIONAL_API_KEY`` has been configured. 43 | 44 | :param app: Default None. The Flask application to track errors 45 | for. If the app is not provided on creation, then it 46 | can be provided later via :meth:`init_app`. 47 | """ 48 | 49 | def __init__(self, app=None): 50 | """Create this Exceptional extension. 51 | """ 52 | if app is not None: 53 | self.init_app(app) 54 | 55 | @property 56 | def __version__(self): 57 | """Get the version for this extension. 58 | """ 59 | if pkg_resources: 60 | ret_val = pkg_resources.get_distribution("flask-exceptional").version # NOQA 61 | else: 62 | ret_val = "unknown" 63 | 64 | return ret_val 65 | 66 | def init_app(self, app): 67 | """Initialize this Exceptional extension. 68 | 69 | :param app: The Flask application to track errors for. 70 | """ 71 | if "EXCEPTIONAL_API_KEY" in app.config: 72 | app.config.setdefault("EXCEPTIONAL_COOKIE_FILTER", None) 73 | app.config.setdefault("EXCEPTIONAL_ENVIRONMENT_FILTER", 74 | ["SECRET_KEY"]) 75 | app.config.setdefault("EXCEPTIONAL_HEADER_FILTER", None) 76 | app.config.setdefault("EXCEPTIONAL_PARAMETER_FILTER", None) 77 | app.config.setdefault("EXCEPTIONAL_SESSION_FILTER", None) 78 | app.config.setdefault("EXCEPTIONAL_HTTP_CODES", 79 | set(xrange(400, 418))) 80 | app.config.setdefault("EXCEPTIONAL_DEBUG_URL", None) 81 | self.__protocol_version = 5 # Using zlib compression. 82 | 83 | if app.debug: 84 | self.url = app.config["EXCEPTIONAL_DEBUG_URL"] 85 | elif app.testing: 86 | self.url = None 87 | else: 88 | self.url = "%s?api_key=%s&protocol_version=%d" % ( 89 | EXCEPTIONAL_URL, 90 | app.config["EXCEPTIONAL_API_KEY"], 91 | self.__protocol_version 92 | ) 93 | 94 | if not hasattr(app, "extensions"): 95 | app.extensions = {} 96 | 97 | if "exceptional" in app.extensions: 98 | app.logger.warning("Repeated Exceptional initialization attempt.") # NOQA 99 | else: 100 | app.handle_exception = self._get_exception_handler(app) 101 | app.handle_http_exception = self._get_http_exception_handler(app) # NOQA 102 | app.extensions["exceptional"] = self 103 | else: 104 | app.logger.warning("Missing 'EXCEPTIONAL_API_KEY' configuration.") 105 | 106 | @staticmethod 107 | def context(data=None, **kwargs): 108 | """Add extra context data to the current tracked exception. The context 109 | data is only valid for the current request. Multiple calls to this 110 | method will update any existing context with new data. 111 | 112 | :param data: Default ``None``. A dictionary of context data. 113 | :param kwargs: A series of keyword arguments to use as context data. 114 | """ 115 | context = getattr(stack.top, "exceptional_context", None) 116 | 117 | if context is None: 118 | context = {} 119 | setattr(stack.top, "exceptional_context", context) 120 | 121 | if data is not None: 122 | context.update(data) 123 | 124 | if len(kwargs): 125 | context.update(kwargs) 126 | 127 | @staticmethod 128 | def publish(config, traceback): 129 | """Publish the given traceback directly to Exceptional. This method is 130 | useful for tracking errors that occur outside the context of a Flask 131 | request. For example, this may be called from an asynchronous queue. 132 | 133 | :param config: A Flask application configuration object. Accepts either 134 | :class:`flask.Config` or the object types allowed by 135 | :meth:`flask.Config.from_object`. 136 | :param traceback: A :class:`werkzeug.debug.tbtools.Traceback` instance 137 | to publish. 138 | """ 139 | app = Flask(__name__) 140 | exceptional = Exceptional() 141 | 142 | if isinstance(config, Config): 143 | app.config = config 144 | else: 145 | app.config.from_object(config) 146 | 147 | exceptional.init_app(app) 148 | 149 | return exceptional._post_data(app, traceback=traceback) 150 | 151 | @staticmethod 152 | def test(config): 153 | """Test the given Flask configuration. If configured correctly, 154 | an error will be tracked by Exceptional for your app. Unlike 155 | the initialized extension, this test will post data to Exceptional, 156 | regardless of the configured ``DEBUG`` setting. 157 | 158 | :param config: The Flask application configuration object to test. 159 | Accepts either :class:`flask.Config` or the object 160 | types allowed by :meth:`flask.Config.from_object`. 161 | """ 162 | context = getattr(stack.top, "exceptional_context", None) 163 | app = Flask(__name__) 164 | exceptional = Exceptional() 165 | 166 | if isinstance(config, Config): 167 | app.config = config 168 | else: 169 | app.config.from_object(config) 170 | 171 | assert "EXCEPTIONAL_API_KEY" in app.config 172 | app.debug = False 173 | app.testing = False 174 | exceptional.init_app(app) 175 | app.testing = True 176 | 177 | @app.route("/exception") 178 | def exception(): 179 | setattr(stack.top, "exceptional_context", context) 180 | message = "Congratulations! Your application is configured for Exceptional error tracking." # NOQA 181 | 182 | raise Exception(message) 183 | 184 | with app.test_client() as client: 185 | client.get("/exception") 186 | json.loads(g.exceptional) 187 | 188 | def _get_exception_handler(self, app): 189 | """Get a wrapped exception handler. Returns a handler that can be 190 | used to override Flask's ``app.handle_exception``. The wrapped 191 | handler posts error data to Exceptional and then passes the exception 192 | to the wrapped method. 193 | 194 | :param app: The app for which the exception handler is being wrapped. 195 | """ 196 | handle_exception = app.handle_exception 197 | 198 | @wraps(handle_exception) 199 | def ret_val(exception): 200 | self._post_data(stack.top) 201 | 202 | return handle_exception(exception) 203 | 204 | return ret_val 205 | 206 | def _get_http_exception_handler(self, app): 207 | """Get a wrapped HTTP exception handler. Returns a handler that can 208 | be used to override Flask's ``app.handle_http_exception``. The wrapped 209 | handler posts HTTP error (i.e. '400: Bad Request') data to Exceptional 210 | and then passes the exception to the wrapped method. 211 | 212 | :param app: The app for which the HTTP exception handler is being 213 | wrapped. 214 | """ 215 | handle_http_exception = app.handle_http_exception 216 | 217 | @wraps(handle_http_exception) 218 | def ret_val(exception): 219 | context = stack.top 220 | 221 | if exception.code in context.app.config["EXCEPTIONAL_HTTP_CODES"]: 222 | self._post_data(context) 223 | 224 | return handle_http_exception(exception) 225 | 226 | return ret_val 227 | 228 | def _post_data(self, context, traceback=None): 229 | """POST data to the the Exceptional API. If DEBUG is True then data is 230 | sent to ``EXCEPTIONAL_DEBUG_URL`` if it has been defined. If TESTING is 231 | true, error data is stored in the global ``flask.g.exceptional`` 232 | variable. 233 | 234 | :param context: The current application or application context. 235 | :param traceback: Default ``None``. The exception stack trace. 236 | """ 237 | if context: 238 | if isinstance(context, Flask): 239 | app = context 240 | context = None 241 | else: 242 | app = context.app 243 | else: 244 | app = stack.top.app 245 | 246 | application_data = self.__get_application_data(app) 247 | client_data = { 248 | "name": "flask-exceptional", 249 | "version": self.__version__, 250 | "protocol_version": self.__protocol_version 251 | } 252 | 253 | if context: 254 | request_data = self.__get_request_data(app, context.request, 255 | context.session) 256 | context_data = getattr(context, "exceptional_context", None) 257 | else: 258 | request_data = None 259 | context_data = None 260 | 261 | traceback = traceback or tbtools.get_current_traceback() 262 | exception_data = self.__get_exception_data(traceback) 263 | encode_basestring = json.encoder.encode_basestring 264 | 265 | def _encode_basestring(value): 266 | if isinstance(value, str) and \ 267 | json.encoder.HAS_UTF8.search(value) is not None: 268 | value = value.decode("utf-8", 269 | "replace") # ensure the decode succeeds. 270 | 271 | replace = lambda match: json.encoder.ESCAPE_DCT[match.group(0)] 272 | 273 | return u'"%s"' % json.encoder.ESCAPE.sub(replace, value) 274 | 275 | try: 276 | json.encoder.encode_basestring = _encode_basestring 277 | ret_val = json.dumps({ 278 | "application_environment": application_data, 279 | "client": client_data, 280 | "request": request_data, 281 | "exception": exception_data, 282 | "context": context_data 283 | }, ensure_ascii=False).encode("utf-8") 284 | finally: 285 | json.encoder.encode_basestring = encode_basestring 286 | 287 | if context and app.testing: 288 | g.exceptional = ret_val 289 | 290 | if self.url: 291 | request = Request(self.url) 292 | request.add_header("Content-Type", "application/json") 293 | 294 | if app.debug: 295 | data = ret_val 296 | else: 297 | request.add_header("Content-Encoding", "deflate") 298 | data = compress(ret_val, 1) 299 | 300 | try: 301 | try: 302 | urlopen(request, data) 303 | except HTTPError, e: 304 | if e.code >= 400: 305 | raise 306 | except URLError: 307 | message = "Unable to connect to %s. See http://status.exceptional.io for details. Error data:\n%s" # NOQA 308 | app.logger.warning(message, self.url, ret_val, 309 | exc_info=True) 310 | except BadStatusLine: 311 | pass 312 | 313 | return ret_val 314 | 315 | @staticmethod 316 | def __filter(app, data, filter_name): 317 | """Filter sensitive data. 318 | """ 319 | filter = app.config.get(filter_name) 320 | 321 | if filter: 322 | ret_val = {} 323 | 324 | for key, value in data.iteritems(): 325 | for item in filter: 326 | if match(item, key): 327 | value = "[FILTERED]" 328 | break 329 | 330 | ret_val[key] = value 331 | else: 332 | ret_val = dict(data) 333 | 334 | return ret_val 335 | 336 | @staticmethod 337 | def __get_application_data(app): 338 | """Get application data. 339 | """ 340 | environment = {} 341 | 342 | for name in app.config: 343 | value = app.config[name] 344 | environment[name] = str(value) if value else None 345 | 346 | for name in os.environ: 347 | value = os.environ[name] 348 | environment["os.%s" % name] = str(value) 349 | 350 | if pkg_resources: 351 | modules = {} 352 | 353 | for module in pkg_resources.working_set: 354 | modules[module.project_name] = module.version 355 | else: 356 | modules = None 357 | 358 | return { 359 | "framework": "flask", 360 | "env": Exceptional.__filter(app, environment, 361 | "EXCEPTIONAL_ENVIRONMENT_FILTER"), 362 | "language": "python", 363 | "language_version": sys.version.replace('\n', ''), 364 | "application_root_directory": app.root_path, 365 | "loaded_libraries": modules 366 | } 367 | 368 | @staticmethod 369 | def __get_exception_data(traceback): 370 | """Get exception data. 371 | """ 372 | timestamp = datetime.utcnow() 373 | backtrace = [] 374 | 375 | for frame in traceback.frames: 376 | backtrace.insert(0, "File \"%s\", line %d, in %s\n\t%s" % ( 377 | frame.filename, 378 | frame.lineno, 379 | frame.function_name, 380 | frame.current_line.strip() 381 | )) 382 | 383 | return { 384 | "occurred_at": "%sZ" % timestamp.isoformat(), 385 | "message": traceback.exception.split(': ', 1)[-1], 386 | "backtrace": backtrace, 387 | "exception_class": traceback.exception_type 388 | } 389 | 390 | @staticmethod 391 | def __get_request_data(app, request, session): 392 | """Get request data. 393 | """ 394 | try: 395 | parameters = request.json or {} 396 | except: 397 | parameters = {"INVALID_JSON": request.data} 398 | 399 | form = request.form.to_dict(flat=False) 400 | 401 | for key, value in form.iteritems(): 402 | if len(value) == 1: 403 | parameters[key] = value[0] 404 | else: 405 | parameters[key] = value 406 | 407 | files = request.files.to_dict(flat=False) 408 | 409 | for key, value in files.iteritems(): 410 | if len(value) == 1: 411 | parameters[key] = value[0].filename 412 | else: 413 | parameters[key] = [file.filename for file in value] 414 | 415 | if request.cookies: 416 | cookies = Exceptional.__filter(app, request.cookies, 417 | "EXCEPTIONAL_COOKIE_FILTER") 418 | headers = Headers(request.headers) # Get a mutable dictionary. 419 | cookie = SimpleCookie() 420 | 421 | for key, value in cookies.iteritems(): 422 | cookie[key] = value 423 | 424 | headers["Cookie"] = cookie.output(header='', sep=';').strip() 425 | else: 426 | headers = request.headers 427 | 428 | return { 429 | "session": Exceptional.__filter(app, session, 430 | "EXCEPTIONAL_SESSION_FILTER"), 431 | "remote_ip": request.remote_addr, 432 | "parameters": Exceptional.__filter(app, parameters, 433 | "EXCEPTIONAL_PARAMETER_FILTER"), 434 | "action": request.endpoint.split('.', 1)[-1] if request.endpoint 435 | else None, 436 | "url": request.url, 437 | "request_method": request.method, 438 | "controller": request.blueprint if hasattr(request, "blueprint") 439 | else request.module, 440 | "headers": Exceptional.__filter(app, headers, 441 | "EXCEPTIONAL_HEADER_FILTER") 442 | } 443 | --------------------------------------------------------------------------------